From d886979e3b5a841f0d839a22fdf02cbc4bae9ec8 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 23 Mar 2021 11:34:20 -0700 Subject: [PATCH 01/88] [Fleet] Remove `upgradePackage` and consolidate it with `installPackage`, optimize calls to create index patterns (#94490) * Add data plugin to server app context * First attempt at switching to indexPatternService for EPM index pattern creation & deletion, instead of interacting directly with index pattern SOs * Simplify bulk install package, remove upgradePackage() method in favor of consolidating with installPackage(), use installPackage() for bulk install instead * Update tests * Change cache logging of objects to trace level * Install index patterns as a post-package installation operation, for bulk package installation, install index pattern only if one or more packages are actually new installs, add debug logging * Allow getAsSavedObjectBody to return non-scripted fields when allowNoIndex is true * Allow `getFieldsForWildcard` to return fields saved on index pattern when allowNoIndices is true * Bring back passing custom ID for index pattern SO * Fix tests * Revert "Merge branch 'index-pattern/allow-no-index' into epm/missing-index-patterns" This reverts commit 8e712e9c0087fc7f9e2b3dc0c729a23f0265f243, reversing changes made to af0fb0eaa84301ffe8514b89acc1a45ca2c5f7cb. * Allow getAsSavedObjectBody to return non-scripted fields when allowNoIndex is true (cherry picked from commit 69b93da1807cc9a613be1ad75b37df6bcee8d676) * Update API docs * Run post-install ops for install by upload too * Remove allowedInstallTypes param * Adjust force install conditions * Revert "Update API docs" This reverts commit b9770fdc5645b6d331bbbb3f0029ae039fbb1871. * Revert "Allow getAsSavedObjectBody to return non-scripted fields when allowNoIndex is true" This reverts commit afc91ce32f4d7c418be8a85d796ac5f71d6028eb. * Go back to using SO client for index patterns :( * Stringify attributes again for SO client saving * Fix condition for reinstall same pkg version * Remove unused type * Adjust comment * Update snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/common/constants/epm.ts | 1 - .../fleet/common/types/rest_spec/epm.ts | 11 +- .../plugins/fleet/server/constants/index.ts | 1 - x-pack/plugins/fleet/server/mocks/index.ts | 2 + x-pack/plugins/fleet/server/plugin.ts | 4 + .../fleet/server/routes/epm/handlers.ts | 31 +-- .../fleet/server/services/app_context.ts | 10 + .../server/services/epm/archive/cache.ts | 18 +- .../__snapshots__/install.test.ts.snap | 218 ++++++---------- .../epm/kibana/index_pattern/install.test.ts | 45 +--- .../epm/kibana/index_pattern/install.ts | 122 ++++----- .../kibana/index_pattern/tests/test_data.ts | 20 +- .../services/epm/packages/_install_package.ts | 15 +- .../epm/packages/bulk_install_packages.ts | 88 ++++--- .../ensure_installed_default_packages.test.ts | 25 +- .../server/services/epm/packages/install.ts | 239 ++++++++++-------- .../server/services/epm/packages/remove.ts | 2 +- x-pack/plugins/fleet/server/types/index.tsx | 1 + .../apis/epm/bulk_upgrade.ts | 15 +- 19 files changed, 371 insertions(+), 497 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index aa17b16b3763c..faa1127cfe1da 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -7,7 +7,6 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const ASSETS_SAVED_OBJECT_TYPE = 'epm-packages-assets'; -export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const MAX_TIME_COMPLETE_INSTALL = 60000; export const FLEET_SERVER_PACKAGE = 'fleet_server'; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index 3a9c9a0cfae9f..3c7a32265d20a 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -82,12 +82,15 @@ export interface IBulkInstallPackageHTTPError { error: string | Error; } +export interface InstallResult { + assets: AssetReference[]; + status: 'installed' | 'already_installed'; +} + export interface BulkInstallPackageInfo { name: string; - newVersion: string; - // this will be null if no package was present before the upgrade (aka it was an install) - oldVersion: string | null; - assets: AssetReference[]; + version: string; + result: InstallResult; } export interface BulkInstallPackagesResponse { diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index fa6051fa7d35b..23df18d5e377d 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -43,7 +43,6 @@ export { OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, - INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, // Defaults diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index d4b6f007feb4d..4bc2bea1e58b6 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -10,6 +10,7 @@ import { savedObjectsServiceMock, coreMock, } from '../../../../../src/core/server/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../../security/server/mocks'; @@ -23,6 +24,7 @@ export * from '../services/artifacts/mocks'; export const createAppContextStartContractMock = (): FleetAppContext => { return { elasticsearch: elasticsearchServiceMock.createStart(), + data: dataPluginMock.createStartContract(), encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), savedObjects: savedObjectsServiceMock.createStartContract(), security: securityMock.createStart(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index a62da8eb41a99..477e6c3959951 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -23,6 +23,7 @@ import type { import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import type { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; import type { LicensingPluginSetup, ILicense } from '../../licensing/server'; import type { EncryptedSavedObjectsPluginStart, @@ -100,12 +101,14 @@ export interface FleetSetupDeps { } export interface FleetStartDeps { + data: DataPluginStart; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security?: SecurityPluginStart; } export interface FleetAppContext { elasticsearch: ElasticsearchServiceStart; + data: DataPluginStart; encryptedSavedObjectsStart?: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; @@ -293,6 +296,7 @@ export class FleetPlugin public async start(core: CoreStart, plugins: FleetStartDeps): Promise { await appContextService.start({ elasticsearch: core.elasticsearch, + data: plugins.data, encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: plugins.security, diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 293a3e9e28237..3ac951f7987f8 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -40,12 +40,10 @@ import { getPackages, getFile, getPackageInfo, - handleInstallPackageFailure, isBulkInstallError, installPackage, removeInstallation, getLimitedPackages, - getInstallationObject, getInstallation, } from '../../services/epm/packages'; import type { BulkInstallResponse } from '../../services/epm/packages'; @@ -228,15 +226,7 @@ export const installPackageFromRegistryHandler: RequestHandler< const savedObjectsClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; const { pkgkey } = request.params; - - let pkgName: string | undefined; - let pkgVersion: string | undefined; - try { - const parsedPkgKey = splitPkgKey(pkgkey); - pkgName = parsedPkgKey.pkgName; - pkgVersion = parsedPkgKey.pkgVersion; - const res = await installPackage({ installSource: 'registry', savedObjectsClient, @@ -245,24 +235,11 @@ export const installPackageFromRegistryHandler: RequestHandler< force: request.body?.force, }); const body: InstallPackageResponse = { - response: res, + response: res.assets, }; return response.ok({ body }); } catch (e) { - const defaultResult = await defaultIngestErrorHandler({ error: e, response }); - if (pkgName && pkgVersion) { - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - await handleInstallPackageFailure({ - savedObjectsClient, - error: e, - pkgName, - pkgVersion, - installedPkg, - esClient, - }); - } - - return defaultResult; + return await defaultIngestErrorHandler({ error: e, response }); } }; @@ -291,7 +268,7 @@ export const bulkInstallPackagesFromRegistryHandler: RequestHandler< const bulkInstalledResponses = await bulkInstallPackages({ savedObjectsClient, esClient, - packagesToUpgrade: request.body.packages, + packagesToInstall: request.body.packages, }); const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry); const body: BulkInstallPackagesResponse = { @@ -324,7 +301,7 @@ export const installPackageByUploadHandler: RequestHandler< contentType, }); const body: InstallPackageResponse = { - response: res, + response: res.assets, }; return response.ok({ body }); } catch (error) { diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 21b519565e758..954308a980861 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -17,6 +17,7 @@ import type { Logger, } from 'src/core/server'; +import type { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; import type { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, @@ -29,6 +30,7 @@ import type { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; + private data: DataPluginStart | undefined; private esClient: ElasticsearchClient | undefined; private security: SecurityPluginStart | undefined; private config$?: Observable; @@ -43,6 +45,7 @@ class AppContextService { private externalCallbacks: ExternalCallbacksStorage = new Map(); public async start(appContext: FleetAppContext) { + this.data = appContext.data; this.esClient = appContext.elasticsearch.client.asInternalUser; this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); this.encryptedSavedObjectsSetup = appContext.encryptedSavedObjectsSetup; @@ -67,6 +70,13 @@ class AppContextService { this.externalCallbacks.clear(); } + public getData() { + if (!this.data) { + throw new Error('Data start service not set.'); + } + return this.data; + } + public getEncryptedSavedObjects() { if (!this.encryptedSavedObjects) { throw new Error('Encrypted saved object start service not set.'); diff --git a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts index 6958f76c17b70..7f479dc5d6b63 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts @@ -28,13 +28,9 @@ export const getArchiveFilelist = (keyArgs: SharedKey) => archiveFilelistCache.get(sharedKey(keyArgs)); export const setArchiveFilelist = (keyArgs: SharedKey, paths: string[]) => { - appContextService - .getLogger() - .debug( - `setting file list to the cache for ${keyArgs.name}-${keyArgs.version}:\n${JSON.stringify( - paths - )}` - ); + const logger = appContextService.getLogger(); + logger.debug(`setting file list to the cache for ${keyArgs.name}-${keyArgs.version}`); + logger.trace(JSON.stringify(paths)); return archiveFilelistCache.set(sharedKey(keyArgs), paths); }; @@ -63,12 +59,10 @@ export const setPackageInfo = ({ version, packageInfo, }: SharedKey & { packageInfo: ArchivePackage | RegistryPackage }) => { + const logger = appContextService.getLogger(); const key = sharedKey({ name, version }); - appContextService - .getLogger() - .debug( - `setting package info to the cache for ${name}-${version}:\n${JSON.stringify(packageInfo)}` - ); + logger.debug(`setting package info to the cache for ${name}-${version}`); + logger.trace(JSON.stringify(packageInfo)); return packageInfoCache.set(key, packageInfo); }; diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap index 862e82589b9bc..da870290329a8 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap @@ -40,7 +40,7 @@ exports[`creating index patterns from yaml fields createIndexPattern function cr { "title": "logs-*", "timeFieldName": "@timestamp", - "fields": "[{\\"name\\":\\"coredns.id\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"coredns.allParams\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"number\\"},{\\"name\\":\\"coredns.query.length\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"number\\"},{\\"name\\":\\"coredns.query.size\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"number\\"},{\\"name\\":\\"coredns.query.class\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"coredns.query.name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"coredns.query.type\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"coredns.response.code\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"coredns.response.flags\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"coredns.response.size\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"number\\"},{\\"name\\":\\"coredns.dnssec_ok\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"boolean\\"},{\\"name\\":\\"@timestamp\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"date\\"},{\\"name\\":\\"labels\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"message\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":false,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"tags\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"agent.ephemeral_id\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"agent.id\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"agent.name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"agent.type\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"agent.version\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"as.number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"number\\"},{\\"name\\":\\"as.organization.name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"nginx.access.remote_ip_list\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.body_sent.bytes\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.method\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.url\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.http_version\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.response_code\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.referrer\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.agent\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.device\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.os\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.os_name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.original\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.continent_name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":false,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"nginx.access.geoip.country_iso_code\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.location\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.region_name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.city_name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.region_iso_code\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"source.geo.continent_name\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":false,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"country\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"country.keyword\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":true,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"},{\\"name\\":\\"country.text\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"analyzed\\":false,\\"searchable\\":true,\\"aggregatable\\":false,\\"doc_values\\":true,\\"readFromDocValues\\":true,\\"type\\":\\"string\\"}]", + "fields": "[{\\"name\\":\\"coredns.id\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.allParams\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.length\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.size\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.class\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.type\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.response.code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.response.flags\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.response.size\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.dnssec_ok\\",\\"type\\":\\"boolean\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"@timestamp\\",\\"type\\":\\"date\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"labels\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"message\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true},{\\"name\\":\\"tags\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.ephemeral_id\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.id\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.type\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.version\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"as.number\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"as.organization.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.remote_ip_list\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.body_sent.bytes\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.method\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.url\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.http_version\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.response_code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.referrer\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.agent\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.device\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.os\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.os_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.original\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.continent_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.country_iso_code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.location\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.region_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.city_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.region_iso_code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"source.geo.continent_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true},{\\"name\\":\\"country\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"country.keyword\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"country.text\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true}]", "fieldFormatMap": "{\\"coredns.allParams\\":{\\"id\\":\\"bytes\\",\\"params\\":{\\"pattern\\":\\"patternValQueryWeight\\",\\"inputFormat\\":\\"inputFormatVal,\\",\\"outputFormat\\":\\"outputFormalVal,\\",\\"outputPrecision\\":\\"3,\\",\\"labelTemplate\\":\\"labelTemplateVal,\\",\\"urlTemplate\\":\\"urlTemplateVal,\\"}},\\"coredns.query.length\\":{\\"params\\":{\\"pattern\\":\\"patternValQueryLength\\"}},\\"coredns.query.size\\":{\\"id\\":\\"bytes\\",\\"params\\":{\\"pattern\\":\\"patternValQuerySize\\"}},\\"coredns.response.size\\":{\\"id\\":\\"bytes\\"}}", "allowNoIndex": true } @@ -51,535 +51,463 @@ exports[`creating index patterns from yaml fields createIndexPatternFields funct "indexPatternFields": [ { "name": "coredns.id", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "coredns.allParams", + "type": "number", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "number" + "readFromDocValues": true }, { "name": "coredns.query.length", + "type": "number", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "number" + "readFromDocValues": true }, { "name": "coredns.query.size", + "type": "number", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "number" + "readFromDocValues": true }, { "name": "coredns.query.class", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "coredns.query.name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "coredns.query.type", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "coredns.response.code", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "coredns.response.flags", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "coredns.response.size", + "type": "number", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "number" + "readFromDocValues": true }, { "name": "coredns.dnssec_ok", + "type": "boolean", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "boolean" + "readFromDocValues": true }, { "name": "@timestamp", + "type": "date", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "date" + "readFromDocValues": true }, { "name": "labels", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "message", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": false, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "tags", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "agent.ephemeral_id", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "agent.id", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "agent.name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "agent.type", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "agent.version", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "as.number", + "type": "number", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "number" + "readFromDocValues": true }, { "name": "as.organization.name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "nginx.access.remote_ip_list", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.body_sent.bytes", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.user_name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.method", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.url", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.http_version", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.response_code", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.referrer", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.agent", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.user_agent.device", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.user_agent.name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.user_agent.os", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.user_agent.os_name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.user_agent.original", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.geoip.continent_name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": false, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "nginx.access.geoip.country_iso_code", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.geoip.location", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.geoip.region_name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.geoip.city_name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "nginx.access.geoip.region_iso_code", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, "readFromDocValues": true }, { "name": "source.geo.continent_name", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": false, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "country", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "country.keyword", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": true, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true }, { "name": "country.text", + "type": "string", "count": 0, "scripted": false, "indexed": true, - "analyzed": false, "searchable": true, "aggregatable": false, - "doc_values": true, - "readFromDocValues": true, - "type": "string" + "readFromDocValues": true } ], "fieldFormatMap": { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts index a0eaed04d649e..dfdaa66a7b43e 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts @@ -11,6 +11,8 @@ import { readFileSync } from 'fs'; import glob from 'glob'; import { safeLoad } from 'js-yaml'; +import type { FieldSpec } from 'src/plugins/data/common'; + import type { Fields, Field } from '../../fields/field'; import { @@ -22,7 +24,6 @@ import { createIndexPatternFields, createIndexPattern, } from './install'; -import type { IndexPatternField } from './install'; import { dupeFields } from './tests/test_data'; // Add our own serialiser to just do JSON.stringify @@ -93,7 +94,6 @@ describe('creating index patterns from yaml fields', () => { const mergedField = deduped.find((field) => field.name === '1'); expect(mergedField?.searchable).toBe(true); expect(mergedField?.aggregatable).toBe(true); - expect(mergedField?.analyzed).toBe(true); expect(mergedField?.count).toBe(0); }); }); @@ -156,7 +156,7 @@ describe('creating index patterns from yaml fields', () => { { fields: [{ name: 'testField', type: 'short' }], expect: 'number' }, { fields: [{ name: 'testField', type: 'byte' }], expect: 'number' }, { fields: [{ name: 'testField', type: 'keyword' }], expect: 'string' }, - { fields: [{ name: 'testField', type: 'invalidType' }], expect: undefined }, + { fields: [{ name: 'testField', type: 'invalidType' }], expect: 'string' }, { fields: [{ name: 'testField', type: 'text' }], expect: 'string' }, { fields: [{ name: 'testField', type: 'date' }], expect: 'date' }, { fields: [{ name: 'testField', type: 'geo_point' }], expect: 'geo_point' }, @@ -171,7 +171,7 @@ describe('creating index patterns from yaml fields', () => { test('transformField changes values based on other values', () => { interface TestWithAttr extends Test { - attr: keyof IndexPatternField; + attr: keyof FieldSpec; } const tests: TestWithAttr[] = [ @@ -211,43 +211,6 @@ describe('creating index patterns from yaml fields', () => { attr: 'aggregatable', }, - // analyzed - { fields: [{ name }], expect: false, attr: 'analyzed' }, - { fields: [{ name, analyzed: true }], expect: true, attr: 'analyzed' }, - { fields: [{ name, analyzed: false }], expect: false, attr: 'analyzed' }, - { fields: [{ name, type: 'binary' }], expect: false, attr: 'analyzed' }, - { fields: [{ name, analyzed: true, type: 'binary' }], expect: false, attr: 'analyzed' }, - { - fields: [{ name, analyzed: true, type: 'object', enabled: false }], - expect: false, - attr: 'analyzed', - }, - - // doc_values always set to true except for meta fields - { fields: [{ name }], expect: true, attr: 'doc_values' }, - { fields: [{ name, doc_values: true }], expect: true, attr: 'doc_values' }, - { fields: [{ name, doc_values: false }], expect: false, attr: 'doc_values' }, - { fields: [{ name, script: 'doc[]' }], expect: false, attr: 'doc_values' }, - { fields: [{ name, doc_values: true, script: 'doc[]' }], expect: false, attr: 'doc_values' }, - { fields: [{ name, type: 'binary' }], expect: false, attr: 'doc_values' }, - { fields: [{ name, doc_values: true, type: 'binary' }], expect: true, attr: 'doc_values' }, - { - fields: [{ name, doc_values: true, type: 'object', enabled: false }], - expect: false, - attr: 'doc_values', - }, - - // enabled - only applies to objects (and only if set) - { fields: [{ name, type: 'binary', enabled: false }], expect: undefined, attr: 'enabled' }, - { fields: [{ name, type: 'binary', enabled: true }], expect: undefined, attr: 'enabled' }, - { fields: [{ name, type: 'object', enabled: true }], expect: true, attr: 'enabled' }, - { fields: [{ name, type: 'object', enabled: false }], expect: false, attr: 'enabled' }, - { - fields: [{ name, type: 'object', enabled: false }], - expect: false, - attr: 'doc_values', - }, - // indexed { fields: [{ name, type: 'binary' }], expect: false, attr: 'indexed' }, { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts index 3ec9d2c65a6da..61d6f6ed8818a 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { SavedObjectsClientContract } from 'src/core/server'; +import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import type { FieldSpec } from 'src/plugins/data/common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../../constants'; import { loadFieldsFromYaml } from '../../fields/field'; import type { Fields, Field } from '../../fields/field'; import { dataTypes, installationStatuses } from '../../../../../common/constants'; @@ -17,6 +17,7 @@ import type { InstallSource, ValueOf, } from '../../../../../common/types'; +import { appContextService } from '../../../../services'; import type { RegistryPackage, DataType } from '../../../../types'; import { getInstallation, getPackageFromSource, getPackageSavedObjects } from '../../packages/get'; @@ -59,29 +60,29 @@ const typeMap: TypeMap = { constant_keyword: 'string', }; -export interface IndexPatternField { - name: string; - type?: string; - count: number; - scripted: boolean; - indexed: boolean; - analyzed: boolean; - searchable: boolean; - aggregatable: boolean; - doc_values: boolean; - enabled?: boolean; - script?: string; - lang?: string; - readFromDocValues: boolean; -} +const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const indexPatternTypes = Object.values(dataTypes); -export async function installIndexPatterns( - savedObjectsClient: SavedObjectsClientContract, - pkgName?: string, - pkgVersion?: string, - installSource?: InstallSource -) { +export async function installIndexPatterns({ + savedObjectsClient, + pkgName, + pkgVersion, + installSource, +}: { + savedObjectsClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; + pkgName?: string; + pkgVersion?: string; + installSource?: InstallSource; +}) { + const logger = appContextService.getLogger(); + + logger.debug( + `kicking off installation of index patterns for ${ + pkgName && pkgVersion ? `${pkgName}-${pkgVersion}` : 'no specific package' + }` + ); + // get all user installed packages const installedPackagesRes = await getPackageSavedObjects(savedObjectsClient); const installedPackagesSavedObjects = installedPackagesRes.saved_objects.filter( @@ -115,6 +116,7 @@ export async function installIndexPatterns( }); } } + // get each package's registry info const packagesToFetchPromise = packagesToFetch.map((pkg) => getPackageFromSource({ @@ -125,27 +127,33 @@ export async function installIndexPatterns( }) ); const packages = await Promise.all(packagesToFetchPromise); + // for each index pattern type, create an index pattern - indexPatternTypes.forEach(async (indexPatternType) => { - // if this is an update because a package is being uninstalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern - if (!pkgName && installedPackagesSavedObjects.length === 0) { - try { - await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); - } catch (err) { - // index pattern was probably deleted by the user already + return Promise.all( + indexPatternTypes.map(async (indexPatternType) => { + // if this is an update because a package is being uninstalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern + if (!pkgName && installedPackagesSavedObjects.length === 0) { + try { + logger.debug(`deleting index pattern ${indexPatternType}-*`); + await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); + } catch (err) { + // index pattern was probably deleted by the user already + } + return; } - return; - } - const packagesWithInfo = packages.map((pkg) => pkg.packageInfo); - // get all data stream fields from all installed packages - const fields = await getAllDataStreamFieldsByType(packagesWithInfo, indexPatternType); - const kibanaIndexPattern = createIndexPattern(indexPatternType, fields); - // create or overwrite the index pattern - await savedObjectsClient.create(INDEX_PATTERN_SAVED_OBJECT_TYPE, kibanaIndexPattern, { - id: `${indexPatternType}-*`, - overwrite: true, - }); - }); + const packagesWithInfo = packages.map((pkg) => pkg.packageInfo); + // get all data stream fields from all installed packages + const fields = await getAllDataStreamFieldsByType(packagesWithInfo, indexPatternType); + const kibanaIndexPattern = createIndexPattern(indexPatternType, fields); + + // create or overwrite the index pattern + await savedObjectsClient.create(INDEX_PATTERN_SAVED_OBJECT_TYPE, kibanaIndexPattern, { + id: `${indexPatternType}-*`, + overwrite: true, + }); + logger.debug(`created index pattern ${kibanaIndexPattern.title}`); + }) + ); } // loops through all given packages and returns an array @@ -189,7 +197,7 @@ export const createIndexPattern = (indexPatternType: string, fields: Fields) => // and also returns the fieldFormatMap export const createIndexPatternFields = ( fields: Fields -): { indexPatternFields: IndexPatternField[]; fieldFormatMap: FieldFormatMap } => { +): { indexPatternFields: FieldSpec[]; fieldFormatMap: FieldFormatMap } => { const flattenedFields = flattenFields(fields); const fieldFormatMap = createFieldFormatMap(flattenedFields); const transformedFields = flattenedFields.map(transformField); @@ -198,8 +206,8 @@ export const createIndexPatternFields = ( }; // merges fields that are duplicates with the existing taking precedence -export const dedupeFields = (fields: IndexPatternField[]) => { - const uniqueObj = fields.reduce<{ [name: string]: IndexPatternField }>((acc, field) => { +export const dedupeFields = (fields: FieldSpec[]) => { + const uniqueObj = fields.reduce<{ [name: string]: FieldSpec }>((acc, field) => { // if field doesn't exist yet if (!acc[field.name]) { acc[field.name] = field; @@ -251,34 +259,20 @@ const getField = (fields: Fields, pathNames: string[]): Field | undefined => { return undefined; }; -export const transformField = (field: Field, i: number, fields: Fields): IndexPatternField => { - const newField: IndexPatternField = { +export const transformField = (field: Field, i: number, fields: Fields): FieldSpec => { + const newField: FieldSpec = { name: field.name, + type: field.type && typeMap[field.type] ? typeMap[field.type] : 'string', count: field.count ?? 0, scripted: false, indexed: field.index ?? true, - analyzed: field.analyzed ?? false, searchable: field.searchable ?? true, aggregatable: field.aggregatable ?? true, - doc_values: field.doc_values ?? true, readFromDocValues: field.doc_values ?? true, }; - // if type exists, check if it exists in the map - if (field.type) { - // if no type match type is not set (undefined) - if (typeMap[field.type]) { - newField.type = typeMap[field.type]; - } - // if type isn't set, default to string - } else { - newField.type = 'string'; - } - if (newField.type === 'binary') { newField.aggregatable = false; - newField.analyzed = false; - newField.doc_values = field.doc_values ?? false; newField.readFromDocValues = field.doc_values ?? false; newField.indexed = false; newField.searchable = false; @@ -286,11 +280,8 @@ export const transformField = (field: Field, i: number, fields: Fields): IndexPa if (field.type === 'object' && field.hasOwnProperty('enabled')) { const enabled = field.enabled ?? true; - newField.enabled = enabled; if (!enabled) { newField.aggregatable = false; - newField.analyzed = false; - newField.doc_values = false; newField.readFromDocValues = false; newField.indexed = false; newField.searchable = false; @@ -305,7 +296,6 @@ export const transformField = (field: Field, i: number, fields: Fields): IndexPa newField.scripted = true; newField.script = field.script; newField.lang = 'painless'; - newField.doc_values = false; newField.readFromDocValues = false; } diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts index 49a32de703776..d9bcf36651081 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { IndexPatternField } from '../install'; +import type { FieldSpec } from 'src/plugins/data/common'; -export const dupeFields: IndexPatternField[] = [ +export const dupeFields: FieldSpec[] = [ { name: '1', type: 'integer', @@ -15,10 +15,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: true, count: 0, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '2', @@ -27,10 +25,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: true, count: 0, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '3', @@ -39,10 +35,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: true, count: 0, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '1', @@ -51,10 +45,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: false, count: 2, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '1.1', @@ -63,10 +55,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: false, count: 0, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '4', @@ -75,10 +65,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: false, count: 0, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '2', @@ -87,10 +75,8 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: false, count: 0, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: true, }, { name: '1', @@ -99,9 +85,7 @@ export const dupeFields: IndexPatternField[] = [ aggregatable: false, count: 1, indexed: true, - doc_values: true, readFromDocValues: true, scripted: false, - analyzed: false, }, ]; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index ca9d490609636..65d71ac5fdc17 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -12,7 +12,6 @@ import type { InstallablePackage, InstallSource, PackageAssetReference } from '. import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { ElasticsearchAssetType } from '../../../types'; import type { AssetReference, Installation, InstallType } from '../../../types'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; import { installTemplates } from '../elasticsearch/template/install'; import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; @@ -81,11 +80,11 @@ export async function _installPackage({ }); } - // kick off `installIndexPatterns` & `installKibanaAssets` as early as possible because they're the longest running operations + // kick off `installKibanaAssets` as early as possible because they're the longest running operations // we don't `await` here because we don't want to delay starting the many other `install*` functions // however, without an `await` or a `.catch` we haven't defined how to handle a promise rejection // we define it many lines and potentially seconds of wall clock time later in - // `await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]);` + // `await installKibanaAssetsPromise` // if we encounter an error before we there, we'll have an "unhandled rejection" which causes its own problems // the program will log something like this _and exit/crash_ // Unhandled Promise rejection detected: @@ -96,13 +95,6 @@ export async function _installPackage({ // add a `.catch` to prevent the "unhandled rejection" case // in that `.catch`, set something that indicates a failure // check for that failure later and act accordingly (throw, ignore, return) - let installIndexPatternError; - const installIndexPatternPromise = installIndexPatterns( - savedObjectsClient, - pkgName, - pkgVersion, - installSource - ).catch((reason) => (installIndexPatternError = reason)); const kibanaAssets = await getKibanaAssets(paths); if (installedPkg) await deleteKibanaSavedObjectsAssets( @@ -184,9 +176,8 @@ export async function _installPackage({ })); // make sure the assets are installed (or didn't error) - if (installIndexPatternError) throw installIndexPatternError; if (installKibanaAssetsError) throw installKibanaAssetsError; - await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); + await installKibanaAssetsPromise; const packageAssetResults = await saveArchiveEntries({ savedObjectsClient, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index b726c60fc1e5e..7323263d4a70f 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -7,58 +7,72 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import { appContextService } from '../../app_context'; import * as Registry from '../registry'; +import { installIndexPatterns } from '../kibana/index_pattern/install'; -import { getInstallationObject } from './index'; -import { upgradePackage } from './install'; +import { installPackage } from './install'; import type { BulkInstallResponse, IBulkInstallPackageError } from './install'; interface BulkInstallPackagesParams { savedObjectsClient: SavedObjectsClientContract; - packagesToUpgrade: string[]; + packagesToInstall: string[]; esClient: ElasticsearchClient; } export async function bulkInstallPackages({ savedObjectsClient, - packagesToUpgrade, + packagesToInstall, esClient, }: BulkInstallPackagesParams): Promise { - const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => - Promise.all([ - getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), - Registry.fetchFindLatestPackage(pkgToUpgrade), - ]) + const logger = appContextService.getLogger(); + const installSource = 'registry'; + const latestPackagesResults = await Promise.allSettled( + packagesToInstall.map((packageName) => Registry.fetchFindLatestPackage(packageName)) ); - const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); - const installResponsePromises = installedAndLatestResults.map(async (result, index) => { - const pkgToUpgrade = packagesToUpgrade[index]; - if (result.status === 'fulfilled') { - const [installedPkg, latestPkg] = result.value; - return upgradePackage({ - savedObjectsClient, - esClient, - installedPkg, - latestPkg, - pkgToUpgrade, - }); - } else { - return { name: pkgToUpgrade, error: result.reason }; - } - }); - const installResults = await Promise.allSettled(installResponsePromises); - const installResponses = installResults.map((result, index) => { - const pkgToUpgrade = packagesToUpgrade[index]; - if (result.status === 'fulfilled') { - return result.value; - } else { - return { name: pkgToUpgrade, error: result.reason }; - } - }); - return installResponses; + logger.debug(`kicking off bulk install of ${packagesToInstall.join(', ')} from registry`); + const installResults = await Promise.allSettled( + latestPackagesResults.map(async (result, index) => { + const packageName = packagesToInstall[index]; + if (result.status === 'fulfilled') { + const latestPackage = result.value; + return { + name: packageName, + version: latestPackage.version, + result: await installPackage({ + savedObjectsClient, + esClient, + pkgkey: Registry.pkgToPkgKey(latestPackage), + installSource, + skipPostInstall: true, + }), + }; + } + return { name: packageName, error: result.reason }; + }) + ); + + // only install index patterns if we completed install for any package-version for the + // first time, aka fresh installs or upgrades + if ( + installResults.find( + (result) => result.status === 'fulfilled' && result.value.result?.status === 'installed' + ) + ) { + await installIndexPatterns({ savedObjectsClient, esClient, installSource }); + } + + return installResults.map((result, index) => { + const packageName = packagesToInstall[index]; + return result.status === 'fulfilled' + ? result.value + : { name: packageName, error: result.reason }; + }); } -export function isBulkInstallError(test: any): test is IBulkInstallPackageError { - return 'error' in test && test.error instanceof Error; +export function isBulkInstallError( + installResponse: any +): installResponse is IBulkInstallPackageError { + return 'error' in installResponse && installResponse.error instanceof Error; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts index e01af7b64c0e3..fa2ea9e2209ed 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -77,9 +77,8 @@ describe('ensureInstalledDefaultPackages', () => { return [ { name: mockInstallation.attributes.name, - assets: [], - newVersion: '', - oldVersion: '', + result: { assets: [], status: 'installed' }, + version: '', statusCode: 200, }, ]; @@ -96,16 +95,14 @@ describe('ensureInstalledDefaultPackages', () => { return [ { name: 'success one', - assets: [], - newVersion: '', - oldVersion: '', + result: { assets: [], status: 'installed' }, + version: '', statusCode: 200, }, { name: 'success two', - assets: [], - newVersion: '', - oldVersion: '', + result: { assets: [], status: 'installed' }, + version: '', statusCode: 200, }, { @@ -114,9 +111,8 @@ describe('ensureInstalledDefaultPackages', () => { }, { name: 'success three', - assets: [], - newVersion: '', - oldVersion: '', + result: { assets: [], status: 'installed' }, + version: '', statusCode: 200, }, { @@ -138,9 +134,8 @@ describe('ensureInstalledDefaultPackages', () => { return [ { name: 'undefined package', - assets: [], - newVersion: '', - oldVersion: '', + result: { assets: [], status: 'installed' }, + version: '', statusCode: 200, }, ]; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 011e77fb7e89b..1a6b41976af98 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -5,10 +5,8 @@ * 2.0. */ -import semverGt from 'semver/functions/gt'; import semverLt from 'semver/functions/lt'; import type Boom from '@hapi/boom'; -import type { UnwrapPromise } from '@kbn/utility-types'; import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'src/core/server'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; @@ -28,12 +26,14 @@ import type { AssetType, EsAssetReference, InstallType, + InstallResult, } from '../../../types'; import { appContextService } from '../../app_context'; import * as Registry from '../registry'; import { setPackageInfo, parseAndVerifyArchiveEntries, unpackBufferToCache } from '../archive'; import { toAssetReference } from '../kibana/assets/install'; import type { ArchiveAsset } from '../kibana/assets/install'; +import { installIndexPatterns } from '../kibana/index_pattern/install'; import { isRequiredPackage, @@ -63,7 +63,7 @@ export async function installLatestPackage(options: { savedObjectsClient, pkgkey, esClient, - }); + }).then(({ assets }) => assets); } catch (err) { throw err; } @@ -76,7 +76,7 @@ export async function ensureInstalledDefaultPackages( const installations = []; const bulkResponse = await bulkInstallPackages({ savedObjectsClient, - packagesToUpgrade: Object.values(defaultPackages), + packagesToInstall: Object.values(defaultPackages), esClient, }); @@ -164,6 +164,7 @@ export async function handleInstallPackageFailure({ savedObjectsClient, pkgkey: prevVersion, esClient, + force: true, }); } } catch (e) { @@ -177,64 +178,6 @@ export interface IBulkInstallPackageError { } export type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; -interface UpgradePackageParams { - savedObjectsClient: SavedObjectsClientContract; - esClient: ElasticsearchClient; - installedPkg: UnwrapPromise>; - latestPkg: UnwrapPromise>; - pkgToUpgrade: string; -} -export async function upgradePackage({ - savedObjectsClient, - esClient, - installedPkg, - latestPkg, - pkgToUpgrade, -}: UpgradePackageParams): Promise { - if (!installedPkg || semverGt(latestPkg.version, installedPkg.attributes.version)) { - const pkgkey = Registry.pkgToPkgKey({ - name: latestPkg.name, - version: latestPkg.version, - }); - - try { - const assets = await installPackage({ - installSource: 'registry', - savedObjectsClient, - pkgkey, - esClient, - }); - return { - name: pkgToUpgrade, - newVersion: latestPkg.version, - oldVersion: installedPkg?.attributes.version ?? null, - assets, - }; - } catch (installFailed) { - await handleInstallPackageFailure({ - savedObjectsClient, - error: installFailed, - pkgName: latestPkg.name, - pkgVersion: latestPkg.version, - installedPkg, - esClient, - }); - return { name: pkgToUpgrade, error: installFailed }; - } - } else { - // package was already at the latest version - return { - name: pkgToUpgrade, - newVersion: latestPkg.version, - oldVersion: latestPkg.version, - assets: [ - ...installedPkg.attributes.installed_es, - ...installedPkg.attributes.installed_kibana, - ], - }; - } -} - interface InstallRegistryPackageParams { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; @@ -247,32 +190,81 @@ async function installPackageFromRegistry({ pkgkey, esClient, force = false, -}: InstallRegistryPackageParams): Promise { +}: InstallRegistryPackageParams): Promise { + const logger = appContextService.getLogger(); // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); + // get the currently installed package const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); const installType = getInstallType({ pkgVersion, installedPkg }); + + // get latest package version + const latestPackage = await Registry.fetchFindLatestPackage(pkgName); + // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update const installOutOfDateVersionOk = - installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback'; + force || ['reinstall', 'reupdate', 'rollback'].includes(installType); + + // if the requested version is the same as installed version, check if we allow it based on + // current installed package status and force flag, if we don't allow it, + // just return the asset references from the existing installation + if ( + installedPkg?.attributes.version === pkgVersion && + installedPkg?.attributes.install_status === 'installed' + ) { + if (!force) { + logger.debug(`${pkgkey} is already installed, skipping installation`); + return { + assets: [ + ...installedPkg.attributes.installed_es, + ...installedPkg.attributes.installed_kibana, + ], + status: 'already_installed', + }; + } + } - const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - if (semverLt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) { - throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); + // if the requested version is out-of-date of the latest package version, check if we allow it + // if we don't allow it, return an error + if (semverLt(pkgVersion, latestPackage.version)) { + if (!installOutOfDateVersionOk) { + throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); + } + logger.debug( + `${pkgkey} is out-of-date, installing anyway due to ${ + force ? 'force flag' : `install type ${installType}` + }` + ); } + // get package info const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); - return _installPackage({ - savedObjectsClient, - esClient, - installedPkg, - paths, - packageInfo, - installType, - installSource: 'registry', - }); + // try installing the package, if there was an error, call error handler and rethrow + try { + return _installPackage({ + savedObjectsClient, + esClient, + installedPkg, + paths, + packageInfo, + installType, + installSource: 'registry', + }).then((assets) => { + return { assets, status: 'installed' }; + }); + } catch (e) { + await handleInstallPackageFailure({ + savedObjectsClient, + error: e, + pkgName, + pkgVersion, + installedPkg, + esClient, + }); + throw e; + } } interface InstallUploadedArchiveParams { @@ -282,16 +274,12 @@ interface InstallUploadedArchiveParams { contentType: string; } -export type InstallPackageParams = - | ({ installSource: Extract } & InstallRegistryPackageParams) - | ({ installSource: Extract } & InstallUploadedArchiveParams); - async function installPackageByUpload({ savedObjectsClient, esClient, archiveBuffer, contentType, -}: InstallUploadedArchiveParams): Promise { +}: InstallUploadedArchiveParams): Promise { const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); const installedPkg = await getInstallationObject({ @@ -329,32 +317,68 @@ async function installPackageByUpload({ packageInfo, installType, installSource, + }).then((assets) => { + return { assets, status: 'installed' }; }); } +export type InstallPackageParams = { + skipPostInstall?: boolean; +} & ( + | ({ installSource: Extract } & InstallRegistryPackageParams) + | ({ installSource: Extract } & InstallUploadedArchiveParams) +); + export async function installPackage(args: InstallPackageParams) { if (!('installSource' in args)) { throw new Error('installSource is required'); } + const logger = appContextService.getLogger(); + const { savedObjectsClient, esClient, skipPostInstall = false, installSource } = args; if (args.installSource === 'registry') { - const { savedObjectsClient, pkgkey, esClient, force } = args; - - return installPackageFromRegistry({ + const { pkgkey, force } = args; + const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); + logger.debug(`kicking off install of ${pkgkey} from registry`); + const response = installPackageFromRegistry({ savedObjectsClient, pkgkey, esClient, force, + }).then(async (installResult) => { + if (skipPostInstall) { + return installResult; + } + logger.debug(`install of ${pkgkey} finished, running post-install`); + return installIndexPatterns({ + savedObjectsClient, + esClient, + pkgName, + pkgVersion, + installSource, + }).then(() => installResult); }); + return response; } else if (args.installSource === 'upload') { - const { savedObjectsClient, esClient, archiveBuffer, contentType } = args; - - return installPackageByUpload({ + const { archiveBuffer, contentType } = args; + logger.debug(`kicking off install of uploaded package`); + const response = installPackageByUpload({ savedObjectsClient, esClient, archiveBuffer, contentType, + }).then(async (installResult) => { + if (skipPostInstall) { + return installResult; + } + logger.debug(`install of uploaded package finished, running post-install`); + return installIndexPatterns({ + savedObjectsClient, + esClient, + installSource, + }).then(() => installResult); }); + return response; } // @ts-expect-error s/b impossibe b/c `never` by this point, but just in case throw new Error(`Unknown installSource: ${args.installSource}`); @@ -451,26 +475,27 @@ export async function ensurePackagesCompletedInstall( searchFields: ['install_status'], search: 'installing', }); - const installingPromises = installingPackages.saved_objects.reduce< - Array> - >((acc, pkg) => { - const startDate = pkg.attributes.install_started_at; - const nowDate = new Date().toISOString(); - const elapsedTime = Date.parse(nowDate) - Date.parse(startDate); - const pkgkey = `${pkg.attributes.name}-${pkg.attributes.install_version}`; - // reinstall package - if (elapsedTime > MAX_TIME_COMPLETE_INSTALL) { - acc.push( - installPackage({ - installSource: 'registry', - savedObjectsClient, - pkgkey, - esClient, - }) - ); - } - return acc; - }, []); + const installingPromises = installingPackages.saved_objects.reduce>>( + (acc, pkg) => { + const startDate = pkg.attributes.install_started_at; + const nowDate = new Date().toISOString(); + const elapsedTime = Date.parse(nowDate) - Date.parse(startDate); + const pkgkey = `${pkg.attributes.name}-${pkg.attributes.install_version}`; + // reinstall package + if (elapsedTime > MAX_TIME_COMPLETE_INSTALL) { + acc.push( + installPackage({ + installSource: 'registry', + savedObjectsClient, + pkgkey, + esClient, + }) + ); + } + return acc; + }, + [] + ); await Promise.all(installingPromises); return installingPackages; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index da407c1d4cfa0..21e4e31be2bd0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -63,7 +63,7 @@ export async function removeInstallation(options: { // recreate or delete index patterns when a package is uninstalled // this must be done after deleting the saved object for the current package otherwise it will retrieve the package // from the registry again and reinstall the index patterns - await installIndexPatterns(savedObjectsClient); + await installIndexPatterns({ savedObjectsClient, esClient }); // remove the package archive and its contents from the cache so that a reinstall fetches // a fresh copy from the registry diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 885809d767323..c25b047c0e1ad 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -72,6 +72,7 @@ export { SettingsSOAttributes, InstallType, InstallSource, + InstallResult, // Agent Request types PostAgentEnrollRequest, PostAgentCheckinRequest, diff --git a/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts index 546493a4c6f39..80d0f67f9e7fb 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts @@ -51,8 +51,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.response.length).equal(1); expect(body.response[0].name).equal('multiple_versions'); const entry = body.response[0] as BulkInstallPackageInfo; - expect(entry.oldVersion).equal('0.1.0'); - expect(entry.newVersion).equal('0.3.0'); + expect(entry.version).equal('0.3.0'); }); it('should return an error for packages that do not exist', async function () { const { body }: { body: BulkInstallPackagesResponse } = await supertest @@ -63,8 +62,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.response.length).equal(2); expect(body.response[0].name).equal('multiple_versions'); const entry = body.response[0] as BulkInstallPackageInfo; - expect(entry.oldVersion).equal('0.1.0'); - expect(entry.newVersion).equal('0.3.0'); + expect(entry.version).equal('0.3.0'); const err = body.response[1] as IBulkInstallPackageHTTPError; expect(err.statusCode).equal(404); @@ -79,12 +77,10 @@ export default function (providerContext: FtrProviderContext) { expect(body.response.length).equal(2); expect(body.response[0].name).equal('multiple_versions'); let entry = body.response[0] as BulkInstallPackageInfo; - expect(entry.oldVersion).equal('0.1.0'); - expect(entry.newVersion).equal('0.3.0'); + expect(entry.version).equal('0.3.0'); entry = body.response[1] as BulkInstallPackageInfo; - expect(entry.oldVersion).equal(null); - expect(entry.newVersion).equal('0.1.0'); + expect(entry.version).equal('0.1.0'); expect(entry.name).equal('overrides'); }); }); @@ -103,8 +99,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.response.length).equal(1); expect(body.response[0].name).equal('multiple_versions'); const entry = body.response[0] as BulkInstallPackageInfo; - expect(entry.oldVersion).equal(null); - expect(entry.newVersion).equal('0.3.0'); + expect(entry.version).equal('0.3.0'); }); }); }); From 685aa20ba6bd86493e2c7a53e2227a1e4aa23859 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 23 Mar 2021 20:45:26 +0200 Subject: [PATCH 02/88] Set initial palette for new series (#95177) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/lib/new_series_fn.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js b/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js index 36e52fc21732f..9064cd1afc3f4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js @@ -17,6 +17,10 @@ export const newSeriesFn = (obj = {}) => { id: uuid.v1(), color: '#68BC00', split_mode: 'everything', + palette: { + type: 'palette', + name: 'default', + }, metrics: [newMetricAggFn()], separate_axis: 0, axis_position: 'right', From 29ee309dd880b273fa8810401f1f3d93699a5b55 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 23 Mar 2021 14:18:11 -0500 Subject: [PATCH 03/88] [App Search] Role mappings migration part 3 (#94763) * Remove validaition of ID property in route body The ID is inferred from the param in the URL. This was fixed in the logic file but the server route was never updated * Add RoleMappings component - ROLE_MAPPINGS_TITLE was moved to a shared constant in an earlier PR - Also removing redundant exports of interface * Add RoleMapping component - Also removing redundant export of interface from AppLogic * Add RoleMappingsRouter ROLE_MAPPINGS_TITLE was moved to a shared constant in an earlier PR # Conflicts: # x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts * Add route and update link in navigation * Remove unused translations * Change casing Co-authored-by: Constance * Change casing Co-authored-by: Constance * Change casing Co-authored-by: Constance * Add ability test * Refactor conditional constants * Refactor role type constants * Remove EuiPageContent * Refactor action mocks Co-authored-by: Constance Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../applications/app_search/app_logic.ts | 2 +- .../components/role_mappings/constants.ts | 172 +++++++++++- .../components/role_mappings/index.ts | 2 +- .../role_mappings/role_mapping.test.tsx | 106 ++++++++ .../components/role_mappings/role_mapping.tsx | 246 ++++++++++++++++++ .../role_mappings/role_mappings.test.tsx | 88 +++++++ .../role_mappings/role_mappings.tsx | 139 ++++++++++ .../role_mappings/role_mappings_logic.ts | 4 +- .../role_mappings_router.test.tsx | 26 ++ .../role_mappings/role_mappings_router.tsx | 29 +++ .../applications/app_search/index.test.tsx | 19 +- .../public/applications/app_search/index.tsx | 14 +- .../public/applications/app_search/types.ts | 2 +- .../app_search/utils/role/types.ts | 5 + .../routes/app_search/role_mappings.test.ts | 7 +- .../server/routes/app_search/role_mappings.ts | 5 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 18 files changed, 841 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 5f7dc683d93b4..44416b596e6ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -13,7 +13,7 @@ import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; -export interface AppValues { +interface AppValues { ilmEnabled: boolean; configuredLimits: ConfiguredLimits; account: Account; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index 6232ba0fb4668..1fed750a86dc4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -7,9 +7,32 @@ import { i18n } from '@kbn/i18n'; -export const ROLE_MAPPINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.roleMappings.title', - { defaultMessage: 'Role Mappings' } +import { AdvanceRoleType } from '../../types'; + +export const SAVE_ROLE_MAPPING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMapping.saveRoleMappingButtonLabel', + { defaultMessage: 'Save role mapping' } +); +export const UPDATE_ROLE_MAPPING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMapping.updateRoleMappingButtonLabel', + { defaultMessage: 'Update role mapping' } +); + +export const ADD_ROLE_MAPPING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMapping.newRoleMappingTitle', + { defaultMessage: 'Add role mapping' } +); +export const MANAGE_ROLE_MAPPING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMapping.manageRoleMappingTitle', + { defaultMessage: 'Manage role mapping' } +); + +export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody', + { + defaultMessage: + 'All users who successfully authenticate will be assigned the Owner role and have access to all engines. Add a new role to override the default.', + } ); export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( @@ -40,3 +63,146 @@ export const ROLE_MAPPING_UPDATED_MESSAGE = i18n.translate( defaultMessage: 'Role mapping successfully updated.', } ); + +export const ROLE_MAPPINGS_ENGINE_ACCESS_HEADING = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading', + { + defaultMessage: 'Engine access', + } +); + +export const ROLE_MAPPINGS_RESET_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingsResetButton', + { + defaultMessage: 'Reset mappings', + } +); + +export const ROLE_MAPPINGS_RESET_CONFIRM_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmTitle', + { + defaultMessage: 'Are you sure you want to reset role mappings?', + } +); + +export const ROLE_MAPPINGS_RESET_CONFIRM_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmButton', + { + defaultMessage: 'Reset role mappings', + } +); + +export const ROLE_MAPPINGS_RESET_CANCEL_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingsResetCancelButton', + { + defaultMessage: 'Cancel', + } +); + +export const DEV_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.DEV_ROLE_TYPE_DESCRIPTION', + { + defaultMessage: 'Devs can manage all aspects of an engine.', + } +); + +export const EDITOR_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.editorRoleTypeDescription', + { + defaultMessage: 'Editors can manage search settings.', + } +); + +export const ANALYST_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.analystRoleTypeDescription', + { + defaultMessage: 'Analysts can only view documents, query tester, and analytics.', + } +); + +export const OWNER_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.ownerRoleTypeDescription', + { + defaultMessage: + 'Owners can do anything. There can be many owners on the account, but there must be at least one owner at any time.', + } +); + +export const ADMIN_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.adminRoleTypeDescription', + { + defaultMessage: 'Admins can do anything, except manage account settings.', + } +); + +export const ADVANCED_ROLE_SELECTORS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.advancedRoleSelectorsTitle', + { + defaultMessage: 'Full or limited engine access', + } +); + +export const ROLE_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.roleTitle', { + defaultMessage: 'Role', +}); + +export const FULL_ENGINE_ACCESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.fullEngineAccessTitle', + { + defaultMessage: 'Full engine access', + } +); + +export const FULL_ENGINE_ACCESS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.fullEngineAccessDescription', + { + defaultMessage: 'Access to all current and future engines.', + } +); + +export const LIMITED_ENGINE_ACCESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.limitedEngineAccessTitle', + { + defaultMessage: 'Limited engine access', + } +); + +export const LIMITED_ENGINE_ACCESS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.limitedEngineAccessDescription', + { + defaultMessage: 'Limit user access to specific engines:', + } +); + +export const ENGINE_ACCESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engineAccessTitle', + { + defaultMessage: 'Engine access', + } +); + +export const ADVANCED_ROLE_TYPES = [ + { + type: 'dev', + description: DEV_ROLE_TYPE_DESCRIPTION, + }, + { + type: 'editor', + description: EDITOR_ROLE_TYPE_DESCRIPTION, + }, + { + type: 'analyst', + description: ANALYST_ROLE_TYPE_DESCRIPTION, + }, +] as AdvanceRoleType[]; + +export const STANDARD_ROLE_TYPES = [ + { + type: 'owner', + description: OWNER_ROLE_TYPE_DESCRIPTION, + }, + { + type: 'admin', + description: ADMIN_ROLE_TYPE_DESCRIPTION, + }, +] as AdvanceRoleType[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts index 91159ea9646ea..ce4b1de6e399d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ROLE_MAPPINGS_TITLE } from './constants'; +export { RoleMappingsRouter } from './role_mappings_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx new file mode 100644 index 0000000000000..f50fc21d5ba58 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx @@ -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 '../../../__mocks__/shallow_useeffect.mock'; +import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__'; +import { setMockActions, setMockValues } from '../../../__mocks__'; +import { engines } from '../../__mocks__/engines.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCheckbox } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { + AttributeSelector, + DeleteMappingCallout, + RoleSelector, +} from '../../../shared/role_mapping'; +import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { RoleMapping } from './role_mapping'; + +describe('RoleMapping', () => { + const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role; + const actions = { + initializeRoleMappings: jest.fn(), + initializeRoleMapping: jest.fn(), + handleSaveMapping: jest.fn(), + handleEngineSelectionChange: jest.fn(), + handleAccessAllEnginesChange: jest.fn(), + handleAttributeValueChange: jest.fn(), + handleAttributeSelectorChange: jest.fn(), + handleDeleteMapping: jest.fn(), + handleRoleChange: jest.fn(), + handleAuthProviderChange: jest.fn(), + resetState: jest.fn(), + }; + + const mockValues = { + attributes: [], + elasticsearchRoles: [], + hasAdvancedRoles: true, + dataLoading: false, + roleType: 'admin', + roleMappings: [asRoleMapping], + attributeValue: '', + attributeName: 'username', + availableEngines: engines, + selectedEngines: new Set(), + accessAllEngines: false, + availableAuthProviders: [], + multipleAuthProvidersConfig: true, + selectedAuthProviders: [], + myRole: { + availableRoleTypes: mockRole.ability.availableRoleTypes, + }, + }; + + beforeEach(() => { + setMockActions(actions); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(AttributeSelector)).toHaveLength(1); + expect(wrapper.find(RoleSelector)).toHaveLength(5); + }); + + it('returns Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders DeleteMappingCallout for existing mapping', () => { + setMockValues({ ...mockValues, roleMapping: asRoleMapping }); + const wrapper = shallow(); + + expect(wrapper.find(DeleteMappingCallout)).toHaveLength(1); + }); + + it('hides DeleteMappingCallout for new mapping', () => { + const wrapper = shallow(); + + expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0); + }); + + it('handles engine checkbox click', () => { + const wrapper = shallow(); + wrapper + .find(EuiCheckbox) + .first() + .simulate('change', { target: { checked: true } }); + + expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith(engines[0].name, true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx new file mode 100644 index 0000000000000..bfa3fefb2732d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useParams } from 'react-router-dom'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPageContentBody, + EuiPageHeader, + EuiPanel, + EuiRadio, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; +import { + AttributeSelector, + DeleteMappingCallout, + RoleSelector, +} from '../../../shared/role_mapping'; +import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; +import { AppLogic } from '../../app_logic'; + +import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; +import { Engine } from '../engine/types'; + +import { + SAVE_ROLE_MAPPING, + UPDATE_ROLE_MAPPING, + ADD_ROLE_MAPPING_TITLE, + MANAGE_ROLE_MAPPING_TITLE, + ADVANCED_ROLE_TYPES, + STANDARD_ROLE_TYPES, + ADVANCED_ROLE_SELECTORS_TITLE, + ROLE_TITLE, + FULL_ENGINE_ACCESS_TITLE, + FULL_ENGINE_ACCESS_DESCRIPTION, + LIMITED_ENGINE_ACCESS_TITLE, + LIMITED_ENGINE_ACCESS_DESCRIPTION, + ENGINE_ACCESS_TITLE, +} from './constants'; +import { RoleMappingsLogic } from './role_mappings_logic'; + +interface RoleMappingProps { + isNew?: boolean; +} + +export const RoleMapping: React.FC = ({ isNew }) => { + const { roleId } = useParams() as { roleId: string }; + const { myRole } = useValues(AppLogic); + + const { + handleAccessAllEnginesChange, + handleAttributeSelectorChange, + handleAttributeValueChange, + handleAuthProviderChange, + handleDeleteMapping, + handleEngineSelectionChange, + handleRoleChange, + handleSaveMapping, + initializeRoleMapping, + resetState, + } = useActions(RoleMappingsLogic); + + const { + accessAllEngines, + attributeName, + attributeValue, + attributes, + availableAuthProviders, + availableEngines, + dataLoading, + elasticsearchRoles, + hasAdvancedRoles, + multipleAuthProvidersConfig, + roleMapping, + roleType, + selectedEngines, + selectedAuthProviders, + } = useValues(RoleMappingsLogic); + + useEffect(() => { + initializeRoleMapping(roleId); + return resetState; + }, []); + + if (dataLoading) return ; + + const SAVE_ROLE_MAPPING_LABEL = isNew ? SAVE_ROLE_MAPPING : UPDATE_ROLE_MAPPING; + const TITLE = isNew ? ADD_ROLE_MAPPING_TITLE : MANAGE_ROLE_MAPPING_TITLE; + + const saveRoleMappingButton = ( + + {SAVE_ROLE_MAPPING_LABEL} + + ); + + const engineSelector = (engine: Engine) => ( + { + handleEngineSelectionChange(engine.name, e.target.checked); + }} + label={engine.name} + /> + ); + + const advancedRoleSelectors = ( + <> + + +

{ADVANCED_ROLE_SELECTORS_TITLE}

+
+ + {ADVANCED_ROLE_TYPES.map(({ type, description }) => ( + + ))} + + ); + + return ( + <> + + + + + + + + + + + +

{ROLE_TITLE}

+
+ + +

{FULL_ENGINE_ACCESS_TITLE}

+
+ + export{' '} + {STANDARD_ROLE_TYPES.map(({ type, description }) => ( + + ))} + {hasAdvancedRoles && advancedRoleSelectors} +
+
+ {hasAdvancedRoles && ( + + + +

{ENGINE_ACCESS_TITLE}

+
+ + + + +

{FULL_ENGINE_ACCESS_TITLE}

+
+

{FULL_ENGINE_ACCESS_DESCRIPTION}

+ + } + /> +
+ + <> + + +

{LIMITED_ENGINE_ACCESS_TITLE}

+
+

{LIMITED_ENGINE_ACCESS_DESCRIPTION}

+ + } + /> + {!accessAllEngines && ( +
+ {availableEngines.map((engine) => engineSelector(engine))} +
+ )} + +
+
+
+ )} +
+ + {roleMapping && } +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx new file mode 100644 index 0000000000000..9275ba0cd16db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__'; + +import React, { MouseEvent } from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiEmptyPrompt, EuiConfirmModal, EuiPageHeader } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { RoleMappingsTable } from '../../../shared/role_mapping'; +import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { RoleMappings } from './role_mappings'; + +describe('RoleMappings', () => { + const initializeRoleMappings = jest.fn(); + const handleResetMappings = jest.fn(); + const mockValues = { + roleMappings: [wsRoleMapping], + dataLoading: false, + multipleAuthProvidersConfig: false, + }; + + beforeEach(() => { + setMockActions({ + initializeRoleMappings, + handleResetMappings, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(RoleMappingsTable)).toHaveLength(1); + }); + + it('returns Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ ...mockValues, roleMappings: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + describe('resetMappingsWarningModal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = shallow(); + const button = wrapper.find(EuiPageHeader).prop('rightSideItems')![0] as any; + button.props.onClick(); + }); + + it('renders reset warnings modal', () => { + expect(wrapper.find(EuiConfirmModal)).toHaveLength(1); + }); + + it('hides reset warnings modal', () => { + const modal = wrapper.find(EuiConfirmModal); + modal.prop('onCancel')(); + + expect(wrapper.find(EuiConfirmModal)).toHaveLength(0); + }); + + it('resets when confirmed', () => { + const event = {} as MouseEvent; + const modal = wrapper.find(EuiConfirmModal); + modal.prop('onConfirm')!(event); + + expect(handleResetMappings).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx new file mode 100644 index 0000000000000..e31f5c04bdb45 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiConfirmModal, + EuiEmptyPrompt, + EuiOverlayMask, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; +import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping'; +import { + EMPTY_ROLE_MAPPINGS_TITLE, + ROLE_MAPPINGS_TITLE, + ROLE_MAPPINGS_DESCRIPTION, +} from '../../../shared/role_mapping/constants'; + +import { ROLE_MAPPING_NEW_PATH } from '../../routes'; + +import { + ROLE_MAPPINGS_ENGINE_ACCESS_HEADING, + EMPTY_ROLE_MAPPINGS_BODY, + ROLE_MAPPINGS_RESET_BUTTON, + ROLE_MAPPINGS_RESET_CONFIRM_TITLE, + ROLE_MAPPINGS_RESET_CONFIRM_BUTTON, + ROLE_MAPPINGS_RESET_CANCEL_BUTTON, +} from './constants'; +import { RoleMappingsLogic } from './role_mappings_logic'; +import { generateRoleMappingPath } from './utils'; + +export const RoleMappings: React.FC = () => { + const { initializeRoleMappings, handleResetMappings, resetState } = useActions(RoleMappingsLogic); + const { roleMappings, multipleAuthProvidersConfig, dataLoading } = useValues(RoleMappingsLogic); + + useEffect(() => { + initializeRoleMappings(); + return resetState; + }, []); + + const [isResetWarningVisible, setResetWarningVisibility] = useState(false); + const showWarning = () => setResetWarningVisibility(true); + const hideWarning = () => setResetWarningVisibility(false); + + if (dataLoading) return ; + + const RESET_MAPPINGS_WARNING_MODAL_BODY = ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.resetMappingsWarningModalBodyBold', { + defaultMessage: 'All role mappings will be deleted', + })} + + ), + }} + /> + ); + + const addMappingButton = ; + + const roleMappingEmptyState = ( + {EMPTY_ROLE_MAPPINGS_TITLE}} + body={

{EMPTY_ROLE_MAPPINGS_BODY}

} + actions={addMappingButton} + /> + ); + + const roleMappingsTable = ( + + ); + + const resetMappings = ( + + {ROLE_MAPPINGS_RESET_BUTTON} + + ); + + const resetMappingsWarningModal = isResetWarningVisible ? ( + + handleResetMappings(hideWarning)} + title={ROLE_MAPPINGS_RESET_CONFIRM_TITLE} + cancelButtonText={ROLE_MAPPINGS_RESET_CANCEL_BUTTON} + confirmButtonText={ROLE_MAPPINGS_RESET_CONFIRM_BUTTON} + buttonColor="danger" + maxWidth={640} + > +

{RESET_MAPPINGS_WARNING_MODAL_BODY}

+
+
+ ) : null; + + return ( + <> + + + + + + {roleMappings.length === 0 ? roleMappingEmptyState : roleMappingsTable} + + + {resetMappingsWarningModal} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index f1b81a59779ac..d6d5677386330 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -48,7 +48,7 @@ const getFirstAttributeName = (roleMapping: ASRoleMapping) => const getFirstAttributeValue = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][1] as AttributeName; -export interface RoleMappingsActions { +interface RoleMappingsActions { handleAccessAllEnginesChange(): void; handleAuthProviderChange(value: string[]): { value: string[] }; handleAttributeSelectorChange( @@ -74,7 +74,7 @@ export interface RoleMappingsActions { setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; } -export interface RoleMappingsValues { +interface RoleMappingsValues { accessAllEngines: boolean; attributeName: AttributeName; attributeValue: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx new file mode 100644 index 0000000000000..e9fc40ba1dbb4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + +import { RoleMapping } from './role_mapping'; +import { RoleMappings } from './role_mappings'; +import { RoleMappingsRouter } from './role_mappings_router'; + +describe('RoleMappingsRouter', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(3); + expect(wrapper.find(RoleMapping)).toHaveLength(2); + expect(wrapper.find(RoleMappings)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx new file mode 100644 index 0000000000000..7aa8b4067d9e5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Route, Switch } from 'react-router-dom'; + +import { ROLE_MAPPING_NEW_PATH, ROLE_MAPPING_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; + +import { RoleMapping } from './role_mapping'; +import { RoleMappings } from './role_mappings'; + +export const RoleMappingsRouter: React.FC = () => ( + + + + + + + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 4da71ec9a135b..62a0ccc01f29a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -25,6 +25,7 @@ import { EngineCreation } from './components/engine_creation'; import { EnginesOverview } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; import { MetaEngineCreation } from './components/meta_engine_creation'; +import { RoleMappingsRouter } from './components/role_mappings'; import { SetupGuide } from './components/setup_guide'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; @@ -88,6 +89,20 @@ describe('AppSearchConfigured', () => { }); describe('ability checks', () => { + describe('canViewRoleMappings', () => { + it('renders RoleMappings when canViewRoleMappings is true', () => { + setMockValues({ myRole: { canViewRoleMappings: true } }); + rerender(wrapper); + expect(wrapper.find(RoleMappingsRouter)).toHaveLength(1); + }); + + it('does not render RoleMappings when user canViewRoleMappings is false', () => { + setMockValues({ myRole: { canManageEngines: false } }); + rerender(wrapper); + expect(wrapper.find(RoleMappingsRouter)).toHaveLength(0); + }); + }); + describe('canManageEngines', () => { it('renders EngineCreation when user canManageEngines is true', () => { setMockValues({ myRole: { canManageEngines: true } }); @@ -155,8 +170,6 @@ describe('AppSearchNav', () => { setMockValues({ myRole: { canViewRoleMappings: true } }); const wrapper = shallow(); - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( - 'http://localhost:3002/as/role_mappings' - ); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/role_mappings'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 88db4004ea9e2..3a46a90d20d66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -12,12 +12,13 @@ import { useValues } from 'kea'; import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; -import { getAppSearchUrl } from '../shared/enterprise_search_url'; import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { NotFound } from '../shared/not_found'; +import { ROLE_MAPPINGS_TITLE } from '../shared/role_mapping/constants'; + import { AppLogic } from './app_logic'; import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; import { EngineNav, EngineRouter } from './components/engine'; @@ -26,7 +27,7 @@ import { EnginesOverview, ENGINES_TITLE } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; -import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; +import { RoleMappingsRouter } from './components/role_mappings'; import { Settings, SETTINGS_TITLE } from './components/settings'; import { SetupGuide } from './components/setup_guide'; import { @@ -64,7 +65,7 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC> = (props) => { const { - myRole: { canManageEngines, canManageMetaEngines }, + myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings }, } = useValues(AppLogic(props)); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); @@ -101,6 +102,11 @@ export const AppSearchConfigured: React.FC> = (props) = + {canViewRoleMappings && ( + + + + )} {canManageEngines && ( @@ -141,7 +147,7 @@ export const AppSearchNav: React.FC = ({ subNav }) => { {CREDENTIALS_TITLE} )} {canViewRoleMappings && ( - + {ROLE_MAPPINGS_TITLE} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index e763264a041de..ca3e67129846b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -6,5 +6,5 @@ */ export * from '../../../common/types/app_search'; -export { Role, RoleTypes, AbilityTypes, ASRoleMapping } from './utils/role'; +export { Role, RoleTypes, AbilityTypes, ASRoleMapping, AdvanceRoleType } from './utils/role'; export { Engine } from './components/engine/types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts index 0fa94b493ed31..0c3abd6909390 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts @@ -53,3 +53,8 @@ export interface ASRoleMapping extends RoleMapping { content: string; }; } + +export interface AdvanceRoleType { + type: RoleTypes; + description: string; +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts index 53368035af225..856004add0f73 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts @@ -128,12 +128,7 @@ describe('role mappings routes', () => { describe('validates', () => { it('correctly', () => { - const request = { - body: { - ...roleMappingBaseSchema, - id: '123', - }, - }; + const request = { body: roleMappingBaseSchema }; mockRouter.shouldValidate(request); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts index 4b77c8614a52c..3bd3b3d904280 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts @@ -66,10 +66,7 @@ export function registerRoleMappingRoute({ { path: '/api/app_search/role_mappings/{id}', validate: { - body: schema.object({ - ...roleMappingBaseSchema, - id: schema.string(), - }), + body: schema.object(roleMappingBaseSchema), params: schema.object({ id: schema.string(), }), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a72585185faac..346dbe55e0f22 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7609,7 +7609,6 @@ "xpack.enterpriseSearch.appSearch.result.documentDetailLink": "ドキュメントの詳細を表示", "xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "追加フィールドを非表示", "xpack.enterpriseSearch.appSearch.result.title": "ドキュメント{id}", - "xpack.enterpriseSearch.appSearch.roleMappings.title": "ロールマッピング", "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label": "分析ログ", "xpack.enterpriseSearch.appSearch.settings.logRetention.api.label": "API ログ", "xpack.enterpriseSearch.appSearch.settings.logRetention.description": "API ログと分析のデフォルト書き込み設定を管理します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3d864494a7d53..cfd00024cd76a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7676,7 +7676,6 @@ "xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "隐藏其他字段", "xpack.enterpriseSearch.appSearch.result.showAdditionalFields": "显示其他 {numberOfAdditionalFields, number} 个{numberOfAdditionalFields, plural, other {字段}}", "xpack.enterpriseSearch.appSearch.result.title": "文档 {id}", - "xpack.enterpriseSearch.appSearch.roleMappings.title": "角色映射", "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label": "分析日志", "xpack.enterpriseSearch.appSearch.settings.logRetention.api.label": "API 日志", "xpack.enterpriseSearch.appSearch.settings.logRetention.description": "管理 API 日志和分析的默认写入设置。", From 55e513364a7a096be106c4b700d60c14fb92244d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 23 Mar 2021 20:23:23 +0100 Subject: [PATCH 04/88] isClusterOptedIn should fallback to true when not found (#94980) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/encryption/encrypt.test.ts | 11 ++ .../server/plugin.test.ts | 140 ++++++++++++++---- .../server/plugin.ts | 3 +- .../server/util.ts | 11 -- .../apis/telemetry/telemetry_local.ts | 54 +++---- .../get_stats_with_xpack.ts | 7 +- .../is_cluster_opted_in.test.ts | 15 +- .../is_cluster_opted_in.ts | 14 ++ 8 files changed, 174 insertions(+), 81 deletions(-) delete mode 100644 src/plugins/telemetry_collection_manager/server/util.ts rename src/plugins/telemetry_collection_manager/server/util.test.ts => x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts (69%) create mode 100644 x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts index c1a1a32e3c7f0..be990f4b89e04 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts @@ -22,6 +22,11 @@ describe('getKID', () => { const kid = getKID(useProdKey); expect(kid).toBe('kibana1'); }); + + it(`should fallback to development`, async () => { + const kid = getKID(); + expect(kid).toBe('kibana_dev1'); + }); }); describe('encryptTelemetry', () => { @@ -46,4 +51,10 @@ describe('encryptTelemetry', () => { await encryptTelemetry(payload, { useProdKey: false }); expect(mockEncrypt).toBeCalledWith('kibana_dev1', payload); }); + + it('should fallback to { useProdKey: false }', async () => { + const payload = { some: 'value' }; + await encryptTelemetry(payload); + expect(mockEncrypt).toBeCalledWith('kibana_dev1', payload); + }); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index ac3904ca58b0f..d05799f82c354 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -9,7 +9,7 @@ import { coreMock, httpServerMock } from '../../../core/server/mocks'; import { usageCollectionPluginMock } from '../../usage_collection/server/mocks'; import { TelemetryCollectionManagerPlugin } from './plugin'; -import { CollectionStrategyConfig, StatsGetterConfig } from './types'; +import type { BasicStatsPayload, CollectionStrategyConfig, StatsGetterConfig } from './types'; import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; function createCollectionStrategy(priority: number): jest.Mocked { @@ -87,6 +87,15 @@ describe('Telemetry Collection Manager', () => { }); describe(`after start`, () => { + const basicStats: BasicStatsPayload = { + cluster_uuid: 'clusterUuid', + cluster_name: 'clusterName', + timestamp: new Date().toISOString(), + cluster_stats: {}, + stack_stats: {}, + version: 'version', + }; + beforeAll(() => { telemetryCollectionManager.start(coreMock.createStart()); }); @@ -97,19 +106,59 @@ describe('Telemetry Collection Manager', () => { describe('unencrypted: false', () => { const config: StatsGetterConfig = { unencrypted: false }; - test('getStats returns empty because clusterDetails returns empty, and the soClient is an instance of the TelemetrySavedObjectsClient', async () => { - collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); - await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); - expect(collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient).toBeInstanceOf( - TelemetrySavedObjectsClient - ); + describe('getStats', () => { + test('returns empty because clusterDetails returns empty, and the soClient is an instance of the TelemetrySavedObjectsClient', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).toBeInstanceOf(TelemetrySavedObjectsClient); + }); + + test('returns encrypted payload', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + collectionStrategy.statsGetter.mockResolvedValue([basicStats]); + await expect(setupApi.getStats(config)).resolves.toStrictEqual([expect.any(String)]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).toBeInstanceOf(TelemetrySavedObjectsClient); + }); }); - test('getOptInStats returns empty', async () => { - collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); - await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); - expect(collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient).toBeInstanceOf( - TelemetrySavedObjectsClient - ); + + describe('getOptInStats', () => { + test('returns empty', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).toBeInstanceOf(TelemetrySavedObjectsClient); + }); + + test('returns encrypted results for opt-in true', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([ + expect.any(String), + ]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).toBeInstanceOf(TelemetrySavedObjectsClient); + }); + + test('returns encrypted results for opt-in false', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + await expect(setupApi.getOptInStats(false, config)).resolves.toStrictEqual([ + expect.any(String), + ]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).toBeInstanceOf(TelemetrySavedObjectsClient); + }); }); }); describe('unencrypted: true', () => { @@ -118,19 +167,60 @@ describe('Telemetry Collection Manager', () => { request: httpServerMock.createKibanaRequest(), }; - test('getStats returns empty because clusterDetails returns empty, and the soClient is not an instance of the TelemetrySavedObjectsClient', async () => { - collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); - await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); - expect( - collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + describe('getStats', () => { + test('getStats returns empty because clusterDetails returns empty, and the soClient is not an instance of the TelemetrySavedObjectsClient', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); + test('returns encrypted payload (assumes opted-in when no explicitly opted-out)', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + collectionStrategy.statsGetter.mockResolvedValue([basicStats]); + await expect(setupApi.getStats(config)).resolves.toStrictEqual([ + { ...basicStats, collectionSource: 'test_collection' }, + ]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); }); - test('getOptInStats returns empty', async () => { - collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); - await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); - expect( - collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + + describe('getOptInStats', () => { + test('returns empty', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); + + test('returns results for opt-in true', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([ + { cluster_uuid: 'clusterUuid', opt_in_status: true }, + ]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); + + test('returns results for opt-in false', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([ + { clusterUuid: 'clusterUuid' }, + ]); + await expect(setupApi.getOptInStats(false, config)).resolves.toStrictEqual([ + { cluster_uuid: 'clusterUuid', opt_in_status: false }, + ]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); }); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index 0fec225d5c424..692d91b963d9d 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -30,7 +30,6 @@ import { UsageStatsPayload, StatsCollectionContext, } from './types'; -import { isClusterOptedIn } from './util'; import { encryptTelemetry } from './encryption'; import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; @@ -233,7 +232,7 @@ export class TelemetryCollectionManagerPlugin return usageData; } - return encryptTelemetry(usageData.filter(isClusterOptedIn), { + return await encryptTelemetry(usageData, { useProdKey: this.isDistributable, }); } diff --git a/src/plugins/telemetry_collection_manager/server/util.ts b/src/plugins/telemetry_collection_manager/server/util.ts deleted file mode 100644 index 226d788e09e48..0000000000000 --- a/src/plugins/telemetry_collection_manager/server/util.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const isClusterOptedIn = (clusterUsage: any): boolean => { - return clusterUsage?.stack_stats?.kibana?.plugins?.telemetry?.opt_in_status === true; -}; diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index 211f2eb85e4e3..a7b4da566b143 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -7,13 +7,27 @@ */ import expect from '@kbn/expect'; +import supertestAsPromised from 'supertest-as-promised'; import { basicUiCounters } from './__fixtures__/ui_counters'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { SavedObject } from '../../../../src/core/server'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { SavedObject } from '../../../../src/core/server'; import ossRootTelemetrySchema from '../../../../src/plugins/telemetry/schema/oss_root.json'; import ossPluginsTelemetrySchema from '../../../../src/plugins/telemetry/schema/oss_plugins.json'; import { assertTelemetryPayload, flatKeys } from './utils'; +async function retrieveTelemetry( + supertest: supertestAsPromised.SuperTest +) { + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + return body[0]; +} + export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); @@ -35,14 +49,7 @@ export default function ({ getService }: FtrProviderContext) { let stats: Record; before('pull local stats', async () => { - const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ unencrypted: true }) - .expect(200); - - expect(body.length).to.be(1); - stats = body[0]; + stats = await retrieveTelemetry(supertest); }); it('should pass the schema validation', () => { @@ -141,14 +148,7 @@ export default function ({ getService }: FtrProviderContext) { before('Add UI Counters saved objects', () => esArchiver.load('saved_objects/ui_counters')); after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/ui_counters')); it('returns ui counters aggregated by day', async () => { - const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ unencrypted: true }) - .expect(200); - - expect(body.length).to.be(1); - const stats = body[0]; + const stats = await retrieveTelemetry(supertest); expect(stats.stack_stats.kibana.plugins.ui_counters).to.eql(basicUiCounters); }); }); @@ -191,14 +191,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return application_usage data', async () => { - const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ unencrypted: true }) - .expect(200); - - expect(body.length).to.be(1); - const stats = body[0]; + const stats = await retrieveTelemetry(supertest); expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ 'test-app': { appId: 'test-app', @@ -262,14 +255,7 @@ export default function ({ getService }: FtrProviderContext) { // 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 () => { - const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ unencrypted: true }) - .expect(200); - - expect(body.length).to.be(1); - const stats = body[0]; + const stats = await retrieveTelemetry(supertest); expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ 'test-app': { appId: 'test-app', diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts index 32e59e01b123d..30bcd19007c0d 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts @@ -9,6 +9,7 @@ import { StatsGetter } from 'src/plugins/telemetry_collection_manager/server'; import { TelemetryLocalStats, getLocalStats } from '../../../../../src/plugins/telemetry/server'; import { getXPackUsage } from './get_xpack'; import { ESLicense, getLicenseFromLocalOrMaster } from './get_license'; +import { isClusterOptedIn } from './is_cluster_opted_in'; export type TelemetryAggregatedStats = TelemetryLocalStats & { stack_stats: { xpack?: object }; @@ -48,6 +49,10 @@ export const getStatsWithXpack: StatsGetter = async fu if (monitoringTelemetry) { delete stats.stack_stats.kibana!.plugins.monitoringTelemetry; } - return [...acc, stats, ...(monitoringTelemetry || [])]; + + // From the monitoring-sourced telemetry, we need to filter out the clusters that are opted-out. + const onlyOptedInMonitoringClusters = (monitoringTelemetry || []).filter(isClusterOptedIn); + + return [...acc, stats, ...onlyOptedInMonitoringClusters]; }, [] as TelemetryAggregatedStats[]); }; diff --git a/src/plugins/telemetry_collection_manager/server/util.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts similarity index 69% rename from src/plugins/telemetry_collection_manager/server/util.test.ts rename to x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts index 12e67c466d4d5..5fa7584879f07 100644 --- a/src/plugins/telemetry_collection_manager/server/util.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts @@ -1,12 +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. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ -import { isClusterOptedIn } from './util'; +import { isClusterOptedIn } from './is_cluster_opted_in'; const createMockClusterUsage = (plugins: any) => { return { @@ -32,9 +31,9 @@ describe('isClusterOptedIn', () => { const result = isClusterOptedIn(mockClusterUsage); expect(result).toBe(false); }); - it('returns false if cluster stats is malformed', () => { - expect(isClusterOptedIn(createMockClusterUsage({}))).toBe(false); - expect(isClusterOptedIn({})).toBe(false); - expect(isClusterOptedIn(undefined)).toBe(false); + it('returns true if kibana.plugins.telemetry does not exist', () => { + expect(isClusterOptedIn(createMockClusterUsage({}))).toBe(true); + expect(isClusterOptedIn({})).toBe(true); + expect(isClusterOptedIn(undefined)).toBe(true); }); }); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts new file mode 100644 index 0000000000000..4bc35a238152b --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isClusterOptedIn = (clusterUsage: any): boolean => { + return ( + clusterUsage?.stack_stats?.kibana?.plugins?.telemetry?.opt_in_status === true || + // If stack_stats.kibana.plugins.telemetry does not exist, assume opted-in for BWC + !clusterUsage?.stack_stats?.kibana?.plugins?.telemetry + ); +}; From 3998a83871202d5559683abdf9e4c36ea6e87ea7 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 23 Mar 2021 15:23:58 -0400 Subject: [PATCH 05/88] [Security Solution][Endpoint][Admin] Refactor policy details protections (#94970) --- .../public/management/pages/policy/types.ts | 6 + .../components/protection_radio.tsx | 91 +++++ .../components/protection_switch.tsx | 100 ++++++ .../policy_forms/components/radio_buttons.tsx | 96 +++++ .../supported_version.tsx | 2 +- .../components/user_notification.tsx | 170 +++++++++ .../view/policy_forms/protections/malware.tsx | 327 +----------------- .../policy_forms/protections/ransomware.tsx | 308 +---------------- .../translations/translations/ja-JP.json | 10 - .../translations/translations/zh-CN.json | 10 - 10 files changed, 488 insertions(+), 632 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/{protections => components}/supported_version.tsx (91%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index f7e054729c7b9..b2b95e2765bd8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -120,6 +120,12 @@ export type RansomwareProtectionOSes = KeysByValueCriteria< { ransomware: ProtectionFields } >; +export type PolicyProtection = + | keyof Pick + | keyof Pick; + +export type MacPolicyProtection = keyof Pick; + export interface GetPolicyListResponse extends GetPackagePoliciesResponse { items: PolicyData[]; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx new file mode 100644 index 0000000000000..8394b557207af --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { cloneDeep } from 'lodash'; +import { htmlIdGenerator, EuiRadio } from '@elastic/eui'; +import { + ImmutableArray, + ProtectionModes, + UIPolicyConfig, +} from '../../../../../../../common/endpoint/types'; +import { MacPolicyProtection, PolicyProtection } from '../../../types'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { AppAction } from '../../../../../../common/store/actions'; +import { useLicense } from '../../../../../../common/hooks/use_license'; + +export const ProtectionRadio = React.memo( + ({ + protection, + protectionMode, + osList, + label, + }: { + protection: PolicyProtection; + protectionMode: ProtectionModes; + osList: ImmutableArray>; + label: string; + }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch<(action: AppAction) => void>(); + const radioButtonId = useMemo(() => htmlIdGenerator()(), []); + const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; + const isPlatinumPlus = useLicense().isPlatinumPlus(); + + const handleRadioChange = useCallback(() => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of osList) { + if (os === 'windows') { + newPayload[os][protection].mode = protectionMode; + } else if (os === 'mac') { + newPayload[os][protection as MacPolicyProtection].mode = protectionMode; + } + if (isPlatinumPlus) { + if (os === 'windows') { + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection].enabled = true; + } else { + newPayload[os].popup[protection].enabled = false; + } + } else if (os === 'mac') { + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection as MacPolicyProtection].enabled = true; + } else { + newPayload[os].popup[protection as MacPolicyProtection].enabled = false; + } + } + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, [dispatch, protectionMode, policyDetailsConfig, isPlatinumPlus, osList, protection]); + + /** + * Passing an arbitrary id because EuiRadio + * requires an id if label is passed + */ + + return ( + + ); + } +); + +ProtectionRadio.displayName = 'ProtectionRadio'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx new file mode 100644 index 0000000000000..cbe118e8dfa36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { EuiSwitch } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { useLicense } from '../../../../../../common/hooks/use_license'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { AppAction } from '../../../../../../common/store/actions'; +import { + ImmutableArray, + ProtectionModes, + UIPolicyConfig, +} from '../../../../../../../common/endpoint/types'; +import { PolicyProtection, MacPolicyProtection } from '../../../types'; + +export const ProtectionSwitch = React.memo( + ({ + protection, + osList, + }: { + protection: PolicyProtection; + osList: ImmutableArray>; + }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const dispatch = useDispatch<(action: AppAction) => void>(); + const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; + + const handleSwitchChange = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + if (event.target.checked === false) { + for (const os of osList) { + if (os === 'windows') { + newPayload[os][protection].mode = ProtectionModes.off; + } else if (os === 'mac') { + newPayload[os][protection as MacPolicyProtection].mode = ProtectionModes.off; + } + if (isPlatinumPlus) { + if (os === 'windows') { + newPayload[os].popup[protection].enabled = event.target.checked; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].enabled = + event.target.checked; + } + } + } + } else { + for (const os of osList) { + if (os === 'windows') { + newPayload[os][protection].mode = ProtectionModes.prevent; + } else if (os === 'mac') { + newPayload[os][protection as MacPolicyProtection].mode = ProtectionModes.prevent; + } + if (isPlatinumPlus) { + if (os === 'windows') { + newPayload[os].popup[protection].enabled = event.target.checked; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].enabled = + event.target.checked; + } + } + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig, isPlatinumPlus, protection, osList] + ); + + return ( + + ); + } +); + +ProtectionSwitch.displayName = 'ProtectionSwitch'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx new file mode 100644 index 0000000000000..793c24a0c4d0c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import styled from 'styled-components'; +import { EuiSpacer, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { + Immutable, + ImmutableArray, + ProtectionModes, + UIPolicyConfig, +} from '../../../../../../../common/endpoint/types'; +import { PolicyProtection } from '../../../types'; +import { ConfigFormHeading } from '../../components/config_form'; +import { ProtectionRadio } from './protection_radio'; + +export const RadioFlexGroup = styled(EuiFlexGroup)` + .no-right-margin-radio { + margin-right: 0; + } + .no-horizontal-margin-radio { + margin: ${(props) => props.theme.eui.ruleMargins.marginSmall} 0; + } +`; + +export const RadioButtons = React.memo( + ({ + protection, + osList, + }: { + protection: PolicyProtection; + osList: ImmutableArray>; + }) => { + const radios: Immutable< + Array<{ + id: ProtectionModes; + label: string; + }> + > = useMemo(() => { + return [ + { + id: ProtectionModes.detect, + label: i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', { + defaultMessage: 'Detect', + }), + }, + { + id: ProtectionModes.prevent, + label: i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', { + defaultMessage: 'Prevent', + }), + }, + ]; + }, []); + + return ( + <> + + + + + + + + + + + + + + ); + } +); + +RadioButtons.displayName = 'RadioButtons'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx index 5985a5fe03ec3..b8418004206b9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; -import { popupVersionsMap } from './popup_options_to_versions'; +import { popupVersionsMap } from '../protections/popup_options_to_versions'; export const SupportedVersionNotice = ({ optionName }: { optionName: string }) => { const version = popupVersionsMap.get(optionName); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx new file mode 100644 index 0000000000000..def9e78e994b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { cloneDeep } from 'lodash'; +import { + EuiSpacer, + EuiFlexItem, + EuiFlexGroup, + EuiCheckbox, + EuiIconTip, + EuiText, + EuiTextArea, +} from '@elastic/eui'; +import { + ImmutableArray, + ProtectionModes, + UIPolicyConfig, +} from '../../../../../../../common/endpoint/types'; +import { PolicyProtection, MacPolicyProtection } from '../../../types'; +import { ConfigFormHeading } from '../../components/config_form'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { AppAction } from '../../../../../../common/store/actions'; +import { SupportedVersionNotice } from './supported_version'; + +export const UserNotification = React.memo( + ({ + protection, + osList, + }: { + protection: PolicyProtection; + osList: ImmutableArray>; + }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch<(action: AppAction) => void>(); + const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; + const userNotificationSelected = + policyDetailsConfig && policyDetailsConfig.windows.popup[protection].enabled; + const userNotificationMessage = + policyDetailsConfig && policyDetailsConfig.windows.popup[protection].message; + + const handleUserNotificationCheckbox = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of osList) { + if (os === 'windows') { + newPayload[os].popup[protection].enabled = event.target.checked; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].enabled = + event.target.checked; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [policyDetailsConfig, dispatch, protection, osList] + ); + + const handleCustomUserNotification = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + for (const os of osList) { + if (os === 'windows') { + newPayload[os].popup[protection].message = event.target.value; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].message = event.target.value; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [policyDetailsConfig, dispatch, protection, osList] + ); + + return ( + <> + + + + + + + + {userNotificationSelected && ( + <> + + + + +

+ +

+
+
+ + + + + + + } + /> + +
+ + + + )} + + ); + } +); + +UserNotification.displayName = 'UserNotification'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index a5be095abfc59..03cd587ca7e5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -5,333 +5,29 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCallOut, - EuiCheckbox, - EuiRadio, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTextArea, - htmlIdGenerator, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, -} from '@elastic/eui'; -import { cloneDeep } from 'lodash'; -import styled from 'styled-components'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; -import { - Immutable, - OperatingSystem, - ProtectionModes, -} from '../../../../../../../common/endpoint/types'; +import { Immutable, OperatingSystem } from '../../../../../../../common/endpoint/types'; import { MalwareProtectionOSes, OS } from '../../../types'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { ConfigForm } from '../../components/config_form'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; import { useLicense } from '../../../../../../common/hooks/use_license'; -import { AppAction } from '../../../../../../common/store/actions'; -import { SupportedVersionNotice } from './supported_version'; - -export const RadioFlexGroup = styled(EuiFlexGroup)` - .no-right-margin-radio { - margin-right: 0; - } - .no-horizontal-margin-radio { - margin: ${(props) => props.theme.eui.ruleMargins.marginSmall} 0; - } -`; - -const OSes: Immutable = [OS.windows, OS.mac]; -const protection = 'malware'; - -const ProtectionRadio = React.memo( - ({ protectionMode, label }: { protectionMode: ProtectionModes; label: string }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch<(action: AppAction) => void>(); - const radioButtonId = useMemo(() => htmlIdGenerator()(), []); - // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value - const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const isPlatinumPlus = useLicense().isPlatinumPlus(); - - const handleRadioChange = useCallback(() => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os][protection].mode = protectionMode; - if (isPlatinumPlus) { - if (protectionMode === ProtectionModes.prevent) { - newPayload[os].popup[protection].enabled = true; - } else { - newPayload[os].popup[protection].enabled = false; - } - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, [dispatch, protectionMode, policyDetailsConfig, isPlatinumPlus]); - - /** - * Passing an arbitrary id because EuiRadio - * requires an id if label is passed - */ - - return ( - - ); - } -); - -ProtectionRadio.displayName = 'ProtectionRadio'; +import { RadioButtons } from '../components/radio_buttons'; +import { UserNotification } from '../components/user_notification'; +import { ProtectionSwitch } from '../components/protection_switch'; /** The Malware Protections form for policy details * which will configure for all relevant OSes. */ export const MalwareProtections = React.memo(() => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch<(action: AppAction) => void>(); - // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value - const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const userNotificationSelected = - policyDetailsConfig && policyDetailsConfig.windows.popup.malware.enabled; - const userNotificationMessage = - policyDetailsConfig && policyDetailsConfig.windows.popup.malware.message; + const OSes: Immutable = [OS.windows, OS.mac]; + const protection = 'malware'; const isPlatinumPlus = useLicense().isPlatinumPlus(); - const radios: Immutable< - Array<{ - id: ProtectionModes; - label: string; - protection: 'malware'; - }> - > = useMemo(() => { - return [ - { - id: ProtectionModes.detect, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', { - defaultMessage: 'Detect', - }), - protection: 'malware', - }, - { - id: ProtectionModes.prevent, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', { - defaultMessage: 'Prevent', - }), - protection: 'malware', - }, - ]; - }, []); - - const handleSwitchChange = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - if (event.target.checked === false) { - for (const os of OSes) { - newPayload[os][protection].mode = ProtectionModes.off; - if (isPlatinumPlus) { - newPayload[os].popup[protection].enabled = event.target.checked; - } - } - } else { - for (const os of OSes) { - newPayload[os][protection].mode = ProtectionModes.prevent; - if (isPlatinumPlus) { - newPayload[os].popup[protection].enabled = event.target.checked; - } - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [dispatch, policyDetailsConfig, isPlatinumPlus] - ); - - const handleUserNotificationCheckbox = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os].popup[protection].enabled = event.target.checked; - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [policyDetailsConfig, dispatch] - ); - - const handleCustomUserNotification = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os].popup[protection].message = event.target.value; - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [policyDetailsConfig, dispatch] - ); - - const radioButtons = useMemo(() => { - return ( - <> - - - - - - - - - - - - - {isPlatinumPlus && ( - <> - - - - - - - - - )} - {isPlatinumPlus && userNotificationSelected && ( - <> - - - - -

- -

-
-
- - - - - - - } - /> - -
- - - - )} - - ); - }, [ - radios, - selected, - isPlatinumPlus, - handleUserNotificationCheckbox, - userNotificationSelected, - userNotificationMessage, - handleCustomUserNotification, - ]); - - const protectionSwitch = useMemo(() => { - return ( - - ); - }, [handleSwitchChange, selected]); - return ( { })} supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC]} dataTestSubj="malwareProtectionsForm" - rightCorner={protectionSwitch} + rightCorner={} > - {radioButtons} + + {isPlatinumPlus && } = [OS.windows]; -const protection = 'ransomware'; - -const ProtectionRadio = React.memo( - ({ protectionMode, label }: { protectionMode: ProtectionModes; label: string }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch<(action: AppAction) => void>(); - const radioButtonId = useMemo(() => htmlIdGenerator()(), []); - const selected = policyDetailsConfig && policyDetailsConfig.windows.ransomware.mode; - - const handleRadioChange: EuiRadioProps['onChange'] = useCallback(() => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os][protection].mode = protectionMode; - if (protectionMode === ProtectionModes.prevent) { - newPayload[os].popup[protection].enabled = true; - } else { - newPayload[os].popup[protection].enabled = false; - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, [dispatch, protectionMode, policyDetailsConfig]); - - /** - * Passing an arbitrary id because EuiRadio - * requires an id if label is passed - */ - - return ( - - ); - } -); - -ProtectionRadio.displayName = 'ProtectionRadio'; +import { RadioButtons } from '../components/radio_buttons'; +import { UserNotification } from '../components/user_notification'; +import { ProtectionSwitch } from '../components/protection_switch'; /** The Ransomware Protections form for policy details * which will configure for all relevant OSes. */ export const Ransomware = React.memo(() => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch<(action: AppAction) => void>(); - const selected = policyDetailsConfig && policyDetailsConfig.windows.ransomware.mode; - const userNotificationSelected = - policyDetailsConfig && policyDetailsConfig.windows.popup.ransomware.enabled; - const userNotificationMessage = - policyDetailsConfig && policyDetailsConfig.windows.popup.ransomware.message; - - const radios: Immutable< - Array<{ - id: ProtectionModes; - label: string; - protection: 'ransomware'; - }> - > = useMemo(() => { - return [ - { - id: ProtectionModes.detect, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', { - defaultMessage: 'Detect', - }), - protection: 'ransomware', - }, - { - id: ProtectionModes.prevent, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', { - defaultMessage: 'Prevent', - }), - protection: 'ransomware', - }, - ]; - }, []); - - const handleSwitchChange: EuiSwitchProps['onChange'] = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - if (event.target.checked === false) { - for (const os of OSes) { - newPayload[os][protection].mode = ProtectionModes.off; - newPayload[os].popup[protection].enabled = event.target.checked; - } - } else { - for (const os of OSes) { - newPayload[os][protection].mode = ProtectionModes.prevent; - newPayload[os].popup[protection].enabled = event.target.checked; - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [dispatch, policyDetailsConfig] - ); - - const handleUserNotificationCheckbox: EuiCheckboxProps['onChange'] = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os].popup[protection].enabled = event.target.checked; - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [policyDetailsConfig, dispatch] - ); - - const handleCustomUserNotification = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of OSes) { - newPayload[os].popup[protection].message = event.target.value; - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [policyDetailsConfig, dispatch] - ); - - const radioButtons = useMemo(() => { - return ( - <> - - - - - - - - - - - - - - - - - - - - {userNotificationSelected && ( - <> - - - - -

- -

-
-
- - - - - - - } - /> - -
- - - - )} - - ); - }, [ - radios, - selected, - handleUserNotificationCheckbox, - userNotificationSelected, - userNotificationMessage, - handleCustomUserNotification, - ]); - - const protectionSwitch = useMemo(() => { - return ( - - ); - }, [handleSwitchChange, selected]); + const OSes: Immutable = [OS.windows]; + const protection = 'ransomware'; return ( { })} supportedOss={[OperatingSystem.WINDOWS]} dataTestSubj="ransomwareProtectionsForm" - rightCorner={protectionSwitch} + rightCorner={} > - {radioButtons} + + Date: Tue, 23 Mar 2021 15:24:24 -0400 Subject: [PATCH 06/88] [Fleet] Add `fleetServerEnabled` config setting and use it in SO migration for Endpoint Policies (#95204) * Add `agents.fleetServerEnabled` to plugin configuration * Use feature flag in Endpoint package policy SO migration --- x-pack/plugins/fleet/common/types/index.ts | 1 + .../fleet/mock/plugin_configuration.ts | 1 + x-pack/plugins/fleet/server/index.ts | 2 +- x-pack/plugins/fleet/server/plugin.ts | 6 +++-- .../security_solution/to_v7_13_0.test.ts | 23 +++++++++++++++++++ .../security_solution/to_v7_13_0.ts | 22 ++++++++++-------- .../saved_objects/migrations/to_v7_13_0.ts | 12 ++++++---- .../fleet/server/services/app_context.ts | 5 ++++ 8 files changed, 56 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 4223697703a8d..5c385f938a69e 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -14,6 +14,7 @@ export interface FleetConfigType { registryProxyUrl?: string; agents: { enabled: boolean; + fleetServerEnabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; maxConcurrentConnections: number; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 5d53425607361..81ef6a6703c34 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -14,6 +14,7 @@ export const createConfigurationMock = (): FleetConfigType => { registryProxyUrl: '', agents: { enabled: true, + fleetServerEnabled: false, tlsCheckDisabled: true, pollingRequestTimeout: 1000, maxConcurrentConnections: 100, diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 73a8b419a869d..8bad868b813ac 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -38,7 +38,6 @@ export const config: PluginConfigDescriptor = { deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('xpack.ingestManager', 'xpack.fleet'), renameFromRoot('xpack.fleet.fleet', 'xpack.fleet.agents'), - unused('agents.fleetServerEnabled'), ], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -46,6 +45,7 @@ export const config: PluginConfigDescriptor = { registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), + fleetServerEnabled: schema.boolean({ defaultValue: false }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), pollingRequestTimeout: schema.number({ defaultValue: AGENT_POLLING_REQUEST_TIMEOUT_MS, diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 477e6c3959951..5d7b05c5eddcb 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -209,6 +209,10 @@ export class FleetPlugin this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; + const config = await this.config$.pipe(first()).toPromise(); + + appContextService.fleetServerEnabled = config.agents.fleetServerEnabled; + registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -248,8 +252,6 @@ export class FleetPlugin const router = core.http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); - // Register usage collection registerFleetUsageCollector(core, config, deps.usageCollection); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.test.ts index 75e2922bd5149..6897efbe94110 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.test.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.test.ts @@ -12,6 +12,8 @@ import type { PackagePolicy } from '../../../../common'; import { migrationMocks } from '../../../../../../../src/core/server/mocks'; +import { appContextService } from '../../../services'; + import { migrateEndpointPackagePolicyToV7130 } from './to_v7_13_0'; describe('7.13.0 Endpoint Package Policy migration', () => { @@ -126,6 +128,16 @@ describe('7.13.0 Endpoint Package Policy migration', () => { const migrationContext = migrationMocks.createContext(); + beforeEach(() => { + // set `fleetServerEnabled` flag to true + appContextService.fleetServerEnabled = true; + }); + + afterEach(() => { + // set `fleetServerEnabled` flag back to false + appContextService.fleetServerEnabled = false; + }); + it('should adjust the relative url for all artifact manifests', () => { expect( migrateEndpointPackagePolicyToV7130(createOldPackagePolicySO(), migrationContext) @@ -142,4 +154,15 @@ describe('7.13.0 Endpoint Package Policy migration', () => { unchangedPackagePolicySo ); }); + + it('should NOT migrate artifact relative_url if fleetServerEnabled is false', () => { + const packagePolicySo = createOldPackagePolicySO(); + const unchangedPackagePolicySo = cloneDeep(packagePolicySo); + + appContextService.fleetServerEnabled = false; + + expect(migrateEndpointPackagePolicyToV7130(packagePolicySo, migrationContext)).toEqual( + unchangedPackagePolicySo + ); + }); }); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.ts index 655ce37b4faaf..5eb0c7a6e3141 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_13_0.ts @@ -10,6 +10,7 @@ import type { SavedObjectMigrationFn } from 'kibana/server'; import type { PackagePolicy } from '../../../../common'; import { relativeDownloadUrlFromArtifact } from '../../../services/artifacts/mappings'; import type { ArtifactElasticsearchProperties } from '../../../services'; +import { appContextService } from '../../../services'; type ArtifactManifestList = Record< string, @@ -21,16 +22,19 @@ export const migrateEndpointPackagePolicyToV7130: SavedObjectMigrationFn< PackagePolicy > = (packagePolicyDoc) => { if (packagePolicyDoc.attributes.package?.name === 'endpoint') { - // Adjust all artifact URLs so that they point at fleet-server - const artifactList: ArtifactManifestList = - packagePolicyDoc.attributes?.inputs[0]?.config?.artifact_manifest.value.artifacts; + // Feature condition check here is temporary until v7.13 ships + if (appContextService.fleetServerEnabled) { + // Adjust all artifact URLs so that they point at fleet-server + const artifactList: ArtifactManifestList = + packagePolicyDoc.attributes?.inputs[0]?.config?.artifact_manifest.value.artifacts; - if (artifactList) { - for (const [identifier, artifactManifest] of Object.entries(artifactList)) { - artifactManifest.relative_url = relativeDownloadUrlFromArtifact({ - identifier, - decodedSha256: artifactManifest.decoded_sha256, - }); + if (artifactList) { + for (const [identifier, artifactManifest] of Object.entries(artifactList)) { + artifactManifest.relative_url = relativeDownloadUrlFromArtifact({ + identifier, + decodedSha256: artifactManifest.decoded_sha256, + }); + } } } } diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts index 1cc2394a8e5fe..e4ba7ce56e847 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts @@ -32,11 +32,15 @@ export const migratePackagePolicyToV7130: SavedObjectMigrationFn { + let updatedPackagePolicyDoc = packagePolicyDoc; + // Endpoint specific migrations - // FIXME:PT remove `-OFF` from below once ready to be released - if (packagePolicyDoc.attributes.package?.name === 'endpoint-OFF') { - return migrateEndpointPackagePolicyToV7130(packagePolicyDoc, migrationContext); + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + updatedPackagePolicyDoc = migrateEndpointPackagePolicyToV7130( + packagePolicyDoc, + migrationContext + ); } - return packagePolicyDoc; + return updatedPackagePolicyDoc; }; diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 954308a980861..c49e536435027 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -44,6 +44,11 @@ class AppContextService { private httpSetup?: HttpServiceSetup; private externalCallbacks: ExternalCallbacksStorage = new Map(); + /** + * Temporary flag until v7.13 ships + */ + public fleetServerEnabled: boolean = false; + public async start(appContext: FleetAppContext) { this.data = appContext.data; this.esClient = appContext.elasticsearch.client.asInternalUser; From ba21c315c9e7292273a5bb5a4af6a6768a17a2f8 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 23 Mar 2021 15:47:14 -0400 Subject: [PATCH 07/88] Document spaces telemetry fields (#95087) --- .../spaces_usage_collector.ts | 294 +++++++++++++++--- .../schema/xpack_plugins.json | 200 +++++++++--- 2 files changed, 413 insertions(+), 81 deletions(-) diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 60a2acc5319df..c0cf71fab0558 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -179,50 +179,262 @@ export function getSpacesUsageCollector( type: 'spaces', isReady: () => true, schema: { - usesFeatureControls: { type: 'boolean' }, + usesFeatureControls: { + type: 'boolean', + _meta: { + description: + 'Indicates if at least one feature is disabled in at least one space. This is a signal that space-level feature controls are in use. This does not account for role-based (security) feature controls.', + }, + }, disabledFeatures: { // "feature": number; - DYNAMIC_KEY: { type: 'long' }, + DYNAMIC_KEY: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, // Known registered features - stackAlerts: { type: 'long' }, - actions: { type: 'long' }, - enterpriseSearch: { type: 'long' }, - fleet: { type: 'long' }, - savedObjectsTagging: { type: 'long' }, - indexPatterns: { type: 'long' }, - discover: { type: 'long' }, - canvas: { type: 'long' }, - maps: { type: 'long' }, - siem: { type: 'long' }, - monitoring: { type: 'long' }, - graph: { type: 'long' }, - uptime: { type: 'long' }, - savedObjectsManagement: { type: 'long' }, - timelion: { type: 'long' }, - dev_tools: { type: 'long' }, - advancedSettings: { type: 'long' }, - infrastructure: { type: 'long' }, - visualize: { type: 'long' }, - logs: { type: 'long' }, - dashboard: { type: 'long' }, - ml: { type: 'long' }, - apm: { type: 'long' }, - }, - available: { type: 'boolean' }, - enabled: { type: 'boolean' }, - count: { type: 'long' }, - 'apiCalls.copySavedObjects.total': { type: 'long' }, - 'apiCalls.copySavedObjects.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.copySavedObjects.kibanaRequest.no': { type: 'long' }, - 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': { type: 'long' }, - 'apiCalls.copySavedObjects.createNewCopiesEnabled.no': { type: 'long' }, - 'apiCalls.copySavedObjects.overwriteEnabled.yes': { type: 'long' }, - 'apiCalls.copySavedObjects.overwriteEnabled.no': { type: 'long' }, - 'apiCalls.resolveCopySavedObjectsErrors.total': { type: 'long' }, - 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no': { type: 'long' }, - 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': { type: 'long' }, - 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': { type: 'long' }, + stackAlerts: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + actions: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + enterpriseSearch: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + fleet: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + savedObjectsTagging: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + indexPatterns: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + discover: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + canvas: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + maps: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + siem: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + monitoring: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + graph: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + uptime: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + savedObjectsManagement: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + timelion: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + dev_tools: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + advancedSettings: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + infrastructure: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + visualize: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + logs: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + dashboard: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + ml: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + apm: { + type: 'long', + _meta: { + description: 'The number of spaces which have this feature disabled.', + }, + }, + }, + available: { + type: 'boolean', + _meta: { + description: 'Indicates if the spaces feature is available in this installation.', + }, + }, + enabled: { + type: 'boolean', + _meta: { + description: 'Indicates if the spaces feature is enabled in this installation.', + }, + }, + count: { + type: 'long', + _meta: { + description: 'The number of spaces in this installation.', + }, + }, + 'apiCalls.copySavedObjects.total': { + type: 'long', + _meta: { + description: 'The number of times the "Copy Saved Objects" API has been called.', + }, + }, + 'apiCalls.copySavedObjects.kibanaRequest.yes': { + type: 'long', + _meta: { + description: + 'The number of times the "Copy Saved Objects" API has been called via the Kibana client.', + }, + }, + 'apiCalls.copySavedObjects.kibanaRequest.no': { + type: 'long', + _meta: { + description: + 'The number of times the "Copy Saved Objects" API has been called via an API consumer (e.g. curl).', + }, + }, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': { + type: 'long', + _meta: { + description: + 'The number of times the "Copy Saved Objects" API has been called with "createNewCopies" set to true.', + }, + }, + 'apiCalls.copySavedObjects.createNewCopiesEnabled.no': { + type: 'long', + _meta: { + description: + 'The number of times the "Copy Saved Objects" API has been called with "createNewCopies" set to false.', + }, + }, + 'apiCalls.copySavedObjects.overwriteEnabled.yes': { + type: 'long', + _meta: { + description: + 'The number of times the "Copy Saved Objects" API has been called with "overwrite" set to true.', + }, + }, + 'apiCalls.copySavedObjects.overwriteEnabled.no': { + type: 'long', + _meta: { + description: + 'The number of times the "Copy Saved Objects" API has been called with "overwrite" set to false.', + }, + }, + 'apiCalls.resolveCopySavedObjectsErrors.total': { + type: 'long', + _meta: { + description: + 'The number of times the "Resolve Copy Saved Objects Errors" API has been called.', + }, + }, + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes': { + type: 'long', + _meta: { + description: + 'The number of times the "Resolve Copy Saved Objects Errors" API has been called via the Kibana client.', + }, + }, + 'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no': { + type: 'long', + _meta: { + description: + 'The number of times the "Resolve Copy Saved Objects Errors" API has been called via an API consumer (e.g. curl).', + }, + }, + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': { + type: 'long', + _meta: { + description: + 'The number of times the "Resolve Copy Saved Objects Errors" API has been called with "createNewCopies" set to true.', + }, + }, + 'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': { + type: 'long', + _meta: { + description: + 'The number of times the "Resolve Copy Saved Objects Errors" API has been called with "createNewCopies" set to false.', + }, + }, }, fetch: async ({ esClient }: CollectorFetchContext) => { const { licensing, kibanaIndexConfig$, features, usageStatsServicePromise } = deps; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 81a7030fe0edd..ed8e44072b914 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3766,128 +3766,248 @@ "spaces": { "properties": { "usesFeatureControls": { - "type": "boolean" + "type": "boolean", + "_meta": { + "description": "Indicates if at least one feature is disabled in at least one space. This is a signal that space-level feature controls are in use. This does not account for role-based (security) feature controls." + } }, "disabledFeatures": { "properties": { "DYNAMIC_KEY": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "stackAlerts": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "actions": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "enterpriseSearch": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "fleet": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "savedObjectsTagging": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "indexPatterns": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "discover": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "canvas": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "maps": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "siem": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "monitoring": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "graph": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "uptime": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "savedObjectsManagement": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "timelion": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "dev_tools": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "advancedSettings": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "infrastructure": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "visualize": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "logs": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "dashboard": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "ml": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } }, "apm": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces which have this feature disabled." + } } } }, "available": { - "type": "boolean" + "type": "boolean", + "_meta": { + "description": "Indicates if the spaces feature is available in this installation." + } }, "enabled": { - "type": "boolean" + "type": "boolean", + "_meta": { + "description": "Indicates if the spaces feature is enabled in this installation." + } }, "count": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of spaces in this installation." + } }, "apiCalls.copySavedObjects.total": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called." + } }, "apiCalls.copySavedObjects.kibanaRequest.yes": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called via the Kibana client." + } }, "apiCalls.copySavedObjects.kibanaRequest.no": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called via an API consumer (e.g. curl)." + } }, "apiCalls.copySavedObjects.createNewCopiesEnabled.yes": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called with \"createNewCopies\" set to true." + } }, "apiCalls.copySavedObjects.createNewCopiesEnabled.no": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called with \"createNewCopies\" set to false." + } }, "apiCalls.copySavedObjects.overwriteEnabled.yes": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called with \"overwrite\" set to true." + } }, "apiCalls.copySavedObjects.overwriteEnabled.no": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Copy Saved Objects\" API has been called with \"overwrite\" set to false." + } }, "apiCalls.resolveCopySavedObjectsErrors.total": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Resolve Copy Saved Objects Errors\" API has been called." + } }, "apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Resolve Copy Saved Objects Errors\" API has been called via the Kibana client." + } }, "apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Resolve Copy Saved Objects Errors\" API has been called via an API consumer (e.g. curl)." + } }, "apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Resolve Copy Saved Objects Errors\" API has been called with \"createNewCopies\" set to true." + } }, "apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no": { - "type": "long" + "type": "long", + "_meta": { + "description": "The number of times the \"Resolve Copy Saved Objects Errors\" API has been called with \"createNewCopies\" set to false." + } } } }, From 3cfb4f061ee6fda6f8e5e601dafccd99120db4b9 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 23 Mar 2021 13:44:51 -0700 Subject: [PATCH 08/88] Warns usage collection is internal only (#95121) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/usage_collection/README.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index e6759b7dc6c7c..04e1e0fbb5006 100644 --- a/src/plugins/usage_collection/README.mdx +++ b/src/plugins/usage_collection/README.mdx @@ -7,10 +7,13 @@ date: 2021-02-24 tags: ['kibana','dev', 'contributor', 'api docs'] --- + # Kibana Usage Collection Service The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. At the same time, it provides necessary the APIs for other services (i.e.: telemetry, monitoring, ...) to consume that usage data. +IMPORTANT: Usage collection and telemetry applies to internal Elastic Kibana developers only. + ## How to report my plugin's usage? The way to report the usage of any feature depends on whether the actions to track occur in the UI, or the usage depends on any server-side data. For that reason, the set of APIs exposed in the `public` and `server` contexts are different. From 3ff76fd02217ffc3f07ca5233e48df920cbbe280 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 23 Mar 2021 17:02:36 -0500 Subject: [PATCH 09/88] [Workplace Search] Fix redirect and state for personal oAuth plugin (#95238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move source added route to top-level index component * Use state passed back from oAuth app to determine context The previous tests weren’t actually using this state so they have been updated with actual state data for proper testing --- .../workplace_search/index.test.tsx | 7 +++++++ .../applications/workplace_search/index.tsx | 5 +++++ .../add_source/add_source_logic.test.ts | 21 +++++++++++++------ .../components/add_source/add_source_logic.ts | 3 ++- .../components/source_added.tsx | 10 ++++++++- .../content_sources/sources_router.test.tsx | 2 +- .../views/content_sources/sources_router.tsx | 6 ------ 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index ceb1a82446132..48bdcd6551b65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -16,6 +16,7 @@ import { shallow } from 'enzyme'; import { Layout } from '../shared/layout'; import { WorkplaceSearchHeaderActions } from './components/layout'; +import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; import { Overview as OverviewMVP } from './views/overview_mvp'; import { SetupGuide } from './views/setup_guide'; @@ -94,4 +95,10 @@ describe('WorkplaceSearchConfigured', () => { expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); }); + + it('renders SourceAdded', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceAdded)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 656c93053e22b..c269a987dc092 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -24,12 +24,14 @@ import { GROUPS_PATH, SETUP_GUIDE_PATH, SOURCES_PATH, + SOURCE_ADDED_PATH, PERSONAL_SOURCES_PATH, ORG_SETTINGS_PATH, ROLE_MAPPINGS_PATH, SECURITY_PATH, } from './routes'; import { SourcesRouter } from './views/content_sources'; +import { SourceAdded } from './views/content_sources/components/source_added'; import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { PrivateSourcesLayout } from './views/content_sources/private_sources_layout'; import { ErrorState } from './views/error_state'; @@ -82,6 +84,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + + {errorConnecting ? : } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index ed67eb9994bc8..d0ab40399fa59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -275,12 +275,12 @@ describe('AddSourceLogic', () => { describe('saveSourceParams', () => { const params = { code: 'code123', - state: '"{"state": "foo"}"', - session_state: 'session123', + state: + '{"action":"create","context":"organization","service_type":"gmail","csrf_token":"token==","index_permissions":false}', }; const queryString = - 'code=code123&state=%22%7B%22state%22%3A%20%22foo%22%7D%22&session_state=session123'; + '?state=%7B%22action%22:%22create%22,%22context%22:%22organization%22,%22service_type%22:%22gmail%22,%22csrf_token%22:%22token%3D%3D%22,%22index_permissions%22:false%7D&code=code123'; const response = { serviceName: 'name', indexPermissions: false, serviceType: 'zendesk' }; @@ -303,9 +303,18 @@ describe('AddSourceLogic', () => { await nextTick(); expect(setAddedSourceSpy).toHaveBeenCalledWith(serviceName, indexPermissions, serviceType); - expect(navigateToUrl).toHaveBeenCalledWith( - getSourcesPath(SOURCES_PATH, AppLogic.values.isOrganization) - ); + expect(navigateToUrl).toHaveBeenCalledWith(getSourcesPath(SOURCES_PATH, true)); + }); + + it('redirects to private dashboard when account context', async () => { + const accountQueryString = + '?state=%7B%22action%22:%22create%22,%22context%22:%22account%22,%22service_type%22:%22gmail%22,%22csrf_token%22:%22token%3D%3D%22,%22index_permissions%22:false%7D&code=code'; + + AddSourceLogic.actions.saveSourceParams(accountQueryString); + + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalledWith(getSourcesPath(SOURCES_PATH, false)); }); it('handles error', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 4e996aff6f5b0..e1f554d87551d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -470,12 +470,13 @@ export const AddSourceLogic = kea { const { http } = HttpLogic.values; - const { isOrganization } = AppLogic.values; const { navigateToUrl } = KibanaLogic.values; const { setAddedSource } = SourcesLogic.actions; const params = (parseQueryParams(search) as unknown) as OauthParams; const query = { ...params, kibana_host: kibanaHost }; const route = '/api/workplace_search/sources/create'; + const state = JSON.parse(params.state); + const isOrganization = state.context !== 'account'; try { const response = await http.get(route, { query }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx index 5f1d2ed0c81c3..7c4e81d8e0755 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -11,6 +11,8 @@ import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions } from 'kea'; +import { EuiPage, EuiPageBody } from '@elastic/eui'; + import { Loading } from '../../../../shared/loading'; import { AddSourceLogic } from './add_source/add_source_logic'; @@ -28,5 +30,11 @@ export const SourceAdded: React.FC = () => { saveSourceParams(search); }, []); - return ; + return ( + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx index 2438061c67759..eac1bd7d3ea27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx @@ -34,7 +34,7 @@ describe('SourcesRouter', () => { }); it('renders sources routes', () => { - const TOTAL_ROUTES = 62; + const TOTAL_ROUTES = 61; const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index b7857cf4612a2..f4a56c8a0beaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -19,7 +19,6 @@ import { AppLogic } from '../../app_logic'; import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, - SOURCE_ADDED_PATH, SOURCE_DETAILS_PATH, PERSONAL_SOURCES_PATH, SOURCES_PATH, @@ -27,7 +26,6 @@ import { } from '../../routes'; import { AddSource, AddSourceList } from './components/add_source'; -import { SourceAdded } from './components/source_added'; import { OrganizationSources } from './organization_sources'; import { PrivateSources } from './private_sources'; import { staticSourceData } from './source_data'; @@ -115,10 +113,6 @@ export const SourcesRouter: React.FC = () => {
- - - - From 80b05b914ac7e521155feb2a6f0bb124337ae76a Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 23 Mar 2021 17:39:57 -0500 Subject: [PATCH 10/88] [Workplace Search] Add missing tests to get 100% coverage (#95240) --- .../workplace_search/app_logic.test.ts | 13 +++- .../workplace_search/routes.test.tsx | 60 ++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index 8ba94e83d26cf..82fc00923202f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -48,7 +48,7 @@ describe('AppLogic', () => { expect(AppLogic.values).toEqual(DEFAULT_VALUES); }); - describe('initializeAppData()', () => { + describe('initializeAppData', () => { it('sets values based on passed props', () => { AppLogic.actions.initializeAppData(DEFAULT_INITIAL_APP_DATA); @@ -66,11 +66,20 @@ describe('AppLogic', () => { }); }); - describe('setContext()', () => { + describe('setContext', () => { it('sets context', () => { AppLogic.actions.setContext(true); expect(AppLogic.values.isOrganization).toEqual(true); }); }); + + describe('setSourceRestriction', () => { + it('sets property', () => { + mount(DEFAULT_INITIAL_APP_DATA); + AppLogic.actions.setSourceRestriction(true); + + expect(AppLogic.values.account.canCreatePersonalSources).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx index 68bec94270a01..7d3e19dfe626a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -13,8 +13,15 @@ import { EuiLink } from '@elastic/eui'; import { getContentSourcePath, + getGroupPath, + getGroupSourcePrioritizationPath, + getReindexJobRoute, + getRoleMappingPath, + getSourcesPath, + GROUPS_PATH, SOURCES_PATH, PERSONAL_SOURCES_PATH, + ROLE_MAPPINGS_PATH, SOURCE_DETAILS_PATH, } from './routes'; @@ -24,17 +31,66 @@ const TestComponent = ({ id, isOrg }: { id: string; isOrg?: boolean }) => { }; describe('getContentSourcePath', () => { - it('should format org route', () => { + it('should format org path', () => { const wrapper = shallow(); const path = wrapper.find(EuiLink).prop('href'); expect(path).toEqual(`${SOURCES_PATH}/123`); }); - it('should format user route', () => { + it('should format user path', () => { const wrapper = shallow(); const path = wrapper.find(EuiLink).prop('href'); expect(path).toEqual(`${PERSONAL_SOURCES_PATH}/123`); }); }); + +describe('getGroupPath', () => { + it('should format path', () => { + expect(getGroupPath('123')).toEqual(`${GROUPS_PATH}/123`); + }); +}); + +describe('getRoleMappingPath', () => { + it('should format path', () => { + expect(getRoleMappingPath('123')).toEqual(`${ROLE_MAPPINGS_PATH}/123`); + }); +}); + +describe('getGroupSourcePrioritizationPath', () => { + it('should format path', () => { + expect(getGroupSourcePrioritizationPath('123')).toEqual( + `${GROUPS_PATH}/123/source_prioritization` + ); + }); +}); + +describe('getSourcesPath', () => { + const PATH = '/foo/123'; + + it('should format org path', () => { + expect(getSourcesPath(PATH, true)).toEqual(PATH); + }); + + it('should format user path', () => { + expect(getSourcesPath(PATH, false)).toEqual(`/p${PATH}`); + }); +}); + +describe('getReindexJobRoute', () => { + const SOURCE_ID = '234'; + const REINDEX_ID = '345'; + + it('should format org path', () => { + expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, true)).toEqual( + `/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}` + ); + }); + + it('should format user path', () => { + expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, false)).toEqual( + `/p/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}` + ); + }); +}); From 1fc50005ccc14ee94d88c7becb939402eced43b6 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 23 Mar 2021 18:54:03 -0400 Subject: [PATCH 11/88] Create best practices doc in developer guide (#94981) * Create best_practies.mdx * Update best_practices.mdx Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/assets/product_stages.png | Bin 0 -> 205603 bytes dev_docs/best_practices.mdx | 148 +++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 dev_docs/assets/product_stages.png create mode 100644 dev_docs/best_practices.mdx diff --git a/dev_docs/assets/product_stages.png b/dev_docs/assets/product_stages.png new file mode 100644 index 0000000000000000000000000000000000000000..d62c5570d0dde067b14422a38827a35fa65a7f79 GIT binary patch literal 205603 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

5mor-GOe{}m>D7|UhI-hkAe$}b1qNo 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 and check out all the available +platform that can simplify plugin development. + +## Performance + +Build with scalability in mind. + +- Consider data with many fields +- Consider data with high cardinality fields +- Consider large data sets, that span a long time range +- Consider slow internet and low bandwidth environments + +## Accessibility + +Did you know Kibana makes a public statement about our commitment to creating an accessible product for people +with disabilities? [We do](https://www.elastic.co/guide/en/kibana/master/accessibility.html)! It's very important +all of our apps are accessible. + +- Learn how [EUI tackles accessibility](https://elastic.github.io/eui/#/guidelines/accessibility) +- If you don't use EUI, follow the same EUI accessibility standards + + + Elasticians, check out the #accessibility channel to ask questions and receive guidance. + + +## Localization + +Kibana is translated into other languages. Use our i18n utilities to ensure your public facing strings will be translated to ensure all Kibana apps are localized. Read and adhere to our [i18n guidelines](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/GUIDELINE.md) + + + Elasticians, check out the #kibana-localization channel to ask questions and receive guidance. + + +## Styleguide + +We use es-lint rules when possible, but please review our [styleguide](https://github.com/elastic/kibana/blob/master/STYLEGUIDE.md), which includes recommendations that can't be linted on. + +Es-lint overrides on a per-plugin level are discouraged. + +## Plugin best practices + +Don't export without reason. Make your public APIs as small as possible. You will have to maintain them, and consider backward compatibility when making changes. + +Add `README.md` to all your plugins and services and include contact information. + +## Re-inventing the wheel + +Over-refactoring can be a problem in it's own right, but it's still important to be aware of the existing services that are out there and use them when it makes sense. Check out our to see what high-level services are at your disposal. In addition, our lists additional services. + +## Feature development + +### Timing + + + +Try not to put your PR in review mode, or merge large changes, right before Feature Freeze. It's inevitably one of the most volatile times for the +Kibana code base, try not to contribute to this volatility. Doing this can: + +- increase the likelyhood of conflicts from other features being merged at the last minute +- means your feature has less QA time +- means your feature gets less careful review as reviewers are often swamped at this time + +All of the above contributes to more bugs being found in the QA cycle and can cause a delay in the release. Prefer instead to merge +your large change right _after_ feature freeze. If you are worried about missing your initial release version goals, review our +[release train philophy](https://github.com/elastic/dev/blob/master/shared/time-based-releases.md). It's okay! + + + +### Size + +When possible, build features with incrementals sets of small and focused PRs, but don't check in unused code, and don't expose any feature on master that you would not be comfortable releasing. + +![product_stages](./assets/product_stages.png) + +If your feature cannot be broken down into smaller components, or multiple engineers will be contributing, you have a few other options to consider. + +**1. Hide your feature behind a feature flag** + +Features can be merged behind a flag if you are not ready to make them the default experience, but all code should still be tested, complete and bug free. + +A good question to ask yourself is, how will you feel if a customer turns this feature on? Is it usable, even if not up to the +level of something we would market? It should have some level of minimal utility. + +Another question to ask yourself is, if this feature gets cancelled, how difficult will it be to remove? + +**2. Develop on a feature branch** + +This option is useful if you have more than one contributor working on a large feature. The downside is handling code conflicts when rebasing with the main branch. + +Consider how you want to handle the PR review. Reviewing each PR going into the feature branch can lighten the review process when merging into the main branch. + +**3. Use an example plugin** + +If you are building a service for developers, create an [example plugin](https://github.com/elastic/kibana/tree/master/examples) to showcase and test intended usage. This is a great way for reviewers and PMs to play around with a feature through the UI, before the production UI is ready. This can also help developers consuming your services get hands on. + +## Embrace the monorepo + +Kibana uses a monorepo and our processes and tooling are built around this decision. Utilizing a monorepo allows us to have a consistent peer review process and enforce the same code quality standards across all of Kibana's code. It also eliminates the necessity to have separate versioning strategies and constantly coordinate changes across repos. + +When experimenting with code, it's completely fine to create a separate GitHub repo to use for initial development. Once the code has matured to a point where it's ready to be used within Kibana, it should be integrated into the Kibana GitHub repo. + +There are some exceptions where a separate repo makes sense. However, they are exceptions to the rule. A separate repo has proven beneficial when there's a dedicated team collaborating on a package which has multiple consumers, for example [EUI](https://github.com/elastic/eui). + +It may be tempting to get caught up in the dream of writing the next package which is published to npm and downloaded millions of times a week. Knowing the quality of developers that are working on Kibana, this is a real possibility. However, knowing which packages will see mass adoption is impossible to predict. Instead of jumping directly to writing code in a separate repo and accepting all of the complications that come along with it, prefer keeping code inside the Kibana repo. A [Kibana package](https://github.com/elastic/kibana/tree/master/packages) can be used to publish a package to npm, while still keeping the code inside the Kibana repo. Move code to an external repo only when there is a good reason, for example to enable external contributions. + +## Hardening + +Review the following items related to vulnerability and security risks. + +- XSS + - Check for usages of `dangerouslySetInnerHtml`, `Element.innerHTML`, `Element.outerHTML` + - Ensure all user input is properly escaped. + - Ensure any input in `$.html`, `$.append`, `$.appendTo`, $.prepend`, `$.prependTo`is escaped. Instead use`$.text`, or don't use jQuery at all. +- CSRF + - Ensure all APIs are running inside the Kibana HTTP service. +- RCE + - Ensure no usages of `eval` + - Ensure no usages of dynamic requires + - Check for template injection + - Check for usages of templating libraries, including `_.template`, and ensure that user provided input isn't influencing the template and is only used as data for rendering the template. + - Check for possible prototype pollution. +- Prototype Pollution - more info [here](https://docs.google.com/document/d/19V-d9sb6IF-fbzF4iyiPpAropQNydCnoJApzSX5FdcI/edit?usp=sharing) + - Check for instances of `anObject[a][b] = c` where a, b, and c are user defined. This includes code paths where the following logical code steps could be performed in separate files by completely different operations, or recursively using dynamic operations. + - Validate any user input, including API url-parameters/query-parameters/payloads, preferable against a schema which only allows specific keys/values. At a very minimum, black-list `__proto__` and `prototype.constructor` for use within keys + - When calling APIs which spawn new processes or potentially perform code generation from strings, defensively protect against Prototype Pollution by checking `Object.hasOwnProperty` if the arguments to the APIs originate from an Object. An example is the Code app's [spawnProcess](https://github.com/elastic/kibana/blob/b49192626a8528af5d888545fb14cd1ce66a72e7/x-pack/legacy/plugins/code/server/lsp/workspace_command.ts#L40-L44). + - Common Node.js offenders: `child_process.spawn`, `child_process.exec`, `eval`, `Function('some string')`, `vm.runIn*Context(x)` + - Common Client-side offenders: `eval`, `Function('some string')`, `setTimeout('some string', num)`, `setInterval('some string', num)` +- Check for accidental reveal of sensitive information + - The biggest culprit is errors which contain stack traces or other sensitive information which end up in the HTTP Response +- Checked for Mishandled API requests + - Ensure no sensitive cookies are forwarded to external resources. + - Ensure that all user controllable variables that are used in constructing a URL are escaped properly. This is relevant when using `transport.request` with the Elasticsearch client as no automatic escaping is performed. +- Reverse tabnabbing - https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTML5_Security_Cheat_Sheet.md#tabnabbing + - When there are user controllable links or hard-coded links to third-party domains that specify target="\_blank" or target="\_window", the `a` tag should have the rel="noreferrer noopener" attribute specified. + - Allowing users to input markdown is a common culprit, a custom link renderer should be used +- SSRF - https://www.owasp.org/index.php/Server_Side_Request_Forgery + - All network requests made from the Kibana server should use an explicit configuration or white-list specified in the `kibana.yml` From 52d0fc044adc1f705cdaef0538763d3e1503e047 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 23 Mar 2021 22:52:44 -0500 Subject: [PATCH 12/88] update codeowners (#95249) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dde90bf1bc47d..2f2f260addb35 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -53,6 +53,7 @@ /src/plugins/navigation/ @elastic/kibana-app-services /src/plugins/share/ @elastic/kibana-app-services /src/plugins/ui_actions/ @elastic/kibana-app-services +/src/plugins/index_pattern_field_editor @elastic/kibana-app-services /x-pack/examples/ui_actions_enhanced_examples/ @elastic/kibana-app-services /x-pack/plugins/data_enhanced/ @elastic/kibana-app-services /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-services From 585f6f2c5c9e5d8d77aa6962555bbc3828a01071 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Mar 2021 09:29:38 +0100 Subject: [PATCH 13/88] Make sure color mapping setting is respected for legacy palette (#95164) --- docs/management/advanced-options.asciidoc | 2 +- src/plugins/charts/public/mocks.ts | 4 +- src/plugins/charts/public/plugin.ts | 2 +- .../public/services/legacy_colors/mock.ts | 1 + .../services/palettes/palettes.test.tsx | 55 +++++++++++++++++-- .../public/services/palettes/palettes.tsx | 7 +-- .../public/services/palettes/service.ts | 5 +- src/plugins/charts/server/plugin.ts | 2 +- 8 files changed, 61 insertions(+), 17 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 5c27a7bdacdee..446b6a2cfd851 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -457,7 +457,7 @@ Enables the legacy charts library for aggregation-based area, line, and bar char [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** -Maps values to specific colors in *Visualize* charts and *TSVB*. This setting does not apply to *Lens*. +Maps values to specific colors in charts using the *Compatibility* palette. [[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed when highlighting another element diff --git a/src/plugins/charts/public/mocks.ts b/src/plugins/charts/public/mocks.ts index a6cde79057be8..c85a91a1ef563 100644 --- a/src/plugins/charts/public/mocks.ts +++ b/src/plugins/charts/public/mocks.ts @@ -17,13 +17,13 @@ export type Start = jest.Mocked>; const createSetupContract = (): Setup => ({ legacyColors: colorsServiceMock, theme: themeServiceMock, - palettes: paletteServiceMock.setup({} as any, {} as any), + palettes: paletteServiceMock.setup({} as any), }); const createStartContract = (): Start => ({ legacyColors: colorsServiceMock, theme: themeServiceMock, - palettes: paletteServiceMock.setup({} as any, {} as any), + palettes: paletteServiceMock.setup({} as any), }); export { colorMapsMock } from './static/color_maps/mock'; diff --git a/src/plugins/charts/public/plugin.ts b/src/plugins/charts/public/plugin.ts index a21264703f6c4..5bc0b8c84560f 100644 --- a/src/plugins/charts/public/plugin.ts +++ b/src/plugins/charts/public/plugin.ts @@ -43,7 +43,7 @@ export class ChartsPlugin implements Plugin { - const palettes: Record = buildPalettes( - coreMock.createStart().uiSettings, - colorsServiceMock - ); + const palettes: Record = buildPalettes(colorsServiceMock); describe('default palette', () => { describe('syncColors: false', () => { it('should return different colors based on behind text flag', () => { @@ -302,6 +298,7 @@ describe('palettes', () => { beforeEach(() => { (colorsServiceMock.mappedColors.mapKeys as jest.Mock).mockClear(); + (colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock).mockReset(); (colorsServiceMock.mappedColors.get as jest.Mock).mockClear(); }); @@ -323,6 +320,30 @@ describe('palettes', () => { expect(colorsServiceMock.mappedColors.get).not.toHaveBeenCalled(); }); + it('should respect the advanced settings color mapping', () => { + const configColorGetter = colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock; + configColorGetter.mockImplementation(() => 'blue'); + const result = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 2, + totalSeriesAtDepth: 10, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: false, + } + ); + expect(result).toEqual('blue'); + expect(configColorGetter).toHaveBeenCalledWith('abc'); + }); + it('should return a color from the legacy palette based on position of first series', () => { const result = palette.getColor( [ @@ -363,6 +384,30 @@ describe('palettes', () => { expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc'); }); + it('should respect the advanced settings color mapping', () => { + const configColorGetter = colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock; + configColorGetter.mockImplementation(() => 'blue'); + const result = palette.getColor( + [ + { + name: 'abc', + rankAtDepth: 2, + totalSeriesAtDepth: 10, + }, + { + name: 'def', + rankAtDepth: 0, + totalSeriesAtDepth: 10, + }, + ], + { + syncColors: false, + } + ); + expect(result).toEqual('blue'); + expect(configColorGetter).toHaveBeenCalledWith('abc'); + }); + it('should always use root series', () => { palette.getColor( [ diff --git a/src/plugins/charts/public/services/palettes/palettes.tsx b/src/plugins/charts/public/services/palettes/palettes.tsx index 8a1dee72139ed..b11d598c1c1cb 100644 --- a/src/plugins/charts/public/services/palettes/palettes.tsx +++ b/src/plugins/charts/public/services/palettes/palettes.tsx @@ -9,7 +9,6 @@ // @ts-ignore import chroma from 'chroma-js'; import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient } from 'src/core/public'; import { euiPaletteColorBlind, euiPaletteCool, @@ -130,7 +129,8 @@ function buildSyncedKibanaPalette( colors.mappedColors.mapKeys([series[0].name]); outputColor = colors.mappedColors.get(series[0].name); } else { - outputColor = staticColors[series[0].rankAtDepth % staticColors.length]; + const configColor = colors.mappedColors.getColorFromConfig(series[0].name); + outputColor = configColor || staticColors[series[0].rankAtDepth % staticColors.length]; } if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) { @@ -199,9 +199,8 @@ function buildCustomPalette(): PaletteDefinition { } export const buildPalettes: ( - uiSettings: IUiSettingsClient, legacyColorsService: LegacyColorsService -) => Record = (uiSettings, legacyColorsService) => { +) => Record = (legacyColorsService) => { return { default: { title: i18n.translate('charts.palettes.defaultPaletteLabel', { diff --git a/src/plugins/charts/public/services/palettes/service.ts b/src/plugins/charts/public/services/palettes/service.ts index 6090bfc0fd140..bb9000e896742 100644 --- a/src/plugins/charts/public/services/palettes/service.ts +++ b/src/plugins/charts/public/services/palettes/service.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { CoreSetup } from 'kibana/public'; import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; import { ChartsPluginSetup, @@ -24,12 +23,12 @@ export class PaletteService { private palettes: Record> | undefined = undefined; constructor() {} - public setup(core: CoreSetup, colorsService: LegacyColorsService) { + public setup(colorsService: LegacyColorsService) { return { getPalettes: async (): Promise => { if (!this.palettes) { const { buildPalettes } = await import('./palettes'); - this.palettes = buildPalettes(core.uiSettings, colorsService); + this.palettes = buildPalettes(colorsService); } return { get: (name: string) => { diff --git a/src/plugins/charts/server/plugin.ts b/src/plugins/charts/server/plugin.ts index 39a93962832f3..63b703e6b7538 100644 --- a/src/plugins/charts/server/plugin.ts +++ b/src/plugins/charts/server/plugin.ts @@ -31,7 +31,7 @@ export class ChartsServerPlugin implements Plugin { type: 'json', description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', { defaultMessage: - 'Maps values to specific colors in Visualize charts and TSVB. This setting does not apply to Lens.', + 'Maps values to specific colors in charts using the Compatibility palette.', }), deprecation: { message: i18n.translate( From 3f3cc8ee359d03675176cb0df29f6c9eea00032a Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 24 Mar 2021 09:54:08 +0100 Subject: [PATCH 14/88] Expose session invalidation API. (#92376) --- docs/api/session-management.asciidoc | 9 + .../session-management/invalidate.asciidoc | 114 ++++++ docs/user/api.asciidoc | 1 + .../security/authentication/index.asciidoc | 8 + .../user/security/session-management.asciidoc | 2 + .../authentication/authenticator.test.ts | 50 +-- .../server/authentication/authenticator.ts | 22 +- .../server/routes/session_management/index.ts | 2 + .../session_management/invalidate.test.ts | 155 ++++++++ .../routes/session_management/invalidate.ts | 49 +++ .../server/session_management/session.mock.ts | 2 +- .../server/session_management/session.test.ts | 115 ++++-- .../server/session_management/session.ts | 93 +++-- .../session_management/session_index.mock.ts | 2 +- .../session_management/session_index.test.ts | 140 ++++++- .../session_management/session_index.ts | 70 +++- x-pack/scripts/functional_tests.js | 1 + .../session_invalidate.config.ts | 55 +++ .../tests/session_invalidate/index.ts | 16 + .../tests/session_invalidate/invalidate.ts | 350 ++++++++++++++++++ 20 files changed, 1163 insertions(+), 93 deletions(-) create mode 100644 docs/api/session-management.asciidoc create mode 100644 docs/api/session-management/invalidate.asciidoc create mode 100644 x-pack/plugins/security/server/routes/session_management/invalidate.test.ts create mode 100644 x-pack/plugins/security/server/routes/session_management/invalidate.ts create mode 100644 x-pack/test/security_api_integration/session_invalidate.config.ts create mode 100644 x-pack/test/security_api_integration/tests/session_invalidate/index.ts create mode 100644 x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts diff --git a/docs/api/session-management.asciidoc b/docs/api/session-management.asciidoc new file mode 100644 index 0000000000000..7ac8b9dddcdbb --- /dev/null +++ b/docs/api/session-management.asciidoc @@ -0,0 +1,9 @@ +[role="xpack"] +[[session-management-api]] +== User session management APIs + +The following <> management APIs are available: + +* <> to invalidate user sessions + +include::session-management/invalidate.asciidoc[] diff --git a/docs/api/session-management/invalidate.asciidoc b/docs/api/session-management/invalidate.asciidoc new file mode 100644 index 0000000000000..c3dced17e72b7 --- /dev/null +++ b/docs/api/session-management/invalidate.asciidoc @@ -0,0 +1,114 @@ +[[session-management-api-invalidate]] +=== Invalidate user sessions API +++++ +Invalidate user sessions +++++ + +experimental[] Invalidates user sessions that match provided query. + +[[session-management-api-invalidate-prereqs]] +==== Prerequisite + +To use the invalidate user sessions API, you must be a `superuser`. + +[[session-management-api-invalidate-request]] +==== Request + +`POST :/api/security/session/_invalidate` + +[role="child_attributes"] +[[session-management-api-invalidate-request-body]] +==== Request body + +`match`:: +(Required, string) Specifies how {kib} determines which sessions to invalidate. Can either be `all` to invalidate all existing sessions, or `query` to only invalidate sessions that match the query specified in the additional `query` parameter. + +`query`:: +(Optional, object) Specifies the query that {kib} uses to match the sessions to invalidate when the `match` parameter is set to `query`. You cannot use this parameter if `match` is set to `all`. ++ +.Properties of `query` +[%collapsible%open] +===== +`provider` ::: +(Required, object) Describes the <> for which to invalidate sessions. + +`type` :::: +(Required, string) The authentication provider `type`. + +`name` :::: +(Optional, string) The authentication provider `name`. + +`username` ::: +(Optional, string) The username for which to invalidate sessions. +===== + +[[session-management-api-invalidate-response-body]] +==== Response body + +`total`:: +(number) The number of successfully invalidated sessions. + +[[session-management-api-invalidate-response-codes]] +==== Response codes + +`200`:: + Indicates a successful call. + +`403`:: + Indicates that the user may not be authorized to invalidate sessions for other users. Refer to <>. + +==== Examples + +Invalidate all existing sessions: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/security/session/_invalidate +{ + "match" : "all" +} +-------------------------------------------------- +// KIBANA + +Invalidate sessions that were created by any <>: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/security/session/_invalidate +{ + "match" : "query", + "query": { + "provider" : { "type": "saml" } + } +} +-------------------------------------------------- +// KIBANA + +Invalidate sessions that were created by the <> with the name `saml1`: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/security/session/_invalidate +{ + "match" : "query", + "query": { + "provider" : { "type": "saml", "name": "saml1" } + } +} +-------------------------------------------------- +// KIBANA + +Invalidate sessions that were created by any <> for the user with the username `user@my-oidc-sso.com`: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/security/session/_invalidate +{ + "match" : "query", + "query": { + "provider" : { "type": "oidc" }, + "username": "user@my-oidc-sso.com" + } +} +-------------------------------------------------- +// KIBANA diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 459dbbdd34b27..c41f3d8a829e4 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -97,6 +97,7 @@ curl -X POST \ include::{kib-repo-dir}/api/features.asciidoc[] 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/actions-and-connectors.asciidoc[] diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index b2d85de91b9fc..a4acc93310e5d 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -397,6 +397,14 @@ NOTE: *Public URL* is available only when anonymous access is configured and you + For more information, refer to <>. +[float] +[[anonymous-access-session]] +===== Anonymous access session + +{kib} maintains a separate <> for every anonymous user, as it does for all other authentication mechanisms. + +You can configure <> and <> for anonymous sessions the same as you do for any other session with the exception that idle timeout is explicitly disabled for anonymous sessions by default. The global <> setting doesn't affect anonymous sessions. To change the idle timeout for anonymous sessions, you must configure the provider-level <.session.idleTimeout`>> setting. + [[http-authentication]] ==== HTTP authentication diff --git a/docs/user/security/session-management.asciidoc b/docs/user/security/session-management.asciidoc index 0df5b3b31a203..ac7a777eb0580 100644 --- a/docs/user/security/session-management.asciidoc +++ b/docs/user/security/session-management.asciidoc @@ -6,6 +6,8 @@ When you log in, {kib} creates a session that is used to authenticate subsequent When your session expires, or you log out, {kib} will invalidate your cookie and remove session information from the index. {kib} also periodically invalidates and removes any expired sessions that weren't explicitly invalidated. +To manage user sessions programmatically, {kib} exposes <>. + [[session-idle-timeout]] ==== Session idle timeout diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index e0b9144f6a66f..be53caffc066d 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -585,8 +585,8 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalledWith(request); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); }); it('clears session if provider asked to do so in `succeeded` result.', async () => { @@ -605,8 +605,8 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalledWith(request); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); }); it('clears session if provider asked to do so in `redirected` result.', async () => { @@ -624,8 +624,8 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalledWith(request); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); }); describe('with Access Agreement', () => { @@ -1191,7 +1191,7 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('extends session for non-system API calls.', async () => { @@ -1213,7 +1213,7 @@ describe('Authenticator', () => { expect(mockOptions.session.extend).toHaveBeenCalledWith(request, mockSessVal); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { @@ -1234,7 +1234,7 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => { @@ -1255,7 +1255,7 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('replaces existing session with the one returned by authentication provider for system API requests', async () => { @@ -1281,7 +1281,7 @@ describe('Authenticator', () => { }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => { @@ -1307,7 +1307,7 @@ describe('Authenticator', () => { }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => { @@ -1324,8 +1324,8 @@ describe('Authenticator', () => { AuthenticationResult.failed(Boom.unauthorized()) ); - expect(mockOptions.session.clear).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalledWith(request); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); @@ -1345,8 +1345,8 @@ describe('Authenticator', () => { AuthenticationResult.failed(Boom.unauthorized()) ); - expect(mockOptions.session.clear).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalledWith(request); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); @@ -1364,8 +1364,8 @@ describe('Authenticator', () => { AuthenticationResult.redirectTo('some-url', { state: null }) ); - expect(mockOptions.session.clear).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalledWith(request); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); @@ -1382,7 +1382,7 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); @@ -1399,7 +1399,7 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); @@ -1789,7 +1789,7 @@ describe('Authenticator', () => { DeauthenticationResult.notHandled() ); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('clears session and returns whatever authentication provider returns.', async () => { @@ -1804,7 +1804,7 @@ describe('Authenticator', () => { ); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); - expect(mockOptions.session.clear).toHaveBeenCalled(); + expect(mockOptions.session.invalidate).toHaveBeenCalled(); }); it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { @@ -1823,7 +1823,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null); - expect(mockOptions.session.clear).toHaveBeenCalled(); + expect(mockOptions.session.invalidate).toHaveBeenCalled(); }); it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => { @@ -1840,7 +1840,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); }); it('returns `notHandled` if session does not exist and provider name is invalid', async () => { @@ -1852,7 +1852,7 @@ describe('Authenticator', () => { ); expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).toHaveBeenCalled(); + expect(mockOptions.session.invalidate).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index ff6f3ff0c2ae7..f86ff54963da9 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -396,7 +396,7 @@ export class Authenticator { sessionValue?.provider.name ?? request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER); if (suggestedProviderName) { - await this.session.clear(request); + await this.invalidateSessionValue(request); // Provider name may be passed in a query param and sourced from the browser's local storage; // hence, we can't assume that this provider exists, so we have to check it. @@ -522,7 +522,7 @@ export class Authenticator { this.logger.warn( `Attempted to retrieve session for the "${existingSessionValue.provider.type}/${existingSessionValue.provider.name}" provider, but it is not configured.` ); - await this.session.clear(request); + await this.invalidateSessionValue(request); return null; } @@ -556,7 +556,7 @@ export class Authenticator { // attempt didn't fail. if (authenticationResult.shouldClearState()) { this.logger.debug('Authentication provider requested to invalidate existing session.'); - await this.session.clear(request); + await this.invalidateSessionValue(request); return null; } @@ -570,7 +570,7 @@ export class Authenticator { if (authenticationResult.failed()) { if (ownsSession && getErrorStatusCode(authenticationResult.error) === 401) { this.logger.debug('Authentication attempt failed, existing session will be invalidated.'); - await this.session.clear(request); + await this.invalidateSessionValue(request); } return null; } @@ -608,17 +608,17 @@ export class Authenticator { this.logger.debug( 'Authentication provider has changed, existing session will be invalidated.' ); - await this.session.clear(request); + await this.invalidateSessionValue(request); existingSessionValue = null; } else if (sessionHasBeenAuthenticated) { this.logger.debug( 'Session is authenticated, existing unauthenticated session will be invalidated.' ); - await this.session.clear(request); + await this.invalidateSessionValue(request); existingSessionValue = null; } else if (usernameHasChanged) { this.logger.debug('Username has changed, existing session will be invalidated.'); - await this.session.clear(request); + await this.invalidateSessionValue(request); existingSessionValue = null; } @@ -651,6 +651,14 @@ export class Authenticator { }; } + /** + * Invalidates session value associated with the specified request. + * @param request Request instance. + */ + private async invalidateSessionValue(request: KibanaRequest) { + await this.session.invalidate(request, { match: 'current' }); + } + /** * Checks whether request should be redirected to the Login Selector UI. * @param request Request instance. diff --git a/x-pack/plugins/security/server/routes/session_management/index.ts b/x-pack/plugins/security/server/routes/session_management/index.ts index 1348179386ce0..16cda7b7b409d 100644 --- a/x-pack/plugins/security/server/routes/session_management/index.ts +++ b/x-pack/plugins/security/server/routes/session_management/index.ts @@ -8,8 +8,10 @@ import type { RouteDefinitionParams } from '../'; import { defineSessionExtendRoutes } from './extend'; import { defineSessionInfoRoutes } from './info'; +import { defineInvalidateSessionsRoutes } from './invalidate'; export function defineSessionManagementRoutes(params: RouteDefinitionParams) { defineSessionInfoRoutes(params); defineSessionExtendRoutes(params); + defineInvalidateSessionsRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts b/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts new file mode 100644 index 0000000000000..215a66a3364e5 --- /dev/null +++ b/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ObjectType } from '@kbn/config-schema'; +import type { PublicMethodsOf } from '@kbn/utility-types'; + +import type { RequestHandler, RouteConfig } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import type { Session } from '../../session_management'; +import { sessionMock } from '../../session_management/session.mock'; +import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { defineInvalidateSessionsRoutes } from './invalidate'; + +describe('Invalidate sessions routes', () => { + let router: jest.Mocked; + let session: jest.Mocked>; + beforeEach(() => { + const routeParamsMock = routeDefinitionParamsMock.create(); + router = routeParamsMock.router; + + session = sessionMock.create(); + routeParamsMock.getSession.mockReturnValue(session); + + defineInvalidateSessionsRoutes(routeParamsMock); + }); + + describe('invalidate sessions', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + beforeEach(() => { + const [extendRouteConfig, extendRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/api/security/session/_invalidate' + )!; + + routeConfig = extendRouteConfig; + routeHandler = extendRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toEqual({ tags: ['access:sessionManagement'] }); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[match]: expected at least one defined value but got [undefined]"` + ); + expect(() => bodySchema.validate({ match: 'current' })).toThrowErrorMatchingInlineSnapshot(` + "[match]: types that failed validation: + - [match.0]: expected value to equal [all] + - [match.1]: expected value to equal [query]" + `); + expect(() => + bodySchema.validate({ match: 'all', query: { provider: { type: 'basic' } } }) + ).toThrowErrorMatchingInlineSnapshot(`"[query]: a value wasn't expected to be present"`); + expect(() => bodySchema.validate({ match: 'query' })).toThrowErrorMatchingInlineSnapshot( + `"[query.provider.type]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodySchema.validate({ match: 'query', query: { username: 'user' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[query.provider.type]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodySchema.validate({ + match: 'query', + query: { provider: { name: 'basic1' }, username: 'user' }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[query.provider.type]: expected value of type [string] but got [undefined]"` + ); + + expect(bodySchema.validate({ match: 'all' })).toEqual({ match: 'all' }); + expect( + bodySchema.validate({ match: 'query', query: { provider: { type: 'basic' } } }) + ).toEqual({ + match: 'query', + query: { provider: { type: 'basic' } }, + }); + expect( + bodySchema.validate({ + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' } }, + }) + ).toEqual({ match: 'query', query: { provider: { type: 'basic', name: 'basic1' } } }); + expect( + bodySchema.validate({ + match: 'query', + query: { provider: { type: 'basic' }, username: 'user' }, + }) + ).toEqual({ match: 'query', query: { provider: { type: 'basic' }, username: 'user' } }); + expect( + bodySchema.validate({ + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' }, username: 'user' }, + }) + ).toEqual({ + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' }, username: 'user' }, + }); + }); + + it('properly constructs `query` match filter.', async () => { + session.invalidate.mockResolvedValue(30); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' }, username: 'user' }, + }, + }); + await expect( + routeHandler( + ({} as unknown) as SecurityRequestHandlerContext, + mockRequest, + kibanaResponseFactory + ) + ).resolves.toEqual({ + status: 200, + options: { body: { total: 30 } }, + payload: { total: 30 }, + }); + + expect(session.invalidate).toHaveBeenCalledTimes(1); + expect(session.invalidate).toHaveBeenCalledWith(mockRequest, { + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' }, username: 'user' }, + }); + }); + + it('properly constructs `all` match filter.', async () => { + session.invalidate.mockResolvedValue(30); + + const mockRequest = httpServerMock.createKibanaRequest({ body: { match: 'all' } }); + await expect( + routeHandler( + ({} as unknown) as SecurityRequestHandlerContext, + mockRequest, + kibanaResponseFactory + ) + ).resolves.toEqual({ + status: 200, + options: { body: { total: 30 } }, + payload: { total: 30 }, + }); + + expect(session.invalidate).toHaveBeenCalledTimes(1); + expect(session.invalidate).toHaveBeenCalledWith(mockRequest, { match: 'all' }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/session_management/invalidate.ts b/x-pack/plugins/security/server/routes/session_management/invalidate.ts new file mode 100644 index 0000000000000..3416be3dd2965 --- /dev/null +++ b/x-pack/plugins/security/server/routes/session_management/invalidate.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import type { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for session invalidation. + */ +export function defineInvalidateSessionsRoutes({ router, getSession }: RouteDefinitionParams) { + router.post( + { + path: '/api/security/session/_invalidate', + validate: { + body: schema.object({ + match: schema.oneOf([schema.literal('all'), schema.literal('query')]), + query: schema.conditional( + schema.siblingRef('match'), + schema.literal('query'), + schema.object({ + provider: schema.object({ + type: schema.string(), + name: schema.maybe(schema.string()), + }), + username: schema.maybe(schema.string()), + }), + schema.never() + ), + }), + }, + options: { tags: ['access:sessionManagement'] }, + }, + async (_context, request, response) => { + return response.ok({ + body: { + total: await getSession().invalidate(request, { + match: request.body.match, + query: request.body.query, + }), + }, + }); + } + ); +} diff --git a/x-pack/plugins/security/server/session_management/session.mock.ts b/x-pack/plugins/security/server/session_management/session.mock.ts index dfe1293f57e92..65ae43e5fa705 100644 --- a/x-pack/plugins/security/server/session_management/session.mock.ts +++ b/x-pack/plugins/security/server/session_management/session.mock.ts @@ -18,7 +18,7 @@ export const sessionMock = { create: jest.fn(), update: jest.fn(), extend: jest.fn(), - clear: jest.fn(), + invalidate: jest.fn(), }), createValue: (sessionValue: Partial = {}): SessionValue => ({ diff --git a/x-pack/plugins/security/server/session_management/session.test.ts b/x-pack/plugins/security/server/session_management/session.test.ts index bd94c9483d561..dfe6ba343ca3c 100644 --- a/x-pack/plugins/security/server/session_management/session.test.ts +++ b/x-pack/plugins/security/server/session_management/session.test.ts @@ -103,7 +103,7 @@ describe('Session', () => { await expect(session.get(httpServerMock.createKibanaRequest())).resolves.toBeNull(); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); - expect(mockSessionIndex.clear).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); }); it('clears session value if session is expired because of lifespan', async () => { @@ -122,7 +122,7 @@ describe('Session', () => { await expect(session.get(httpServerMock.createKibanaRequest())).resolves.toBeNull(); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); - expect(mockSessionIndex.clear).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); }); it('clears session value if session cookie does not have corresponding session index value', async () => { @@ -151,7 +151,7 @@ describe('Session', () => { await expect(session.get(httpServerMock.createKibanaRequest())).resolves.toBeNull(); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); - expect(mockSessionIndex.clear).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); }); it('clears session value if session index value content cannot be decrypted because of wrong AAD', async () => { @@ -170,7 +170,7 @@ describe('Session', () => { await expect(session.get(httpServerMock.createKibanaRequest())).resolves.toBeNull(); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); - expect(mockSessionIndex.clear).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); }); it('returns session value with decrypted content', async () => { @@ -199,7 +199,7 @@ describe('Session', () => { username: 'some-user', }); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); }); }); @@ -279,7 +279,7 @@ describe('Session', () => { const mockRequest = httpServerMock.createKibanaRequest(); await expect(session.update(mockRequest, sessionMock.createValue())).resolves.toBeNull(); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); expect(mockSessionCookie.clear).toHaveBeenCalledWith(mockRequest); }); @@ -432,7 +432,7 @@ describe('Session', () => { }) ); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); } @@ -485,7 +485,7 @@ describe('Session', () => { ); expect(mockSessionIndex.update).not.toHaveBeenCalled(); expect(mockSessionCookie.set).not.toHaveBeenCalled(); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); }); @@ -563,7 +563,7 @@ describe('Session', () => { mockRequest, expect.objectContaining({ idleTimeoutExpiration: expectedNewExpiration }) ); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); }); @@ -582,7 +582,7 @@ describe('Session', () => { ) ).resolves.toBeNull(); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); expect(mockSessionCookie.clear).toHaveBeenCalledWith(mockRequest); }); @@ -625,7 +625,7 @@ describe('Session', () => { mockRequest, expect.objectContaining({ idleTimeoutExpiration: expectedNewExpiration }) ); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); }); @@ -653,7 +653,7 @@ describe('Session', () => { mockRequest, expect.objectContaining({ idleTimeoutExpiration: expectedNewExpiration }) ); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); }); @@ -696,7 +696,7 @@ describe('Session', () => { mockRequest, expect.objectContaining({ idleTimeoutExpiration: expectedNewExpiration }) ); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); }); }); @@ -764,7 +764,7 @@ describe('Session', () => { ); } - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); } @@ -786,27 +786,98 @@ describe('Session', () => { }); }); - describe('#clear', () => { - it('does not clear anything if session does not exist', async () => { + describe('#invalidate', () => { + beforeEach(() => { + mockSessionCookie.get.mockResolvedValue(sessionCookieMock.createValue()); + mockSessionIndex.invalidate.mockResolvedValue(10); + }); + + it('[match=current] does not clear anything if session does not exist', async () => { mockSessionCookie.get.mockResolvedValue(null); - await session.clear(httpServerMock.createKibanaRequest()); + await session.invalidate(httpServerMock.createKibanaRequest(), { match: 'current' }); - expect(mockSessionIndex.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).not.toHaveBeenCalled(); expect(mockSessionCookie.clear).not.toHaveBeenCalled(); }); - it('clears both session cookie and session index', async () => { + it('[match=current] clears both session cookie and session index', async () => { mockSessionCookie.get.mockResolvedValue(sessionCookieMock.createValue()); const mockRequest = httpServerMock.createKibanaRequest(); - await session.clear(mockRequest); + await session.invalidate(mockRequest, { match: 'current' }); - expect(mockSessionIndex.clear).toHaveBeenCalledTimes(1); - expect(mockSessionIndex.clear).toHaveBeenCalledWith('some-long-sid'); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledWith({ + match: 'sid', + sid: 'some-long-sid', + }); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); expect(mockSessionCookie.clear).toHaveBeenCalledWith(mockRequest); }); + + it('[match=all] clears all sessions even if current initiator request does not have a session', async () => { + mockSessionCookie.get.mockResolvedValue(null); + + await expect( + session.invalidate(httpServerMock.createKibanaRequest(), { match: 'all' }) + ).resolves.toBe(10); + + expect(mockSessionCookie.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledWith({ match: 'all' }); + }); + + it('[match=query] properly forwards filter with the provider type to the session index', async () => { + await expect( + session.invalidate(httpServerMock.createKibanaRequest(), { + match: 'query', + query: { provider: { type: 'basic' } }, + }) + ).resolves.toBe(10); + + expect(mockSessionCookie.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledWith({ + match: 'query', + query: { provider: { type: 'basic' } }, + }); + }); + + it('[match=query] properly forwards filter with the provider type and provider name to the session index', async () => { + await expect( + session.invalidate(httpServerMock.createKibanaRequest(), { + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' } }, + }) + ).resolves.toBe(10); + + expect(mockSessionCookie.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledWith({ + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' } }, + }); + }); + + it('[match=query] properly forwards filter with the provider type, provider name, and username hash to the session index', async () => { + await expect( + session.invalidate(httpServerMock.createKibanaRequest(), { + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' }, username: 'elastic' }, + }) + ).resolves.toBe(10); + + expect(mockSessionCookie.clear).not.toHaveBeenCalled(); + expect(mockSessionIndex.invalidate).toHaveBeenCalledTimes(1); + expect(mockSessionIndex.invalidate).toHaveBeenCalledWith({ + match: 'query', + query: { + provider: { type: 'basic', name: 'basic1' }, + usernameHash: 'eb28536c8ead72bf81a0a9226e38fc9bad81f5e07c2081bb801b2a5c8842924e', + }, + }); + }); }); }); diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 7fada4d1730cd..8d2b56b4d2b7a 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -79,6 +79,18 @@ export interface SessionValueContentToEncrypt { state: unknown; } +/** + * Filter provided for the `Session.invalidate` method that determines which session values should + * be invalidated. It can have three possible types: + * - `all` means that all existing active and inactive sessions should be invalidated. + * - `current` means that session associated with the current request should be invalidated. + * - `query` means that only sessions that match specified query should be invalidated. + */ +export type InvalidateSessionsFilter = + | { match: 'all' } + | { match: 'current' } + | { match: 'query'; query: { provider: { type: string; name?: string }; username?: string } }; + /** * The SIDs and AAD must be unpredictable to prevent guessing attacks, where an attacker is able to * guess or predict the ID of a valid session through statistical analysis techniques. That's why we @@ -133,7 +145,7 @@ export class Session { (sessionCookieValue.lifespanExpiration && sessionCookieValue.lifespanExpiration < now) ) { sessionLogger.debug('Session has expired and will be invalidated.'); - await this.clear(request); + await this.invalidate(request, { match: 'current' }); return null; } @@ -155,7 +167,7 @@ export class Session { sessionLogger.warn( `Unable to decrypt session content, session will be invalidated: ${err.message}` ); - await this.clear(request); + await this.invalidate(request, { match: 'current' }); return null; } @@ -194,7 +206,7 @@ export class Session { ...publicSessionValue, ...sessionExpirationInfo, sid, - usernameHash: username && createHash('sha3-256').update(username).digest('hex'), + usernameHash: username && Session.getUsernameHash(username), content: await this.crypto.encrypt(JSON.stringify({ username, state }), aad), }); @@ -230,7 +242,7 @@ export class Session { ...sessionValue.metadata.index, ...publicSessionInfo, ...sessionExpirationInfo, - usernameHash: username && createHash('sha3-256').update(username).digest('hex'), + usernameHash: username && Session.getUsernameHash(username), content: await this.crypto.encrypt( JSON.stringify({ username, state }), sessionCookieValue.aad @@ -358,24 +370,53 @@ export class Session { } /** - * Clears session value for the specified request. - * @param request Request instance to clear session value for. + * Invalidates sessions that match the specified filter. + * @param request Request instance initiated invalidation. + * @param filter Filter that narrows down the list of the sessions that should be invalidated. */ - async clear(request: KibanaRequest) { + async invalidate(request: KibanaRequest, filter: InvalidateSessionsFilter) { + // We don't require request to have the associated session, but nevertheless we still want to + // log the SID if session is available. const sessionCookieValue = await this.options.sessionCookie.get(request); - if (!sessionCookieValue) { - return; - } - - const sessionLogger = this.getLoggerForSID(sessionCookieValue.sid); - sessionLogger.debug('Invalidating session.'); + const sessionLogger = this.getLoggerForSID(sessionCookieValue?.sid); + + // We clear session cookie only when the current session should be invalidated since it's the + // only case when this action is explicitly and unequivocally requested. This behavior doesn't + // introduce any risk since even if the current session has been affected the session cookie + // will be automatically invalidated as soon as client attempts to re-use it due to missing + // underlying session index value. + let invalidateIndexValueFilter; + if (filter.match === 'current') { + if (!sessionCookieValue) { + return; + } - await Promise.all([ - this.options.sessionCookie.clear(request), - this.options.sessionIndex.clear(sessionCookieValue.sid), - ]); + sessionLogger.debug('Invalidating current session.'); + await this.options.sessionCookie.clear(request); + invalidateIndexValueFilter = { match: 'sid' as 'sid', sid: sessionCookieValue.sid }; + } else if (filter.match === 'all') { + sessionLogger.debug('Invalidating all sessions.'); + invalidateIndexValueFilter = filter; + } else { + sessionLogger.debug( + `Invalidating sessions that match query: ${JSON.stringify( + filter.query.username ? { ...filter.query, username: '[REDACTED]' } : filter.query + )}.` + ); + invalidateIndexValueFilter = filter.query.username + ? { + ...filter, + query: { + provider: filter.query.provider, + usernameHash: Session.getUsernameHash(filter.query.username), + }, + } + : filter; + } - sessionLogger.debug('Successfully invalidated session.'); + const invalidatedCount = await this.options.sessionIndex.invalidate(invalidateIndexValueFilter); + sessionLogger.debug(`Successfully invalidated ${invalidatedCount} session(s).`); + return invalidatedCount; } private calculateExpiry( @@ -414,9 +455,19 @@ export class Session { /** * Creates logger scoped to a specified session ID. - * @param sid Session ID to create logger for. + * @param [sid] Session ID to create logger for. + */ + private getLoggerForSID(sid?: string) { + return this.options.logger.get(sid?.slice(-10) ?? 'no_session'); + } + + /** + * Generates a sha3-256 hash for the specified `username`. The hash is intended to be stored in + * the session index to allow querying user specific sessions and don't expose the original + * `username` at the same time. + * @param username Username string to generate hash for. */ - private getLoggerForSID(sid: string) { - return this.options.logger.get(sid?.slice(-10)); + private static getUsernameHash(username: string) { + return createHash('sha3-256').update(username).digest('hex'); } } diff --git a/x-pack/plugins/security/server/session_management/session_index.mock.ts b/x-pack/plugins/security/server/session_management/session_index.mock.ts index 56049a3ae9205..9498b60d916a2 100644 --- a/x-pack/plugins/security/server/session_management/session_index.mock.ts +++ b/x-pack/plugins/security/server/session_management/session_index.mock.ts @@ -14,7 +14,7 @@ export const sessionIndexMock = { get: jest.fn(), create: jest.fn(), update: jest.fn(), - clear: jest.fn(), + invalidate: jest.fn(), initialize: jest.fn(), cleanUp: jest.fn(), }), diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index 2b3ec0adeb51e..b5b4f64438902 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -162,7 +162,7 @@ describe('Session index', () => { }); }); - describe('cleanUp', () => { + describe('#cleanUp', () => { const now = 123456; beforeEach(() => { mockElasticsearchClient.deleteByQuery.mockResolvedValue( @@ -797,18 +797,26 @@ describe('Session index', () => { }); }); - describe('#clear', () => { - it('throws if call to Elasticsearch fails', async () => { + describe('#invalidate', () => { + beforeEach(() => { + mockElasticsearchClient.deleteByQuery.mockResolvedValue( + securityMock.createApiResponse({ body: { deleted: 10 } }) + ); + }); + + it('[match=sid] throws if call to Elasticsearch fails', async () => { const failureReason = new errors.ResponseError( securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) ); mockElasticsearchClient.delete.mockRejectedValue(failureReason); - await expect(sessionIndex.clear('some-long-sid')).rejects.toBe(failureReason); + await expect(sessionIndex.invalidate({ match: 'sid', sid: 'some-long-sid' })).rejects.toBe( + failureReason + ); }); - it('properly removes session value from the index', async () => { - await sessionIndex.clear('some-long-sid'); + it('[match=sid] properly removes session value from the index', async () => { + await sessionIndex.invalidate({ match: 'sid', sid: 'some-long-sid' }); expect(mockElasticsearchClient.delete).toHaveBeenCalledTimes(1); expect(mockElasticsearchClient.delete).toHaveBeenCalledWith( @@ -816,5 +824,125 @@ describe('Session index', () => { { ignore: [404] } ); }); + + it('[match=all] throws if call to Elasticsearch fails', async () => { + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.deleteByQuery.mockRejectedValue(failureReason); + + await expect(sessionIndex.invalidate({ match: 'all' })).rejects.toBe(failureReason); + }); + + it('[match=all] properly constructs query', async () => { + await expect(sessionIndex.invalidate({ match: 'all' })).resolves.toBe(10); + + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith({ + index: indexName, + refresh: true, + body: { query: { match_all: {} } }, + }); + }); + + it('[match=query] throws if call to Elasticsearch fails', async () => { + const failureReason = new errors.ResponseError( + securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } })) + ); + mockElasticsearchClient.deleteByQuery.mockRejectedValue(failureReason); + + await expect( + sessionIndex.invalidate({ match: 'query', query: { provider: { type: 'basic' } } }) + ).rejects.toBe(failureReason); + }); + + it('[match=query] when only provider type is specified', async () => { + await expect( + sessionIndex.invalidate({ match: 'query', query: { provider: { type: 'basic' } } }) + ).resolves.toBe(10); + + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith({ + index: indexName, + refresh: true, + body: { query: { bool: { must: [{ term: { 'provider.type': 'basic' } }] } } }, + }); + }); + + it('[match=query] when both provider type and provider name are specified', async () => { + await expect( + sessionIndex.invalidate({ + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' } }, + }) + ).resolves.toBe(10); + + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith({ + index: indexName, + refresh: true, + body: { + query: { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic1' } }, + ], + }, + }, + }, + }); + }); + + it('[match=query] when both provider type and username hash are specified', async () => { + await expect( + sessionIndex.invalidate({ + match: 'query', + query: { provider: { type: 'basic' }, usernameHash: 'some-hash' }, + }) + ).resolves.toBe(10); + + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith({ + index: indexName, + refresh: true, + body: { + query: { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { usernameHash: 'some-hash' } }, + ], + }, + }, + }, + }); + }); + + it('[match=query] when provider type, provider name, and username hash are specified', async () => { + await expect( + sessionIndex.invalidate({ + match: 'query', + query: { provider: { type: 'basic', name: 'basic1' }, usernameHash: 'some-hash' }, + }) + ).resolves.toBe(10); + + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1); + expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith({ + index: indexName, + refresh: true, + body: { + query: { + bool: { + must: [ + { term: { 'provider.type': 'basic' } }, + { term: { 'provider.name': 'basic1' } }, + { term: { usernameHash: 'some-hash' } }, + ], + }, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 828c8fa11acdd..1b5c820ec4710 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -17,6 +17,18 @@ export interface SessionIndexOptions { readonly logger: Logger; } +/** + * Filter provided for the `SessionIndex.invalidate` method that determines which session index + * values should be invalidated (removed from the index). It can have three possible types: + * - `all` means that all existing active and inactive sessions should be invalidated. + * - `sid` means that only session with the specified SID should be invalidated. + * - `query` means that only sessions that match specified query should be invalidated. + */ +export type InvalidateSessionsFilter = + | { match: 'all' } + | { match: 'sid'; sid: string } + | { match: 'query'; query: { provider: { type: string; name?: string }; usernameHash?: string } }; + /** * Version of the current session index template. */ @@ -237,19 +249,57 @@ export class SessionIndex { } /** - * Clears session value with the specified ID. - * @param sid Session ID to clear. + * Clears session value(s) determined by the specified filter. + * @param filter Filter that narrows down the list of the session values that should be cleared. */ - async clear(sid: string) { + async invalidate(filter: InvalidateSessionsFilter) { + if (filter.match === 'sid') { + try { + // We don't specify primary term and sequence number as delete should always take precedence + // over any updates that could happen in the meantime. + const { statusCode } = await this.options.elasticsearchClient.delete( + { id: filter.sid, index: this.indexName, refresh: 'wait_for' }, + { ignore: [404] } + ); + + // 404 means the session with such SID wasn't found and hence nothing was removed. + return statusCode !== 404 ? 1 : 0; + } catch (err) { + this.options.logger.error(`Failed to clear session value: ${err.message}`); + throw err; + } + } + + // If filter is specified we should clear only session values that are matched by the filter. + // Otherwise all session values should be cleared. + let deleteQuery; + if (filter.match === 'query') { + deleteQuery = { + bool: { + must: [ + { term: { 'provider.type': filter.query.provider.type } }, + ...(filter.query.provider.name + ? [{ term: { 'provider.name': filter.query.provider.name } }] + : []), + ...(filter.query.usernameHash + ? [{ term: { usernameHash: filter.query.usernameHash } }] + : []), + ], + }, + }; + } else { + deleteQuery = { match_all: {} }; + } + try { - // We don't specify primary term and sequence number as delete should always take precedence - // over any updates that could happen in the meantime. - await this.options.elasticsearchClient.delete( - { id: sid, index: this.indexName, refresh: 'wait_for' }, - { ignore: [404] } - ); + const { body: response } = await this.options.elasticsearchClient.deleteByQuery({ + index: this.indexName, + refresh: true, + body: { query: deleteQuery }, + }); + return response.deleted as number; } catch (err) { - this.options.logger.error(`Failed to clear session value: ${err.message}`); + this.options.logger.error(`Failed to clear session value(s): ${err.message}`); throw err; } } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 132915922fcea..88c4410fde941 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -40,6 +40,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/plugin_api_integration/config.ts'), require.resolve('../test/security_api_integration/saml.config.ts'), require.resolve('../test/security_api_integration/session_idle.config.ts'), + require.resolve('../test/security_api_integration/session_invalidate.config.ts'), require.resolve('../test/security_api_integration/session_lifespan.config.ts'), require.resolve('../test/security_api_integration/login_selector.config.ts'), require.resolve('../test/security_api_integration/audit.config.ts'), diff --git a/x-pack/test/security_api_integration/session_invalidate.config.ts b/x-pack/test/security_api_integration/session_invalidate.config.ts new file mode 100644 index 0000000000000..82510062035a9 --- /dev/null +++ b/x-pack/test/security_api_integration/session_invalidate.config.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 { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; + +// the default export of config files must be a config provider +// that returns an object with the projects config values +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port'); + const idpPath = resolve(__dirname, './fixtures/saml/idp_metadata.xml'); + + return { + testFiles: [resolve(__dirname, './tests/session_invalidate')], + services, + servers: xPackAPITestsConfig.get('servers'), + esTestCluster: { + ...xPackAPITestsConfig.get('esTestCluster'), + serverArgs: [ + ...xPackAPITestsConfig.get('esTestCluster.serverArgs'), + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.realms.native.native1.order=0', + 'xpack.security.authc.realms.saml.saml1.order=1', + `xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`, + 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1', + `xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`, + `xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`, + `xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`, + 'xpack.security.authc.realms.saml.saml1.attributes.principal=urn:oid:0.0.7', + ], + }, + + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.security.authc.providers=${JSON.stringify({ + basic: { basic1: { order: 0 } }, + saml: { saml1: { order: 1, realm: 'saml1' } }, + })}`, + ], + }, + + junit: { + reportName: 'X-Pack Security API Integration Tests (Session Invalidate)', + }, + }; +} diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts new file mode 100644 index 0000000000000..6408e4cfbd43d --- /dev/null +++ b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('security APIs - Session Invalidate', function () { + this.tags('ciGroup6'); + + loadTestFile(require.resolve('./invalidate')); + }); +} diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts new file mode 100644 index 0000000000000..60605c88ce45e --- /dev/null +++ b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts @@ -0,0 +1,350 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import request, { Cookie } from 'request'; +import expect from '@kbn/expect'; +import { adminTestUser } from '@kbn/test'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; +import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const es = getService('es'); + const security = getService('security'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const config = getService('config'); + const randomness = getService('randomness'); + const kibanaServerConfig = config.get('servers.kibana'); + const notSuperuserTestUser = { username: 'test_user', password: 'changeme' }; + + async function checkSessionCookie( + sessionCookie: Cookie, + username: string, + provider: AuthenticationProvider + ) { + const apiResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .expect(200); + + expect(apiResponse.body.username).to.be(username); + expect(apiResponse.body.authentication_provider).to.eql(provider); + + return Array.isArray(apiResponse.headers['set-cookie']) + ? request.cookie(apiResponse.headers['set-cookie'][0])! + : undefined; + } + + async function loginWithSAML() { + const handshakeResponse = await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ providerType: 'saml', providerName: 'saml1', currentURL: '' }) + .expect(200); + + const authenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', request.cookie(handshakeResponse.headers['set-cookie'][0])!.cookieString()) + .send({ + SAMLResponse: await getSAMLResponse({ + destination: `http://localhost:${kibanaServerConfig.port}/api/security/saml/callback`, + sessionIndex: String(randomness.naturalNumber()), + inResponseTo: await getSAMLRequestId(handshakeResponse.body.location), + }), + }) + .expect(302); + + const cookie = request.cookie(authenticationResponse.headers['set-cookie'][0])!; + await checkSessionCookie(cookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + return cookie; + } + + async function loginWithBasic(credentials: { username: string; password: string }) { + const authenticationResponse = await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic1', + currentURL: '/', + params: credentials, + }) + .expect(200); + + const cookie = request.cookie(authenticationResponse.headers['set-cookie'][0])!; + await checkSessionCookie(cookie, credentials.username, { type: 'basic', name: 'basic1' }); + return cookie; + } + + describe('Session Invalidate', () => { + beforeEach(async () => { + await es.cluster.health({ index: '.kibana_security_session*', wait_for_status: 'green' }); + await esDeleteAllIndices('.kibana_security_session*'); + await security.testUser.setRoles(['kibana_admin']); + }); + + it('should be able to invalidate all sessions at once', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + // Invalidate all sessions and make sure neither of the sessions is active now. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'all' }) + .expect(200, { total: 2 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', basicSessionCookie.cookieString()) + .expect(401); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', samlSessionCookie.cookieString()) + .expect(401); + }); + + it('should do nothing if specified provider type is not configured', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'oidc' } } }) + .expect(200, { total: 0 }); + await checkSessionCookie(basicSessionCookie, notSuperuserTestUser.username, { + type: 'basic', + name: 'basic1', + }); + await checkSessionCookie(samlSessionCookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + }); + + it('should be able to invalidate session only for a specific provider type', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + // Invalidate `basic` session and make sure that only `saml` session is still active. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'basic' } } }) + .expect(200, { total: 1 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', basicSessionCookie.cookieString()) + .expect(401); + await checkSessionCookie(samlSessionCookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + + // Invalidate `saml` session and make sure neither of the sessions is active now. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'saml' } } }) + .expect(200, { total: 1 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', samlSessionCookie.cookieString()) + .expect(401); + }); + + it('should do nothing if specified provider name is not configured', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'basic', name: 'basic2' } } }) + .expect(200, { total: 0 }); + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'saml', name: 'saml2' } } }) + .expect(200, { total: 0 }); + await checkSessionCookie(basicSessionCookie, notSuperuserTestUser.username, { + type: 'basic', + name: 'basic1', + }); + await checkSessionCookie(samlSessionCookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + }); + + it('should be able to invalidate session only for a specific provider name', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + // Invalidate `saml1` session and make sure that only `basic1` session is still active. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'saml', name: 'saml1' } } }) + .expect(200, { total: 1 }); + await checkSessionCookie(basicSessionCookie, notSuperuserTestUser.username, { + type: 'basic', + name: 'basic1', + }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', samlSessionCookie.cookieString()) + .expect(401); + + // Invalidate `basic1` session and make sure neither of the sessions is active now. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ match: 'query', query: { provider: { type: 'basic', name: 'basic1' } } }) + .expect(200, { total: 1 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', basicSessionCookie.cookieString()) + .expect(401); + }); + + it('should do nothing if specified username does not have session', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ + match: 'query', + query: { + provider: { type: 'basic', name: 'basic1' }, + username: `_${notSuperuserTestUser.username}`, + }, + }) + .expect(200, { total: 0 }); + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ + match: 'query', + query: { provider: { type: 'saml', name: 'saml1' }, username: '_a@b.c' }, + }) + .expect(200, { total: 0 }); + await checkSessionCookie(basicSessionCookie, notSuperuserTestUser.username, { + type: 'basic', + name: 'basic1', + }); + await checkSessionCookie(samlSessionCookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + }); + + it('should be able to invalidate session only for a specific user', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + // Invalidate session for `test_user` and make sure that only session of `a@b.c` is still active. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ + match: 'query', + query: { + provider: { type: 'basic', name: 'basic1' }, + username: notSuperuserTestUser.username, + }, + }) + .expect(200, { total: 1 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', basicSessionCookie.cookieString()) + .expect(401); + await checkSessionCookie(samlSessionCookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + + // Invalidate session for `a@b.c` and make sure neither of the sessions is active now. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(adminTestUser.username, adminTestUser.password) + .send({ + match: 'query', + query: { provider: { type: 'saml', name: 'saml1' }, username: 'a@b.c' }, + }) + .expect(200, { total: 1 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', samlSessionCookie.cookieString()) + .expect(401); + }); + + it('only super users should be able to invalidate sessions', async function () { + const basicSessionCookie = await loginWithBasic(notSuperuserTestUser); + const samlSessionCookie = await loginWithSAML(); + + // User without a superuser role shouldn't be able to invalidate sessions. + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(notSuperuserTestUser.username, notSuperuserTestUser.password) + .send({ match: 'all' }) + .expect(403); + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(notSuperuserTestUser.username, notSuperuserTestUser.password) + .send({ match: 'query', query: { provider: { type: 'basic' } } }) + .expect(403); + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(notSuperuserTestUser.username, notSuperuserTestUser.password) + .send({ + match: 'query', + query: { provider: { type: 'basic' }, username: notSuperuserTestUser.username }, + }) + .expect(403); + + await checkSessionCookie(basicSessionCookie, notSuperuserTestUser.username, { + type: 'basic', + name: 'basic1', + }); + await checkSessionCookie(samlSessionCookie, 'a@b.c', { type: 'saml', name: 'saml1' }); + + // With superuser role, it should be possible now. + await security.testUser.setRoles(['superuser']); + + await supertest + .post('/api/security/session/_invalidate') + .set('kbn-xsrf', 'xxx') + .auth(notSuperuserTestUser.username, notSuperuserTestUser.password) + .send({ match: 'all' }) + .expect(200, { total: 2 }); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', basicSessionCookie.cookieString()) + .expect(401); + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', samlSessionCookie.cookieString()) + .expect(401); + }); + }); +} From fdda564a84da90e54c259e89e7082ddfe307308d Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 24 Mar 2021 10:29:46 +0000 Subject: [PATCH 15/88] [ML] Adding additional runtime mapping checks (#94760) * [ML] Adding additional runtime mapping checks * adding functional test for datafeed preview * renaming findFieldsInAgg * updating query check * always use runtime mappings if present in agg field exists check * changes based on review * updating tests based on review * fixing permission check on endpoint and test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../job_creator/advanced_job_creator.ts | 1 + .../util/filter_runtime_mappings.ts | 31 +- .../services/ml_api_service/index.ts | 5 +- .../polled_data_checker.js | 4 +- .../models/data_visualizer/data_visualizer.ts | 8 +- .../models/fields_service/fields_service.ts | 3 + .../ml/server/models/job_service/datafeeds.ts | 1 + .../models/job_validation/job_validation.ts | 8 +- .../ml/server/routes/fields_service.ts | 4 +- .../plugins/ml/server/routes/job_service.ts | 2 +- .../routes/schemas/fields_service_schema.ts | 1 + .../apis/ml/jobs/datafeed_preview.ts | 272 ++++++++++++++++++ .../api_integration/apis/ml/jobs/index.ts | 1 + 13 files changed, 327 insertions(+), 14 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index da5cfc53b7950..2ca95a14fb812 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -181,6 +181,7 @@ export class AdvancedJobCreator extends JobCreator { index: this._indexPatternTitle, timeFieldName: this.timeFieldName, query: this.query, + runtimeMappings: this.datafeedConfig.runtime_mappings, indicesOptions: this.datafeedConfig.indices_options, }); this.setTimeRange(start.epoch, end.epoch); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts index 5319cd3c3aabc..bfed2d811e206 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts @@ -13,6 +13,7 @@ import type { RuntimeMappings } from '../../../../../../../common/types/fields'; import type { Datafeed, Job } from '../../../../../../../common/types/anomaly_detection_jobs'; +import { isPopulatedObject } from '../../../../../../../common/util/object_utils'; interface Response { runtime_mappings: RuntimeMappings; @@ -20,7 +21,10 @@ interface Response { } export function filterRuntimeMappings(job: Job, datafeed: Datafeed): Response { - if (datafeed.runtime_mappings === undefined) { + if ( + datafeed.runtime_mappings === undefined || + isPopulatedObject(datafeed.runtime_mappings) === false + ) { return { runtime_mappings: {}, discarded_mappings: {}, @@ -71,13 +75,18 @@ function findFieldsInJob(job: Job, datafeed: Datafeed) { findFieldsInAgg(aggs).forEach((f) => usedFields.add(f)); } + const query = datafeed.query; + if (query !== undefined) { + findFieldsInQuery(query).forEach((f) => usedFields.add(f)); + } + return [...usedFields]; } -function findFieldsInAgg(obj: Record) { +function findFieldsInAgg(obj: Record) { const fields: string[] = []; Object.entries(obj).forEach(([key, val]) => { - if (typeof val === 'object' && val !== null) { + if (isPopulatedObject(val)) { fields.push(...findFieldsInAgg(val)); } else if (typeof val === 'string' && key === 'field') { fields.push(val); @@ -86,6 +95,22 @@ function findFieldsInAgg(obj: Record) { return fields; } +function findFieldsInQuery(obj: object) { + const fields: string[] = []; + Object.entries(obj).forEach(([key, val]) => { + // return all nested keys in the object + // most will not be fields, but better to catch everything + // and not accidentally remove a used runtime field. + if (isPopulatedObject(val)) { + fields.push(key); + fields.push(...findFieldsInQuery(val)); + } else { + fields.push(key); + } + }); + return fields; +} + function createMappings(rm: RuntimeMappings, usedFieldNames: string[]) { return { runtimeMappings: usedFieldNames.reduce((acc, cur) => { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index e6d0d93cade1f..4acb7fca09d0d 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -25,6 +25,7 @@ import { import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { BucketSpanEstimatorData } from '../../../../common/types/job_service'; +import { RuntimeMappings } from '../../../../common/types/fields'; import { Job, JobStats, @@ -690,14 +691,16 @@ export function mlApiServicesProvider(httpService: HttpService) { index, timeFieldName, query, + runtimeMappings, indicesOptions, }: { index: string; timeFieldName?: string; query: any; + runtimeMappings?: RuntimeMappings; indicesOptions?: IndicesOptions; }) { - const body = JSON.stringify({ index, timeFieldName, query, indicesOptions }); + const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings, indicesOptions }); return httpService.http({ path: `${basePath()}/fields_service/time_field_range`, diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index 8a40787f44490..5fe783e1fc1d5 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -15,11 +15,12 @@ import { get } from 'lodash'; export function polledDataCheckerFactory({ asCurrentUser }) { class PolledDataChecker { - constructor(index, timeField, duration, query, indicesOptions) { + constructor(index, timeField, duration, query, runtimeMappings, indicesOptions) { this.index = index; this.timeField = timeField; this.duration = duration; this.query = query; + this.runtimeMappings = runtimeMappings; this.indicesOptions = indicesOptions; this.isPolled = false; @@ -62,6 +63,7 @@ export function polledDataCheckerFactory({ asCurrentUser }) { }, }, }, + ...this.runtimeMappings, }; return search; diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 4db8295d93997..2a820e0629b75 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -625,15 +625,13 @@ export class DataVisualizer { cardinalityField = aggs[`${safeFieldName}_cardinality`] = { cardinality: { script: datafeedConfig?.script_fields[field].script }, }; - } else if (datafeedConfig?.runtime_mappings?.hasOwnProperty(field)) { - cardinalityField = { - cardinality: { field }, - }; - runtimeMappings.runtime_mappings = datafeedConfig.runtime_mappings; } else { cardinalityField = { cardinality: { field }, }; + if (datafeedConfig !== undefined && isPopulatedObject(datafeedConfig?.runtime_mappings)) { + runtimeMappings.runtime_mappings = datafeedConfig.runtime_mappings; + } } aggs[`${safeFieldName}_cardinality`] = cardinalityField; }); diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 1270cc6f08e23..8e4dbaf23212f 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -14,6 +14,7 @@ import { AggCardinality } from '../../../common/types/fields'; import { isValidAggregationField } from '../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; +import { RuntimeMappings } from '../../../common/types/fields'; /** * Service for carrying out queries to obtain data @@ -212,6 +213,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { index: string[] | string, timeFieldName: string, query: any, + runtimeMappings?: RuntimeMappings, indicesOptions?: IndicesOptions ): Promise<{ success: boolean; @@ -239,6 +241,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { }, }, }, + ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, ...(indicesOptions ?? {}), }); diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index cb651a0a410af..cf9b2027225c9 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -219,6 +219,7 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie datafeed.indices, job.data_description.time_field, query, + datafeed.runtime_mappings, datafeed.indices_options ); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 31d98753f0bd1..2beade7f5dbc4 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -64,7 +64,13 @@ export async function validateJob( const fs = fieldsServiceProvider(client); const index = job.datafeed_config.indices.join(','); const timeField = job.data_description.time_field; - const timeRange = await fs.getTimeFieldRange(index, timeField, job.datafeed_config.query); + const timeRange = await fs.getTimeFieldRange( + index, + timeField, + job.datafeed_config.query, + job.datafeed_config.runtime_mappings, + job.datafeed_config.indices_options + ); duration = { start: timeRange.start.epoch, diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index c087b86172fa9..dc43d915e87a2 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -22,8 +22,8 @@ function getCardinalityOfFields(client: IScopedClusterClient, payload: any) { function getTimeFieldRange(client: IScopedClusterClient, payload: any) { const fs = fieldsServiceProvider(client); - const { index, timeFieldName, query, indicesOptions } = payload; - return fs.getTimeFieldRange(index, timeFieldName, query, indicesOptions); + const { index, timeFieldName, query, runtimeMappings, indicesOptions } = payload; + return fs.getTimeFieldRange(index, timeFieldName, query, runtimeMappings, indicesOptions); } /** diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index b3aa9f956895a..1f755c27db871 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -791,7 +791,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { body: datafeedPreviewSchema, }, options: { - tags: ['access:ml:canGetJobs'], + tags: ['access:ml:canPreviewDatafeed'], }, }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { diff --git a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts index db827b26fe73a..76a307e710dc8 100644 --- a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts @@ -31,5 +31,6 @@ export const getTimeFieldRangeSchema = schema.object({ /** Query to match documents in the index(es). */ query: schema.maybe(schema.any()), /** Additional search options. */ + runtimeMappings: schema.maybe(schema.any()), indicesOptions: indicesOptionsSchema, }); diff --git a/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts b/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts new file mode 100644 index 0000000000000..2fcf75f99ff17 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts @@ -0,0 +1,272 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `fq_datafeed_preview_${Date.now()}`; + + const job = { + job_id: `${jobId}_1`, + description: '', + groups: ['automated', 'farequote'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'distinct_count', field_name: 'airline' }], + influencers: [], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + }; + + function isUpperCase(str: string) { + return /^[A-Z]+$/.test(str); + } + + function isLowerCase(str: string) { + return !/[A-Z]+/.test(str); + } + + describe('Datafeed preview', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it(`should return a normal datafeed preview`, async () => { + const datafeed = { + datafeed_id: '', + job_id: '', + indices: ['ft_farequote'], + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + runtime_mappings: {}, + }; + + const { body } = await supertest + .post('/api/ml/jobs/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job, datafeed }) + .expect(200); + + expect(body.hits.total.value).to.eql(3207, 'Response body total hits should be 3207'); + expect(Array.isArray(body.hits?.hits[0]?.fields?.airline)).to.eql( + true, + 'Response body airlines should be an array' + ); + + const airlines: string[] = body.hits.hits.map((a: any) => a.fields.airline[0]); + expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); + expect(airlines.every((a) => isUpperCase(a))).to.eql( + true, + 'Response body airlines should all be upper case' + ); + }); + + it(`should return a datafeed preview using custom query`, async () => { + const datafeed = { + datafeed_id: '', + job_id: '', + indices: ['ft_farequote'], + query: { + bool: { + should: [ + { + match: { + airline: 'AAL', + }, + }, + ], + }, + }, + runtime_mappings: {}, + }; + + const { body } = await supertest + .post('/api/ml/jobs/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job, datafeed }) + .expect(200); + + expect(body.hits.total.value).to.eql(300, 'Response body total hits should be 300'); + expect(Array.isArray(body.hits?.hits[0]?.fields?.airline)).to.eql( + true, + 'Response body airlines should be an array' + ); + + const airlines: string[] = body.hits.hits.map((a: any) => a.fields.airline[0]); + expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); + expect(airlines.every((a) => a === 'AAL')).to.eql( + true, + 'Response body airlines should all be AAL' + ); + }); + + it(`should return a datafeed preview using runtime mappings`, async () => { + const datafeed = { + datafeed_id: '', + job_id: '', + indices: ['ft_farequote'], + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + runtime_mappings: { + lowercase_airline: { + type: 'keyword', + script: { + source: 'emit(params._source.airline.toLowerCase())', + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/jobs/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job, datafeed }) + .expect(200); + + expect(body.hits.total.value).to.eql(3207, 'Response body total hits should be 3207'); + expect(Array.isArray(body.hits?.hits[0]?.fields?.lowercase_airline)).to.eql( + true, + 'Response body airlines should be an array' + ); + + const airlines: string[] = body.hits.hits.map((a: any) => a.fields.lowercase_airline[0]); + expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); + expect(isLowerCase(airlines[0])).to.eql( + true, + 'Response body airlines should all be lower case' + ); + }); + + it(`should return a datafeed preview using custom query and runtime mappings which override the field name`, async () => { + const datafeed = { + datafeed_id: '', + job_id: '', + indices: ['ft_farequote'], + query: { + bool: { + should: [ + { + match: { + airline: 'aal', + }, + }, + ], + }, + }, + runtime_mappings: { + // override the airline field name + airline: { + type: 'keyword', + script: { + source: 'emit(params._source.airline.toLowerCase())', + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/jobs/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job, datafeed }) + .expect(200); + + expect(body.hits.total.value).to.eql(300, 'Response body total hits should be 300'); + expect(Array.isArray(body.hits?.hits[0]?.fields?.airline)).to.eql( + true, + 'Response body airlines should be an array' + ); + + const airlines: string[] = body.hits.hits.map((a: any) => a.fields.airline[0]); + expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); + expect(isLowerCase(airlines[0])).to.eql( + true, + 'Response body airlines should all be lower case' + ); + }); + + it(`should return not a datafeed preview for ML viewer user`, async () => { + const datafeed = { + datafeed_id: '', + job_id: '', + indices: ['ft_farequote'], + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + runtime_mappings: {}, + }; + + await supertest + .post('/api/ml/jobs/datafeed_preview') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job, datafeed }) + .expect(403); + }); + + it(`should return not a datafeed preview for unauthorized user`, async () => { + const datafeed = { + datafeed_id: '', + job_id: '', + indices: ['ft_farequote'], + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + runtime_mappings: {}, + }; + + await supertest + .post('/api/ml/jobs/datafeed_preview') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send({ job, datafeed }) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts index bea6d5972e7a6..4c52f2ef862c3 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/index.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./jobs_exist_spaces')); loadTestFile(require.resolve('./close_jobs_spaces')); loadTestFile(require.resolve('./delete_jobs_spaces')); + loadTestFile(require.resolve('./datafeed_preview')); }); } From f07b1722cb71609663b70dd889545d2d4aeec028 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Mar 2021 11:44:23 +0100 Subject: [PATCH 16/88] [Lens] Implement filtered metric (#92589) --- api_docs/charts.json | 42 +- api_docs/data.json | 345 +- api_docs/data_search.json | 4404 +++++++++++++---- api_docs/expressions.json | 81 + api_docs/fleet.json | 177 +- api_docs/lens.json | 18 +- api_docs/observability.json | 4 +- ...c.aggfunctionsmapping.aggfilteredmetric.md | 11 + ...plugins-data-public.aggfunctionsmapping.md | 1 + ...plugin-plugins-data-public.metric_types.md | 1 + ...r.aggfunctionsmapping.aggfilteredmetric.md | 11 + ...plugins-data-server.aggfunctionsmapping.md | 1 + ...plugin-plugins-data-server.metric_types.md | 1 + .../data/common/search/aggs/agg_config.ts | 4 +- .../data/common/search/aggs/agg_configs.ts | 20 +- .../data/common/search/aggs/agg_type.ts | 8 + .../data/common/search/aggs/agg_types.ts | 2 + .../common/search/aggs/aggs_service.test.ts | 2 + .../data/common/search/aggs/buckets/filter.ts | 27 +- .../search/aggs/buckets/filter_fn.test.ts | 21 + .../common/search/aggs/buckets/filter_fn.ts | 19 +- .../aggs/metrics/filtered_metric.test.ts | 72 + .../search/aggs/metrics/filtered_metric.ts | 56 + .../aggs/metrics/filtered_metric_fn.test.ts | 51 + .../search/aggs/metrics/filtered_metric_fn.ts | 94 + .../data/common/search/aggs/metrics/index.ts | 2 + .../metrics/lib/parent_pipeline_agg_helper.ts | 1 + .../lib/sibling_pipeline_agg_helper.ts | 8 +- .../search/aggs/metrics/metric_agg_types.ts | 1 + src/plugins/data/common/search/aggs/types.ts | 4 + src/plugins/data/public/public.api.md | 14 +- .../public/search/aggs/aggs_service.test.ts | 4 +- src/plugins/data/server/server.api.md | 10 +- .../vis_type_metric/public/metric_vis_type.ts | 1 + .../public/legacy/table_vis_legacy_type.ts | 2 +- .../vis_type_table/public/table_vis_type.ts | 2 +- .../public/tag_cloud_type.ts | 1 + src/plugins/vis_type_vislib/public/gauge.ts | 1 + src/plugins/vis_type_vislib/public/goal.ts | 1 + src/plugins/vis_type_vislib/public/heatmap.ts | 1 + .../vis_type_xy/public/vis_types/area.ts | 2 +- .../vis_type_xy/public/vis_types/histogram.ts | 2 +- .../public/vis_types/horizontal_bar.ts | 2 +- .../vis_type_xy/public/vis_types/line.ts | 2 +- .../dimension_panel/advanced_options.tsx | 82 + .../dimension_panel/dimension_editor.tsx | 69 +- .../dimension_panel/dimension_panel.test.tsx | 227 +- .../dimension_panel/filtering.tsx | 131 + .../dimension_panel/time_scaling.tsx | 65 +- .../indexpattern.test.ts | 117 + .../definitions/calculations/counter_rate.tsx | 2 + .../calculations/cumulative_sum.tsx | 2 + .../definitions/calculations/derivative.tsx | 2 + .../calculations/moving_average.tsx | 2 + .../operations/definitions/cardinality.tsx | 2 + .../operations/definitions/column_types.ts | 2 + .../operations/definitions/count.tsx | 2 + .../filters/filter_popover.test.tsx | 3 +- .../definitions/filters/filter_popover.tsx | 56 +- .../operations/definitions/index.ts | 1 + .../operations/definitions/last_value.tsx | 2 + .../operations/definitions/metrics.tsx | 2 + .../operations/layer_helpers.test.ts | 43 + .../operations/layer_helpers.ts | 13 +- .../operations/mocks.ts | 1 + .../indexpattern_datasource/query_input.tsx | 66 + .../indexpattern_datasource/to_expression.ts | 53 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 69 files changed, 4976 insertions(+), 1505 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md create mode 100644 src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/filtered_metric.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx diff --git a/api_docs/charts.json b/api_docs/charts.json index 5c4008d0f25bc..70bf2166de7c8 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -1597,7 +1597,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 3160 + "lineNumber": 174 }, "signature": [ { @@ -1816,7 +1816,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 558 + "lineNumber": 56 }, "signature": [ { @@ -1837,7 +1837,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 559 + "lineNumber": 57 } }, { @@ -1848,7 +1848,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 562 + "lineNumber": 60 }, "signature": [ "[number, number[]][]" @@ -1859,7 +1859,7 @@ "label": "[ColorSchemas.Greens]", "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 557 + "lineNumber": 55 } }, { @@ -1875,7 +1875,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1078 + "lineNumber": 74 }, "signature": [ { @@ -1896,7 +1896,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1079 + "lineNumber": 75 } }, { @@ -1907,7 +1907,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1082 + "lineNumber": 78 }, "signature": [ "[number, number[]][]" @@ -1918,7 +1918,7 @@ "label": "[ColorSchemas.Greys]", "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1077 + "lineNumber": 73 } }, { @@ -1934,7 +1934,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1598 + "lineNumber": 92 }, "signature": [ { @@ -1955,7 +1955,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1599 + "lineNumber": 93 } }, { @@ -1966,7 +1966,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1602 + "lineNumber": 96 }, "signature": [ "[number, number[]][]" @@ -1977,7 +1977,7 @@ "label": "[ColorSchemas.Reds]", "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 1597 + "lineNumber": 91 } }, { @@ -1993,7 +1993,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2118 + "lineNumber": 110 }, "signature": [ { @@ -2014,7 +2014,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2119 + "lineNumber": 111 } }, { @@ -2025,7 +2025,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2122 + "lineNumber": 114 }, "signature": [ "[number, number[]][]" @@ -2036,7 +2036,7 @@ "label": "[ColorSchemas.YellowToRed]", "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2117 + "lineNumber": 109 } }, { @@ -2052,7 +2052,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2639 + "lineNumber": 129 }, "signature": [ { @@ -2073,7 +2073,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2640 + "lineNumber": 130 } }, { @@ -2084,7 +2084,7 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2643 + "lineNumber": 133 }, "signature": [ "[number, number[]][]" @@ -2095,7 +2095,7 @@ "label": "[ColorSchemas.GreenToRed]", "source": { "path": "src/plugins/charts/public/static/color_maps/color_maps.ts", - "lineNumber": 2638 + "lineNumber": 128 } } ], diff --git a/api_docs/data.json b/api_docs/data.json index a78aec92b1fa5..a9ef03d881ce8 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -608,7 +608,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 263 + "lineNumber": 265 } }, { @@ -634,7 +634,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 294 + "lineNumber": 296 } }, { @@ -654,7 +654,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 305 + "lineNumber": 307 } }, { @@ -680,7 +680,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 314 + "lineNumber": 316 } }, { @@ -712,7 +712,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 369 + "lineNumber": 371 } }, { @@ -736,7 +736,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 373 + "lineNumber": 375 } }, { @@ -760,7 +760,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 377 + "lineNumber": 379 } }, { @@ -782,7 +782,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381 + "lineNumber": 383 } } ], @@ -790,7 +790,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381 + "lineNumber": 383 } }, { @@ -812,7 +812,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 385 + "lineNumber": 387 } }, { @@ -825,7 +825,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 385 + "lineNumber": 387 } } ], @@ -833,7 +833,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 385 + "lineNumber": 387 } }, { @@ -849,7 +849,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 393 + "lineNumber": 395 } }, { @@ -865,7 +865,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 399 + "lineNumber": 401 } }, { @@ -883,7 +883,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 408 + "lineNumber": 410 } }, { @@ -905,7 +905,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 412 + "lineNumber": 414 } } ], @@ -913,7 +913,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 412 + "lineNumber": 414 } }, { @@ -936,7 +936,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 426 + "lineNumber": 428 } }, { @@ -960,7 +960,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 430 + "lineNumber": 432 } }, { @@ -976,7 +976,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 434 + "lineNumber": 436 } }, { @@ -992,7 +992,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 439 + "lineNumber": 441 } }, { @@ -1003,7 +1003,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 446 + "lineNumber": 448 }, "signature": [ { @@ -1023,7 +1023,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 450 + "lineNumber": 452 }, "signature": [ { @@ -1068,7 +1068,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 480 + "lineNumber": 482 } } ], @@ -1076,7 +1076,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 480 + "lineNumber": 482 } } ], @@ -1101,7 +1101,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 58 + "lineNumber": 65 }, "signature": [ { @@ -1121,7 +1121,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 59 + "lineNumber": 66 }, "signature": [ { @@ -1142,7 +1142,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 62 + "lineNumber": 69 }, "signature": [ { @@ -1180,7 +1180,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 65 + "lineNumber": 72 } }, { @@ -1217,7 +1217,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 66 + "lineNumber": 73 } }, { @@ -1236,7 +1236,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 67 + "lineNumber": 74 } } ], @@ -1244,7 +1244,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 64 + "lineNumber": 71 } }, { @@ -1280,7 +1280,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 79 + "lineNumber": 86 } } ], @@ -1288,7 +1288,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 79 + "lineNumber": 86 } }, { @@ -1317,7 +1317,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 97 + "lineNumber": 104 } } ], @@ -1325,7 +1325,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 97 + "lineNumber": 104 } }, { @@ -1366,7 +1366,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 111 + "lineNumber": 118 } }, { @@ -1379,7 +1379,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 112 + "lineNumber": 119 } } ], @@ -1429,7 +1429,7 @@ "label": "createAggConfig", "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 110 + "lineNumber": 117 }, "tags": [], "returnComment": [] @@ -1472,7 +1472,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 153 + "lineNumber": 160 } } ], @@ -1480,7 +1480,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 153 + "lineNumber": 160 } }, { @@ -1502,7 +1502,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 165 + "lineNumber": 172 } } ], @@ -1510,7 +1510,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 165 + "lineNumber": 172 } }, { @@ -1534,7 +1534,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 227 + "lineNumber": 241 } }, { @@ -1563,7 +1563,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 231 + "lineNumber": 245 } } ], @@ -1571,7 +1571,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 231 + "lineNumber": 245 } }, { @@ -1601,7 +1601,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 235 + "lineNumber": 249 } } ], @@ -1609,7 +1609,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 235 + "lineNumber": 249 } }, { @@ -1639,7 +1639,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 239 + "lineNumber": 253 } } ], @@ -1647,7 +1647,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 239 + "lineNumber": 253 } }, { @@ -1677,7 +1677,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 243 + "lineNumber": 257 } } ], @@ -1685,7 +1685,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 243 + "lineNumber": 257 } }, { @@ -1715,7 +1715,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 247 + "lineNumber": 261 } } ], @@ -1723,7 +1723,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 247 + "lineNumber": 261 } }, { @@ -1753,7 +1753,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 251 + "lineNumber": 265 } } ], @@ -1761,7 +1761,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 251 + "lineNumber": 265 } }, { @@ -1785,7 +1785,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 255 + "lineNumber": 269 } }, { @@ -1815,7 +1815,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 269 + "lineNumber": 283 } } ], @@ -1823,7 +1823,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 269 + "lineNumber": 283 } }, { @@ -1851,7 +1851,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 284 + "lineNumber": 298 } }, { @@ -1885,7 +1885,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 298 + "lineNumber": 312 } } ], @@ -1895,7 +1895,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 298 + "lineNumber": 312 } }, { @@ -1941,7 +1941,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 307 + "lineNumber": 321 } }, { @@ -1961,7 +1961,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 307 + "lineNumber": 321 } } ], @@ -1969,13 +1969,13 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 307 + "lineNumber": 321 } } ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 57 + "lineNumber": 64 }, "initialIsOpen": false }, @@ -6059,7 +6059,7 @@ "type": "Function", "label": "setField", "signature": [ - "(field: K, value: ", + "(field: K, value: ", { "pluginId": "data", "scope": "common", @@ -6123,7 +6123,7 @@ "type": "Function", "label": "removeField", "signature": [ - "(field: K) => this" + "(field: K) => this" ], "description": [ "\nremove field" @@ -6250,7 +6250,7 @@ "type": "Function", "label": "getField", "signature": [ - "(field: K, recurse?: boolean) => ", + "(field: K, recurse?: boolean) => ", { "pluginId": "data", "scope": "common", @@ -6303,7 +6303,7 @@ "type": "Function", "label": "getOwnField", "signature": [ - "(field: K) => ", + "(field: K) => ", { "pluginId": "data", "scope": "common", @@ -7635,7 +7635,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 204 + "lineNumber": 207 }, "signature": [ "FunctionDefinition" @@ -7649,7 +7649,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 205 + "lineNumber": 208 }, "signature": [ "FunctionDefinition" @@ -7663,7 +7663,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 206 + "lineNumber": 209 }, "signature": [ "FunctionDefinition" @@ -7677,7 +7677,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 207 + "lineNumber": 210 }, "signature": [ "FunctionDefinition" @@ -7691,7 +7691,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 208 + "lineNumber": 211 }, "signature": [ "FunctionDefinition" @@ -7705,7 +7705,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 209 + "lineNumber": 212 }, "signature": [ "FunctionDefinition" @@ -7719,7 +7719,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 210 + "lineNumber": 213 }, "signature": [ "FunctionDefinition" @@ -7733,7 +7733,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 211 + "lineNumber": 214 }, "signature": [ "FunctionDefinition" @@ -7747,7 +7747,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 212 + "lineNumber": 215 }, "signature": [ "FunctionDefinition" @@ -7761,7 +7761,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 213 + "lineNumber": 216 }, "signature": [ "FunctionDefinition" @@ -7775,7 +7775,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 214 + "lineNumber": 217 }, "signature": [ "FunctionDefinition" @@ -7789,7 +7789,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 215 + "lineNumber": 218 }, "signature": [ "FunctionDefinition" @@ -7803,7 +7803,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 216 + "lineNumber": 219 }, "signature": [ "FunctionDefinition" @@ -7817,7 +7817,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 217 + "lineNumber": 220 }, "signature": [ "FunctionDefinition" @@ -7831,7 +7831,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 218 + "lineNumber": 221 }, "signature": [ "FunctionDefinition" @@ -7845,7 +7845,21 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 219 + "lineNumber": 222 + }, + "signature": [ + "FunctionDefinition" + ] + }, + { + "tags": [], + "id": "def-public.AggFunctionsMapping.aggFilteredMetric", + "type": "Object", + "label": "aggFilteredMetric", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/types.ts", + "lineNumber": 223 }, "signature": [ "FunctionDefinition" @@ -7859,7 +7873,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 220 + "lineNumber": 224 }, "signature": [ "FunctionDefinition" @@ -7873,7 +7887,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 221 + "lineNumber": 225 }, "signature": [ "FunctionDefinition" @@ -7887,7 +7901,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 222 + "lineNumber": 226 }, "signature": [ "FunctionDefinition" @@ -7901,7 +7915,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 223 + "lineNumber": 227 }, "signature": [ "FunctionDefinition" @@ -7915,7 +7929,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 224 + "lineNumber": 228 }, "signature": [ "FunctionDefinition" @@ -7929,7 +7943,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 225 + "lineNumber": 229 }, "signature": [ "FunctionDefinition" @@ -7943,7 +7957,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 226 + "lineNumber": 230 }, "signature": [ "FunctionDefinition" @@ -7957,7 +7971,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 227 + "lineNumber": 231 }, "signature": [ "FunctionDefinition" @@ -7971,7 +7985,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 228 + "lineNumber": 232 }, "signature": [ "FunctionDefinition" @@ -7985,7 +7999,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 229 + "lineNumber": 233 }, "signature": [ "FunctionDefinition" @@ -7999,7 +8013,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 230 + "lineNumber": 234 }, "signature": [ "FunctionDefinition" @@ -8013,7 +8027,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 231 + "lineNumber": 235 }, "signature": [ "FunctionDefinition" @@ -8027,7 +8041,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 232 + "lineNumber": 236 }, "signature": [ "FunctionDefinition" @@ -8041,7 +8055,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 233 + "lineNumber": 237 }, "signature": [ "FunctionDefinition" @@ -8055,7 +8069,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 234 + "lineNumber": 238 }, "signature": [ "FunctionDefinition" @@ -8069,7 +8083,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 235 + "lineNumber": 239 }, "signature": [ "FunctionDefinition" @@ -8078,7 +8092,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 203 + "lineNumber": 206 }, "initialIsOpen": false }, @@ -11407,7 +11421,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 139 + "lineNumber": 141 }, "signature": [ "{ calculateAutoTimeExpression: (range: TimeRange) => string | undefined; getDateMetaByDatatableColumn: (column: DatatableColumn) => Promise<{ timeZone: string; timeRange?: TimeRange | undefined; interval: string; } | undefined>; datatableUtilities: { getIndexPattern: (column: DatatableColumn) => Promise; getAggConfig: (column: DatatableColumn) => Promise; isFilterable: (column: DatatableColumn) => boolean; }; createAggConfigs: (indexPattern: IndexPattern, configStates?: Pick>" @@ -11946,7 +11960,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 29 + "lineNumber": 32 }, "signature": [ "{ type: \"kibana_context\"; } & ExecutionContextSearch" @@ -18807,7 +18821,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 204 + "lineNumber": 207 }, "signature": [ "FunctionDefinition" @@ -18821,7 +18835,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 205 + "lineNumber": 208 }, "signature": [ "FunctionDefinition" @@ -18835,7 +18849,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 206 + "lineNumber": 209 }, "signature": [ "FunctionDefinition" @@ -18849,7 +18863,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 207 + "lineNumber": 210 }, "signature": [ "FunctionDefinition" @@ -18863,7 +18877,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 208 + "lineNumber": 211 }, "signature": [ "FunctionDefinition" @@ -18877,7 +18891,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 209 + "lineNumber": 212 }, "signature": [ "FunctionDefinition" @@ -18891,7 +18905,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 210 + "lineNumber": 213 }, "signature": [ "FunctionDefinition" @@ -18905,7 +18919,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 211 + "lineNumber": 214 }, "signature": [ "FunctionDefinition" @@ -18919,7 +18933,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 212 + "lineNumber": 215 }, "signature": [ "FunctionDefinition" @@ -18933,7 +18947,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 213 + "lineNumber": 216 }, "signature": [ "FunctionDefinition" @@ -18947,7 +18961,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 214 + "lineNumber": 217 }, "signature": [ "FunctionDefinition" @@ -18961,7 +18975,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 215 + "lineNumber": 218 }, "signature": [ "FunctionDefinition" @@ -18975,7 +18989,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 216 + "lineNumber": 219 }, "signature": [ "FunctionDefinition" @@ -18989,7 +19003,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 217 + "lineNumber": 220 }, "signature": [ "FunctionDefinition" @@ -19003,7 +19017,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 218 + "lineNumber": 221 }, "signature": [ "FunctionDefinition" @@ -19017,7 +19031,21 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 219 + "lineNumber": 222 + }, + "signature": [ + "FunctionDefinition" + ] + }, + { + "tags": [], + "id": "def-server.AggFunctionsMapping.aggFilteredMetric", + "type": "Object", + "label": "aggFilteredMetric", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/types.ts", + "lineNumber": 223 }, "signature": [ "FunctionDefinition" @@ -19031,7 +19059,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 220 + "lineNumber": 224 }, "signature": [ "FunctionDefinition" @@ -19045,7 +19073,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 221 + "lineNumber": 225 }, "signature": [ "FunctionDefinition" @@ -19059,7 +19087,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 222 + "lineNumber": 226 }, "signature": [ "FunctionDefinition" @@ -19073,7 +19101,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 223 + "lineNumber": 227 }, "signature": [ "FunctionDefinition" @@ -19087,7 +19115,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 224 + "lineNumber": 228 }, "signature": [ "FunctionDefinition" @@ -19101,7 +19129,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 225 + "lineNumber": 229 }, "signature": [ "FunctionDefinition" @@ -19115,7 +19143,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 226 + "lineNumber": 230 }, "signature": [ "FunctionDefinition" @@ -19129,7 +19157,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 227 + "lineNumber": 231 }, "signature": [ "FunctionDefinition" @@ -19143,7 +19171,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 228 + "lineNumber": 232 }, "signature": [ "FunctionDefinition" @@ -19157,7 +19185,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 229 + "lineNumber": 233 }, "signature": [ "FunctionDefinition" @@ -19171,7 +19199,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 230 + "lineNumber": 234 }, "signature": [ "FunctionDefinition" @@ -19185,7 +19213,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 231 + "lineNumber": 235 }, "signature": [ "FunctionDefinition" @@ -19199,7 +19227,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 232 + "lineNumber": 236 }, "signature": [ "FunctionDefinition" @@ -19213,7 +19241,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 233 + "lineNumber": 237 }, "signature": [ "FunctionDefinition" @@ -19227,7 +19255,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 234 + "lineNumber": 238 }, "signature": [ "FunctionDefinition" @@ -19241,7 +19269,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 235 + "lineNumber": 239 }, "signature": [ "FunctionDefinition" @@ -19250,7 +19278,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 203 + "lineNumber": 206 }, "initialIsOpen": false }, @@ -20453,7 +20481,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 14 + "lineNumber": 15 }, "signature": [ "{ filters?: Filter[] | undefined; query?: Query | Query[] | undefined; timeRange?: TimeRange | undefined; }" @@ -20499,7 +20527,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 25 + "lineNumber": 26 }, "signature": [ "ExpressionFunctionDefinition<\"kibana_context\", ", @@ -20530,7 +20558,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 20 + "lineNumber": 21 }, "signature": [ "{ type: \"kibana_context\"; } & ExecutionContextSearch" @@ -20593,7 +20621,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 58 + "lineNumber": 59 }, "signature": [ "AggType>" @@ -20722,7 +20750,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 29 + "lineNumber": 32 }, "signature": [ "{ type: \"kibana_context\"; } & ExecutionContextSearch" @@ -22654,7 +22682,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 40 + "lineNumber": 42 } }, { @@ -22667,7 +22695,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 41 + "lineNumber": 43 } }, { @@ -22680,7 +22708,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 42 + "lineNumber": 44 } }, { @@ -22693,7 +22721,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 43 + "lineNumber": 45 } }, { @@ -22706,7 +22734,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 44 + "lineNumber": 46 } }, { @@ -22725,7 +22753,7 @@ "description": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 45 + "lineNumber": 47 } } ], @@ -22733,7 +22761,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/es_query/filters/build_filters.ts", - "lineNumber": 39 + "lineNumber": 41 }, "initialIsOpen": false }, @@ -23062,7 +23090,7 @@ "section": "def-common.FilterStateStore", "text": "FilterStateStore" }, - ") => ", + " | undefined) => ", { "pluginId": "data", "scope": "common", @@ -23183,9 +23211,9 @@ } }, { - "type": "Enum", + "type": "CompoundType", "label": "store", - "isRequired": true, + "isRequired": false, "signature": [ { "pluginId": "data", @@ -23193,7 +23221,8 @@ "docId": "kibDataPluginApi", "section": "def-common.FilterStateStore", "text": "FilterStateStore" - } + }, + " | undefined" ], "description": [], "source": { diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 68cf4a1123bdb..a75b669cbd288 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -2955,7 +2955,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 263 + "lineNumber": 265 } }, { @@ -2981,7 +2981,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 294 + "lineNumber": 296 } }, { @@ -3001,7 +3001,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 305 + "lineNumber": 307 } }, { @@ -3027,7 +3027,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 314 + "lineNumber": 316 } }, { @@ -3059,7 +3059,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 369 + "lineNumber": 371 } }, { @@ -3083,7 +3083,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 373 + "lineNumber": 375 } }, { @@ -3107,7 +3107,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 377 + "lineNumber": 379 } }, { @@ -3129,7 +3129,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381 + "lineNumber": 383 } } ], @@ -3137,7 +3137,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381 + "lineNumber": 383 } }, { @@ -3159,7 +3159,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 385 + "lineNumber": 387 } }, { @@ -3172,7 +3172,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 385 + "lineNumber": 387 } } ], @@ -3180,7 +3180,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 385 + "lineNumber": 387 } }, { @@ -3196,7 +3196,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 393 + "lineNumber": 395 } }, { @@ -3212,7 +3212,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 399 + "lineNumber": 401 } }, { @@ -3230,7 +3230,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 408 + "lineNumber": 410 } }, { @@ -3252,7 +3252,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 412 + "lineNumber": 414 } } ], @@ -3260,7 +3260,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 412 + "lineNumber": 414 } }, { @@ -3283,7 +3283,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 426 + "lineNumber": 428 } }, { @@ -3307,7 +3307,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 430 + "lineNumber": 432 } }, { @@ -3323,7 +3323,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 434 + "lineNumber": 436 } }, { @@ -3339,7 +3339,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 439 + "lineNumber": 441 } }, { @@ -3350,7 +3350,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 446 + "lineNumber": 448 }, "signature": [ { @@ -3370,7 +3370,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 450 + "lineNumber": 452 }, "signature": [ { @@ -3415,7 +3415,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 480 + "lineNumber": 482 } } ], @@ -3423,7 +3423,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 480 + "lineNumber": 482 } } ], @@ -3448,7 +3448,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 58 + "lineNumber": 65 }, "signature": [ { @@ -3468,7 +3468,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 59 + "lineNumber": 66 }, "signature": [ { @@ -3489,7 +3489,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 62 + "lineNumber": 69 }, "signature": [ { @@ -3527,7 +3527,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 65 + "lineNumber": 72 } }, { @@ -3564,7 +3564,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 66 + "lineNumber": 73 } }, { @@ -3583,7 +3583,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 67 + "lineNumber": 74 } } ], @@ -3591,7 +3591,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 64 + "lineNumber": 71 } }, { @@ -3627,7 +3627,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 79 + "lineNumber": 86 } } ], @@ -3635,7 +3635,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 79 + "lineNumber": 86 } }, { @@ -3664,7 +3664,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 97 + "lineNumber": 104 } } ], @@ -3672,7 +3672,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 97 + "lineNumber": 104 } }, { @@ -3713,7 +3713,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 111 + "lineNumber": 118 } }, { @@ -3726,7 +3726,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 112 + "lineNumber": 119 } } ], @@ -3776,7 +3776,7 @@ "label": "createAggConfig", "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 110 + "lineNumber": 117 }, "tags": [], "returnComment": [] @@ -3819,7 +3819,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 153 + "lineNumber": 160 } } ], @@ -3827,7 +3827,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 153 + "lineNumber": 160 } }, { @@ -3849,7 +3849,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 165 + "lineNumber": 172 } } ], @@ -3857,7 +3857,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 165 + "lineNumber": 172 } }, { @@ -3881,7 +3881,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 227 + "lineNumber": 241 } }, { @@ -3910,7 +3910,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 231 + "lineNumber": 245 } } ], @@ -3918,7 +3918,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 231 + "lineNumber": 245 } }, { @@ -3948,7 +3948,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 235 + "lineNumber": 249 } } ], @@ -3956,7 +3956,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 235 + "lineNumber": 249 } }, { @@ -3986,7 +3986,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 239 + "lineNumber": 253 } } ], @@ -3994,7 +3994,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 239 + "lineNumber": 253 } }, { @@ -4024,7 +4024,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 243 + "lineNumber": 257 } } ], @@ -4032,7 +4032,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 243 + "lineNumber": 257 } }, { @@ -4062,7 +4062,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 247 + "lineNumber": 261 } } ], @@ -4070,7 +4070,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 247 + "lineNumber": 261 } }, { @@ -4100,7 +4100,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 251 + "lineNumber": 265 } } ], @@ -4108,7 +4108,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 251 + "lineNumber": 265 } }, { @@ -4132,7 +4132,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 255 + "lineNumber": 269 } }, { @@ -4162,7 +4162,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 269 + "lineNumber": 283 } } ], @@ -4170,7 +4170,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 269 + "lineNumber": 283 } }, { @@ -4198,7 +4198,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 284 + "lineNumber": 298 } }, { @@ -4232,7 +4232,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 298 + "lineNumber": 312 } } ], @@ -4242,7 +4242,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 298 + "lineNumber": 312 } }, { @@ -4288,7 +4288,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 307 + "lineNumber": 321 } }, { @@ -4308,7 +4308,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 307 + "lineNumber": 321 } } ], @@ -4316,13 +4316,13 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 307 + "lineNumber": 321 } } ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 57 + "lineNumber": 64 }, "initialIsOpen": false }, @@ -4572,7 +4572,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 70 + "lineNumber": 71 } }, { @@ -4583,7 +4583,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 72 + "lineNumber": 73 } }, { @@ -4594,7 +4594,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 73 + "lineNumber": 74 }, "signature": [ "string | undefined" @@ -4613,7 +4613,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 80 + "lineNumber": 81 } }, { @@ -4629,7 +4629,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 87 + "lineNumber": 88 } }, { @@ -4645,7 +4645,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 94 + "lineNumber": 95 } }, { @@ -4658,7 +4658,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 99 + "lineNumber": 100 }, "signature": [ "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"ip\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"conflict\" | \"nested\" | \"histogram\" | \"null\" | undefined" @@ -4676,7 +4676,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 108 + "lineNumber": 109 }, "signature": [ "((aggConfig: TAggConfig) => string) | (() => string)" @@ -4695,7 +4695,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 123 + "lineNumber": 124 }, "signature": [ "any" @@ -4713,7 +4713,22 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 131 + "lineNumber": 132 + } + }, + { + "tags": [ + "type" + ], + "id": "def-common.AggType.hasNoDslParams", + "type": "boolean", + "label": "hasNoDslParams", + "description": [ + "\nFlag that prevents params from this aggregation from being included in the dsl. Sibling and parent aggs are still written.\n" + ], + "source": { + "path": "src/plugins/data/common/search/aggs/agg_type.ts", + "lineNumber": 138 } }, { @@ -4726,7 +4741,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 138 + "lineNumber": 145 }, "signature": [ "((aggConfig: TAggConfig, key: any, params?: any) => any) | undefined" @@ -4745,7 +4760,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 145 + "lineNumber": 152 }, "signature": [ "TParam[]" @@ -4763,7 +4778,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 155 + "lineNumber": 162 }, "signature": [ "((aggConfig: TAggConfig) => TAggConfig[]) | (() => void | TAggConfig[])" @@ -4781,7 +4796,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 166 + "lineNumber": 173 }, "signature": [ "((aggConfig: TAggConfig) => TAggConfig[]) | (() => void | TAggConfig[])" @@ -4797,7 +4812,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 171 + "lineNumber": 178 }, "signature": [ "() => any" @@ -4815,7 +4830,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 184 + "lineNumber": 191 }, "signature": [ "(resp: any, aggConfigs: ", @@ -4857,7 +4872,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 202 + "lineNumber": 209 }, "signature": [ "(agg: TAggConfig) => ", @@ -4879,7 +4894,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 204 + "lineNumber": 211 }, "signature": [ "(agg: TAggConfig, bucket: any) => any" @@ -4893,7 +4908,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 206 + "lineNumber": 213 }, "signature": [ "((bucket: any, key: any, agg: TAggConfig) => any) | undefined" @@ -4913,7 +4928,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 208 + "lineNumber": 215 } } ], @@ -4924,7 +4939,7 @@ "label": "paramByName", "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 208 + "lineNumber": 215 }, "tags": [], "returnComment": [] @@ -4943,7 +4958,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 212 + "lineNumber": 219 } } ], @@ -4954,7 +4969,7 @@ "label": "getValueBucketPath", "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 212 + "lineNumber": 219 }, "tags": [], "returnComment": [] @@ -4997,7 +5012,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 225 + "lineNumber": 232 } } ], @@ -5008,13 +5023,13 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 225 + "lineNumber": 232 } } ], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 60 + "lineNumber": 61 }, "initialIsOpen": false }, @@ -6450,7 +6465,7 @@ "type": "Function", "label": "setField", "signature": [ - "(field: K, value: ", + "(field: K, value: ", { "pluginId": "data", "scope": "common", @@ -6514,7 +6529,7 @@ "type": "Function", "label": "removeField", "signature": [ - "(field: K) => this" + "(field: K) => this" ], "description": [ "\nremove field" @@ -6641,7 +6656,7 @@ "type": "Function", "label": "getField", "signature": [ - "(field: K, recurse?: boolean) => ", + "(field: K, recurse?: boolean) => ", { "pluginId": "data", "scope": "common", @@ -6694,7 +6709,7 @@ "type": "Function", "label": "getOwnField", "signature": [ - "(field: K) => ", + "(field: K) => ", { "pluginId": "data", "scope": "common", @@ -7625,6 +7640,23 @@ "returnComment": [], "initialIsOpen": false }, + { + "id": "def-common.aggFilteredMetric", + "type": "Function", + "children": [], + "signature": [ + "() => FunctionDefinition" + ], + "description": [], + "label": "aggFilteredMetric", + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.ts", + "lineNumber": 30 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + }, { "id": "def-common.aggFilters", "type": "Function", @@ -8457,6 +8489,76 @@ }, "initialIsOpen": false }, + { + "id": "def-common.filtersToAst", + "type": "Function", + "children": [ + { + "type": "CompoundType", + "label": "filters", + "isRequired": true, + "signature": [ + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + " | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/filters_to_ast.ts", + "lineNumber": 13 + } + } + ], + "signature": [ + "(filters: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + " | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]) => ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionAstExpressionBuilder", + "text": "ExpressionAstExpressionBuilder" + }, + "[]" + ], + "description": [], + "label": "filtersToAst", + "source": { + "path": "src/plugins/data/common/search/expressions/filters_to_ast.ts", + "lineNumber": 13 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + }, { "id": "def-common.functionWrapper", "type": "Function", @@ -8983,9 +9085,37 @@ { "id": "def-common.getFilterBucketAgg", "type": "Function", - "children": [], + "children": [ + { + "id": "def-common.getFilterBucketAgg.{-getConfig }", + "type": "Object", + "label": "{ getConfig }", + "tags": [], + "description": [], + "children": [ + { + "tags": [], + "id": "def-common.getFilterBucketAgg.{-getConfig }.getConfig", + "type": "Function", + "label": "getConfig", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/buckets/filter.ts", + "lineNumber": 27 + }, + "signature": [ + "(key: string) => any" + ] + } + ], + "source": { + "path": "src/plugins/data/common/search/aggs/buckets/filter.ts", + "lineNumber": 27 + } + } + ], "signature": [ - "() => ", + "({ getConfig }: { getConfig: (key: string) => any; }) => ", { "pluginId": "data", "scope": "common", @@ -9007,7 +9137,40 @@ "label": "getFilterBucketAgg", "source": { "path": "src/plugins/data/common/search/aggs/buckets/filter.ts", - "lineNumber": 24 + "lineNumber": 27 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + }, + { + "id": "def-common.getFilteredMetricAgg", + "type": "Function", + "children": [], + "signature": [ + "() => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.MetricAggType", + "text": "MetricAggType" + }, + "<", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.IMetricAggConfig", + "text": "IMetricAggConfig" + }, + ">" + ], + "description": [], + "label": "getFilteredMetricAgg", + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", + "lineNumber": 30 }, "tags": [], "returnComment": [], @@ -11475,7 +11638,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 38 + "lineNumber": 45 }, "signature": [ { @@ -11490,7 +11653,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_configs.ts", - "lineNumber": 37 + "lineNumber": 44 }, "initialIsOpen": false }, @@ -11511,7 +11674,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 204 + "lineNumber": 207 }, "signature": [ "FunctionDefinition" @@ -11525,7 +11688,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 205 + "lineNumber": 208 }, "signature": [ "FunctionDefinition" @@ -11539,7 +11702,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 206 + "lineNumber": 209 }, "signature": [ "FunctionDefinition" @@ -11553,7 +11716,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 207 + "lineNumber": 210 }, "signature": [ "FunctionDefinition" @@ -11567,7 +11730,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 208 + "lineNumber": 211 }, "signature": [ "FunctionDefinition" @@ -11581,7 +11744,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 209 + "lineNumber": 212 }, "signature": [ "FunctionDefinition" @@ -11595,7 +11758,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 210 + "lineNumber": 213 }, "signature": [ "FunctionDefinition" @@ -11609,7 +11772,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 211 + "lineNumber": 214 }, "signature": [ "FunctionDefinition" @@ -11623,7 +11786,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 212 + "lineNumber": 215 }, "signature": [ "FunctionDefinition" @@ -11637,7 +11800,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 213 + "lineNumber": 216 }, "signature": [ "FunctionDefinition" @@ -11651,7 +11814,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 214 + "lineNumber": 217 }, "signature": [ "FunctionDefinition" @@ -11665,7 +11828,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 215 + "lineNumber": 218 }, "signature": [ "FunctionDefinition" @@ -11679,7 +11842,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 216 + "lineNumber": 219 }, "signature": [ "FunctionDefinition" @@ -11693,7 +11856,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 217 + "lineNumber": 220 }, "signature": [ "FunctionDefinition" @@ -11707,7 +11870,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 218 + "lineNumber": 221 }, "signature": [ "FunctionDefinition" @@ -11721,7 +11884,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 219 + "lineNumber": 222 }, "signature": [ "FunctionDefinition" @@ -11729,13 +11892,13 @@ }, { "tags": [], - "id": "def-common.AggFunctionsMapping.aggCardinality", + "id": "def-common.AggFunctionsMapping.aggFilteredMetric", "type": "Object", - "label": "aggCardinality", + "label": "aggFilteredMetric", "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 220 + "lineNumber": 223 }, "signature": [ "FunctionDefinition" @@ -11743,13 +11906,27 @@ }, { "tags": [], - "id": "def-common.AggFunctionsMapping.aggCount", + "id": "def-common.AggFunctionsMapping.aggCardinality", "type": "Object", - "label": "aggCount", + "label": "aggCardinality", "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 221 + "lineNumber": 224 + }, + "signature": [ + "FunctionDefinition" + ] + }, + { + "tags": [], + "id": "def-common.AggFunctionsMapping.aggCount", + "type": "Object", + "label": "aggCount", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/types.ts", + "lineNumber": 225 }, "signature": [ "FunctionDefinition" @@ -11763,7 +11940,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 222 + "lineNumber": 226 }, "signature": [ "FunctionDefinition" @@ -11777,7 +11954,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 223 + "lineNumber": 227 }, "signature": [ "FunctionDefinition" @@ -11791,7 +11968,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 224 + "lineNumber": 228 }, "signature": [ "FunctionDefinition" @@ -11805,7 +11982,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 225 + "lineNumber": 229 }, "signature": [ "FunctionDefinition" @@ -11819,7 +11996,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 226 + "lineNumber": 230 }, "signature": [ "FunctionDefinition" @@ -11833,7 +12010,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 227 + "lineNumber": 231 }, "signature": [ "FunctionDefinition" @@ -11847,7 +12024,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 228 + "lineNumber": 232 }, "signature": [ "FunctionDefinition" @@ -11861,7 +12038,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 229 + "lineNumber": 233 }, "signature": [ "FunctionDefinition" @@ -11875,7 +12052,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 230 + "lineNumber": 234 }, "signature": [ "FunctionDefinition" @@ -11889,7 +12066,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 231 + "lineNumber": 235 }, "signature": [ "FunctionDefinition" @@ -11903,7 +12080,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 232 + "lineNumber": 236 }, "signature": [ "FunctionDefinition" @@ -11917,7 +12094,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 233 + "lineNumber": 237 }, "signature": [ "FunctionDefinition" @@ -11931,7 +12108,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 234 + "lineNumber": 238 }, "signature": [ "FunctionDefinition" @@ -11945,7 +12122,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 235 + "lineNumber": 239 }, "signature": [ "FunctionDefinition" @@ -11954,7 +12131,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 203 + "lineNumber": 206 }, "initialIsOpen": false }, @@ -12823,7 +13000,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/buckets/filter.ts", - "lineNumber": 21 + "lineNumber": 24 }, "signature": [ "Partial<{ top_left: GeoPoint; top_right: GeoPoint; bottom_right: GeoPoint; bottom_left: GeoPoint; }> | { wkt: string; } | GeoBox | undefined" @@ -12832,7 +13009,76 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/buckets/filter.ts", - "lineNumber": 20 + "lineNumber": 23 + }, + "initialIsOpen": false + }, + { + "id": "def-common.AggParamsFilteredMetric", + "type": "Interface", + "label": "AggParamsFilteredMetric", + "signature": [ + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.AggParamsFilteredMetric", + "text": "AggParamsFilteredMetric" + }, + " extends ", + "BaseAggParams" + ], + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.AggParamsFilteredMetric.customMetric", + "type": "Object", + "label": "customMetric", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", + "lineNumber": 18 + }, + "signature": [ + "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.SerializableState", + "text": "SerializableState" + }, + " | undefined; schema?: string | undefined; } | undefined" + ] + }, + { + "tags": [], + "id": "def-common.AggParamsFilteredMetric.customBucket", + "type": "Object", + "label": "customBucket", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", + "lineNumber": 19 + }, + "signature": [ + "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.SerializableState", + "text": "SerializableState" + }, + " | undefined; schema?: string | undefined; } | undefined" + ] + } + ], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", + "lineNumber": 17 }, "initialIsOpen": false }, @@ -14347,6 +14593,20 @@ "boolean | undefined" ] }, + { + "tags": [], + "id": "def-common.AggTypeConfig.hasNoDslParams", + "type": "CompoundType", + "label": "hasNoDslParams", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/agg_type.ts", + "lineNumber": 35 + }, + "signature": [ + "boolean | undefined" + ] + }, { "tags": [], "id": "def-common.AggTypeConfig.params", @@ -14355,7 +14615,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 35 + "lineNumber": 36 }, "signature": [ "Partial[] | undefined" @@ -14369,7 +14629,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 36 + "lineNumber": 37 }, "signature": [ "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"ip\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"conflict\" | \"nested\" | \"histogram\" | \"null\" | undefined" @@ -14383,7 +14643,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 37 + "lineNumber": 38 }, "signature": [ "((aggConfig: TAggConfig) => TAggConfig[]) | (() => void | TAggConfig[]) | undefined" @@ -14397,7 +14657,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 38 + "lineNumber": 39 }, "signature": [ "((aggConfig: TAggConfig) => TAggConfig[]) | (() => void | TAggConfig[]) | undefined" @@ -14411,7 +14671,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 39 + "lineNumber": 40 }, "signature": [ "boolean | undefined" @@ -14425,7 +14685,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 40 + "lineNumber": 41 }, "signature": [ "boolean | undefined" @@ -14439,7 +14699,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 41 + "lineNumber": 42 }, "signature": [ "(() => any) | undefined" @@ -14453,7 +14713,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 42 + "lineNumber": 43 }, "signature": [ "((resp: any, aggConfigs: ", @@ -14491,7 +14751,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 51 + "lineNumber": 52 }, "signature": [ "((agg: TAggConfig) => ", @@ -14513,7 +14773,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 52 + "lineNumber": 53 }, "signature": [ "((agg: TAggConfig, bucket: any) => any) | undefined" @@ -14527,7 +14787,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 53 + "lineNumber": 54 }, "signature": [ "((bucket: any, key: any, agg: TAggConfig) => any) | undefined" @@ -14541,7 +14801,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 54 + "lineNumber": 55 }, "signature": [ "((agg: TAggConfig) => string) | undefined" @@ -17926,6 +18186,21 @@ ], "initialIsOpen": false }, + { + "tags": [], + "id": "def-common.aggFilteredMetricFnName", + "type": "string", + "label": "aggFilteredMetricFnName", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.ts", + "lineNumber": 14 + }, + "signature": [ + "\"aggFilteredMetric\"" + ], + "initialIsOpen": false + }, { "tags": [], "id": "def-common.aggFilterFnName", @@ -18223,7 +18498,7 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/types.ts", - "lineNumber": 139 + "lineNumber": 141 }, "signature": [ "{ calculateAutoTimeExpression: (range: TimeRange) => string | undefined; getDateMetaByDatatableColumn: (column: DatatableColumn) => Promise<{ timeZone: string; timeRange?: TimeRange | undefined; interval: string; } | undefined>; datatableUtilities: { getIndexPattern: (column: DatatableColumn) => Promise; getAggConfig: (column: DatatableColumn) => Promise; isFilterable: (column: DatatableColumn) => boolean; }; createAggConfigs: (indexPattern: IndexPattern, configStates?: Pick | null, object, ", - { - "pluginId": "expressions", - "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExpressionValueBoxed", - "text": "ExpressionValueBoxed" - }, - "<\"kibana_context\", ExecutionContextSearch>, ExecutionContext>" - ], - "initialIsOpen": false - }, - { - "id": "def-common.ExpressionFunctionKibanaContext", + "id": "def-common.ExpressionFunctionExistsFilter", "type": "Type", - "label": "ExpressionFunctionKibanaContext", + "label": "ExpressionFunctionExistsFilter", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 25 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 20 }, "signature": [ - "ExpressionFunctionDefinition<\"kibana_context\", ", + "ExpressionFunctionDefinition<\"existsFilter\", null, Arguments, ", { "pluginId": "expressions", "scope": "common", @@ -18517,30 +18761,15 @@ "section": "def-common.ExpressionValueBoxed", "text": "ExpressionValueBoxed" }, - "<\"kibana_context\", ExecutionContextSearch> | null, Arguments, Promise<", + "<\"kibana_filter\", ", { - "pluginId": "expressions", + "pluginId": "data", "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExpressionValueBoxed", - "text": "ExpressionValueBoxed" + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" }, - "<\"kibana_context\", ExecutionContextSearch>>, ExecutionContext>" - ], - "initialIsOpen": false - }, - { - "id": "def-common.ExpressionFunctionKibanaTimerange", - "type": "Type", - "label": "ExpressionFunctionKibanaTimerange", - "tags": [], - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 15 - }, - "signature": [ - "ExpressionFunctionDefinition<\"timerange\", null, TimeRange, ExpressionValueBoxed<\"timerange\", TimeRange>, ", + ">, ", { "pluginId": "expressions", "scope": "common", @@ -18557,23 +18786,22 @@ "text": "Adapters" }, ", ", - "SerializableState", - ">>" + "SerializableState" ], "initialIsOpen": false }, { - "id": "def-common.ExpressionFunctionKql", + "id": "def-common.ExpressionFunctionField", "type": "Type", - "label": "ExpressionFunctionKql", + "label": "ExpressionFunctionField", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 17 + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 19 }, "signature": [ - "ExpressionFunctionDefinition<\"kql\", null, Arguments, ", + "ExpressionFunctionDefinition<\"field\", null, Arguments, ", { "pluginId": "expressions", "scope": "common", @@ -18581,13 +18809,13 @@ "section": "def-common.ExpressionValueBoxed", "text": "ExpressionValueBoxed" }, - "<\"kibana_query\", ", + "<\"kibana_field\", ", { "pluginId": "data", - "scope": "common", + "scope": "public", "docId": "kibDataPluginApi", - "section": "def-common.Query", - "text": "Query" + "section": "def-public.IndexPatternField", + "text": "IndexPatternField" }, ">, ", { @@ -18611,17 +18839,17 @@ "initialIsOpen": false }, { - "id": "def-common.ExpressionFunctionLucene", + "id": "def-common.ExpressionFunctionKibana", "type": "Type", - "label": "ExpressionFunctionLucene", + "label": "ExpressionFunctionKibana", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", + "path": "src/plugins/data/common/search/expressions/kibana.ts", "lineNumber": 17 }, "signature": [ - "ExpressionFunctionDefinition<\"lucene\", null, Arguments, ", + "ExpressionFunctionDefinition<\"kibana\", ", { "pluginId": "expressions", "scope": "common", @@ -18629,193 +18857,513 @@ "section": "def-common.ExpressionValueBoxed", "text": "ExpressionValueBoxed" }, - "<\"kibana_query\", ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.Query", - "text": "Query" - }, - ">, ", + "<\"kibana_context\", ExecutionContextSearch> | null, object, ", { "pluginId": "expressions", "scope": "common", "docId": "kibExpressionsPluginApi", - "section": "def-common.ExecutionContext", - "text": "ExecutionContext" + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" }, - "<", - { - "pluginId": "inspector", - "scope": "common", - "docId": "kibInspectorPluginApi", - "section": "def-common.Adapters", - "text": "Adapters" - }, - ", ", - "SerializableState" + "<\"kibana_context\", ExecutionContextSearch>, ExecutionContext>" ], "initialIsOpen": false }, { - "id": "def-common.ExpressionValueSearchContext", + "id": "def-common.ExpressionFunctionKibanaContext", "type": "Type", - "label": "ExpressionValueSearchContext", + "label": "ExpressionFunctionKibanaContext", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 20 + "path": "src/plugins/data/common/search/expressions/kibana_context.ts", + "lineNumber": 26 }, "signature": [ - "{ type: \"kibana_context\"; } & ExecutionContextSearch" + "ExpressionFunctionDefinition<\"kibana_context\", ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_context\", ExecutionContextSearch> | null, Arguments, Promise<", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_context\", ExecutionContextSearch>>, ExecutionContext>" ], "initialIsOpen": false }, { - "id": "def-common.FieldTypes", + "id": "def-common.ExpressionFunctionKibanaFilter", "type": "Type", - "label": "FieldTypes", + "label": "ExpressionFunctionKibanaFilter", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/param_types/field.ts", - "lineNumber": 19 + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 18 }, "signature": [ + "ExpressionFunctionDefinition<\"kibanaFilter\", null, Arguments, ", { - "pluginId": "data", + "pluginId": "expressions", "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.KBN_FIELD_TYPES", - "text": "KBN_FIELD_TYPES" + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" }, - "._SOURCE | ", + "<\"kibana_filter\", ", { "pluginId": "data", - "scope": "common", + "scope": "public", "docId": "kibDataPluginApi", - "section": "def-common.KBN_FIELD_TYPES", - "text": "KBN_FIELD_TYPES" + "section": "def-public.Filter", + "text": "Filter" }, - ".ATTACHMENT | ", + ">, ", { - "pluginId": "data", + "pluginId": "expressions", "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.KBN_FIELD_TYPES", - "text": "KBN_FIELD_TYPES" + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" }, - ".BOOLEAN | ", + "<", { - "pluginId": "data", + "pluginId": "inspector", "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.KBN_FIELD_TYPES", - "text": "KBN_FIELD_TYPES" + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" }, - ".DATE | ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.KBN_FIELD_TYPES", - "text": "KBN_FIELD_TYPES" - } + ", ", + "SerializableState" ], "initialIsOpen": false }, { - "id": "def-common.IAggConfig", + "id": "def-common.ExpressionFunctionKibanaTimerange", "type": "Type", - "label": "IAggConfig", - "tags": [ - "name", - "description" - ], + "label": "ExpressionFunctionKibanaTimerange", + "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 53 + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 15 }, "signature": [ - "AggConfig" + "ExpressionFunctionDefinition<\"timerange\", null, TimeRange, ExpressionValueBoxed<\"timerange\", TimeRange>, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + "SerializableState", + ">>" ], "initialIsOpen": false }, { - "id": "def-common.IAggType", + "id": "def-common.ExpressionFunctionKql", "type": "Type", - "label": "IAggType", + "label": "ExpressionFunctionKql", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/agg_type.ts", - "lineNumber": 58 + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 17 }, "signature": [ - "AggType>" + "ExpressionFunctionDefinition<\"kql\", null, Arguments, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_query\", ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Query", + "text": "Query" + }, + ">, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + "SerializableState" ], "initialIsOpen": false }, { - "id": "def-common.IEsSearchResponse", + "id": "def-common.ExpressionFunctionLucene", "type": "Type", - "label": "IEsSearchResponse", + "label": "ExpressionFunctionLucene", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/es_search/types.ts", - "lineNumber": 23 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 17 }, "signature": [ - "IKibanaSearchResponse>" + "ExpressionFunctionDefinition<\"lucene\", null, Arguments, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_query\", ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Query", + "text": "Query" + }, + ">, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + "SerializableState" ], "initialIsOpen": false }, { - "id": "def-common.IFieldParamType", + "id": "def-common.ExpressionFunctionPhraseFilter", "type": "Type", - "label": "IFieldParamType", + "label": "ExpressionFunctionPhraseFilter", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/param_types/field.ts", + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", "lineNumber": 21 }, "signature": [ - "FieldParamType" + "ExpressionFunctionDefinition<\"rangeFilter\", null, Arguments, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_filter\", ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + ">, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + "SerializableState" ], "initialIsOpen": false }, { - "id": "def-common.IMetricAggType", + "id": "def-common.ExpressionFunctionRange", "type": "Type", - "label": "IMetricAggType", - "tags": [], - "description": [], - "source": { - "path": "src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts", - "lineNumber": 35 - }, - "signature": [ - "MetricAggType" - ], - "initialIsOpen": false - }, - { + "label": "ExpressionFunctionRange", "tags": [], - "id": "def-common.intervalOptions", - "type": "Array", - "label": "intervalOptions", "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/buckets/_interval_options.ts", - "lineNumber": 15 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 21 }, "signature": [ - "({ display: string; val: string; enabled(agg: ", + "ExpressionFunctionDefinition<\"range\", null, Arguments, ExpressionValueBoxed<\"kibana_range\", Arguments>, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + "SerializableState", + ">>" + ], + "initialIsOpen": false + }, + { + "id": "def-common.ExpressionFunctionRangeFilter", + "type": "Type", + "label": "ExpressionFunctionRangeFilter", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 22 + }, + "signature": [ + "ExpressionFunctionDefinition<\"rangeFilter\", null, Arguments, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_filter\", ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + ">, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + "SerializableState" + ], + "initialIsOpen": false + }, + { + "id": "def-common.ExpressionValueSearchContext", + "type": "Type", + "label": "ExpressionValueSearchContext", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 21 + }, + "signature": [ + "{ type: \"kibana_context\"; } & ExecutionContextSearch" + ], + "initialIsOpen": false + }, + { + "id": "def-common.FieldTypes", + "type": "Type", + "label": "FieldTypes", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/param_types/field.ts", + "lineNumber": 19 + }, + "signature": [ + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.KBN_FIELD_TYPES", + "text": "KBN_FIELD_TYPES" + }, + "._SOURCE | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.KBN_FIELD_TYPES", + "text": "KBN_FIELD_TYPES" + }, + ".ATTACHMENT | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.KBN_FIELD_TYPES", + "text": "KBN_FIELD_TYPES" + }, + ".BOOLEAN | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.KBN_FIELD_TYPES", + "text": "KBN_FIELD_TYPES" + }, + ".DATE | ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.KBN_FIELD_TYPES", + "text": "KBN_FIELD_TYPES" + } + ], + "initialIsOpen": false + }, + { + "id": "def-common.IAggConfig", + "type": "Type", + "label": "IAggConfig", + "tags": [ + "name", + "description" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/agg_config.ts", + "lineNumber": 53 + }, + "signature": [ + "AggConfig" + ], + "initialIsOpen": false + }, + { + "id": "def-common.IAggType", + "type": "Type", + "label": "IAggType", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/agg_type.ts", + "lineNumber": 59 + }, + "signature": [ + "AggType>" + ], + "initialIsOpen": false + }, + { + "id": "def-common.IEsSearchResponse", + "type": "Type", + "label": "IEsSearchResponse", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/es_search/types.ts", + "lineNumber": 23 + }, + "signature": [ + "IKibanaSearchResponse>" + ], + "initialIsOpen": false + }, + { + "id": "def-common.IFieldParamType", + "type": "Type", + "label": "IFieldParamType", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/param_types/field.ts", + "lineNumber": 21 + }, + "signature": [ + "FieldParamType" + ], + "initialIsOpen": false + }, + { + "id": "def-common.IMetricAggType", + "type": "Type", + "label": "IMetricAggType", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts", + "lineNumber": 35 + }, + "signature": [ + "MetricAggType" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.intervalOptions", + "type": "Array", + "label": "intervalOptions", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/buckets/_interval_options.ts", + "lineNumber": 15 + }, + "signature": [ + "({ display: string; val: string; enabled(agg: ", { "pluginId": "data", "scope": "common", @@ -19037,7 +19585,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 28 + "lineNumber": 31 }, "signature": [ "\"kibana_context\"" @@ -19052,7 +19600,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 29 + "lineNumber": 32 }, "signature": [ "{ type: \"kibana_context\"; } & ExecutionContextSearch" @@ -19060,65 +19608,110 @@ "initialIsOpen": false }, { - "id": "def-common.KibanaQueryOutput", + "id": "def-common.KibanaField", "type": "Type", - "label": "KibanaQueryOutput", + "label": "KibanaField", "tags": [], "description": [], "source": { "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 25 + "lineNumber": 28 }, "signature": [ - "{ type: \"kibana_query\"; } & Query" + "{ type: \"kibana_field\"; } & IndexPatternField" ], "initialIsOpen": false }, { - "id": "def-common.KibanaTimerangeOutput", + "id": "def-common.KibanaFilter", "type": "Type", - "label": "KibanaTimerangeOutput", + "label": "KibanaFilter", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 13 + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 27 }, "signature": [ - "{ type: \"timerange\"; } & TimeRange" + "{ type: \"kibana_filter\"; } & Filter" ], "initialIsOpen": false }, { + "id": "def-common.KibanaQueryOutput", + "type": "Type", + "label": "KibanaQueryOutput", "tags": [], - "id": "def-common.parentPipelineType", - "type": "string", - "label": "parentPipelineType", "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", "lineNumber": 26 }, + "signature": [ + "{ type: \"kibana_query\"; } & Query" + ], "initialIsOpen": false }, { - "id": "def-common.ParsedInterval", + "id": "def-common.KibanaRange", "type": "Type", - "label": "ParsedInterval", + "label": "KibanaRange", "tags": [], "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts", - "lineNumber": 18 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 19 }, "signature": [ - "{ value: number; unit: Unit; type: \"calendar\" | \"fixed\"; }" + "{ type: \"kibana_range\"; } & Arguments" ], "initialIsOpen": false }, { + "id": "def-common.KibanaTimerangeOutput", + "type": "Type", + "label": "KibanaTimerangeOutput", "tags": [], - "id": "def-common.SEARCH_SESSION_TYPE", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 13 + }, + "signature": [ + "{ type: \"timerange\"; } & TimeRange" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.parentPipelineType", + "type": "string", + "label": "parentPipelineType", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "lineNumber": 27 + }, + "initialIsOpen": false + }, + { + "id": "def-common.ParsedInterval", + "type": "Type", + "label": "ParsedInterval", + "tags": [], + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts", + "lineNumber": 18 + }, + "signature": [ + "{ value: number; unit: Unit; type: \"calendar\" | \"fixed\"; }" + ], + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.SEARCH_SESSION_TYPE", "type": "string", "label": "SEARCH_SESSION_TYPE", "description": [], @@ -19169,7 +19762,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", - "lineNumber": 33 + "lineNumber": 34 }, "initialIsOpen": false }, @@ -19253,634 +19846,2109 @@ "initialIsOpen": false }, { - "id": "def-common.kibana", + "id": "def-common.existsFilterFunction", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibana.name", + "id": "def-common.existsFilterFunction.name", "type": "string", "label": "name", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 27 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 28 }, "signature": [ - "\"kibana\"" + "\"existsFilter\"" ] }, { "tags": [], - "id": "def-common.kibana.type", + "id": "def-common.existsFilterFunction.type", "type": "string", "label": "type", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 28 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 29 }, "signature": [ - "\"kibana_context\"" + "\"kibana_filter\"" ] }, { "tags": [], - "id": "def-common.kibana.inputTypes", + "id": "def-common.existsFilterFunction.inputTypes", "type": "Array", "label": "inputTypes", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", "lineNumber": 30 }, "signature": [ - "(\"kibana_context\" | \"null\")[]" + "\"null\"[]" ] }, { "tags": [], - "id": "def-common.kibana.help", + "id": "def-common.existsFilterFunction.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 32 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 31 } }, { - "id": "def-common.kibana.args", + "id": "def-common.existsFilterFunction.args", "type": "Object", "tags": [], - "children": [], + "children": [ + { + "id": "def-common.existsFilterFunction.args.field", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.existsFilterFunction.args.field.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 36 + }, + "signature": [ + "\"kibana_field\"[]" + ] + }, + { + "tags": [], + "id": "def-common.existsFilterFunction.args.field.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 37 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.existsFilterFunction.args.field.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 38 + } + } + ], + "description": [], + "label": "field", + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 35 + } + }, + { + "id": "def-common.existsFilterFunction.args.negate", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.existsFilterFunction.args.negate.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 43 + }, + "signature": [ + "\"boolean\"[]" + ] + }, + { + "tags": [], + "id": "def-common.existsFilterFunction.args.negate.default", + "type": "boolean", + "label": "default", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 44 + }, + "signature": [ + "false" + ] + }, + { + "tags": [], + "id": "def-common.existsFilterFunction.args.negate.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 45 + } + } + ], + "description": [], + "label": "negate", + "source": { + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 42 + } + } + ], "description": [], "label": "args", "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 36 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 34 } }, { - "id": "def-common.kibana.fn", + "id": "def-common.existsFilterFunction.fn", "type": "Function", "label": "fn", "signature": [ - "(input: Input, _: object, { getSearchContext }: ", - { - "pluginId": "expressions", - "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExecutionContext", - "text": "ExecutionContext" - }, - "<", - { - "pluginId": "inspector", - "scope": "common", - "docId": "kibInspectorPluginApi", - "section": "def-common.Adapters", - "text": "Adapters" - }, - ", ", + "(input: null, args: Arguments) => { $state?: ", { "pluginId": "data", "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.ExecutionContextSearch", - "text": "ExecutionContextSearch" - }, - ">) => ", - { - "pluginId": "expressions", - "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExpressionValueBoxed", - "text": "ExpressionValueBoxed" + "docId": "kibDataPluginApi", + "section": "def-common.FilterState", + "text": "FilterState" }, - "<\"kibana_context\", ", + " | undefined; meta: ", { "pluginId": "data", "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.ExecutionContextSearch", - "text": "ExecutionContextSearch" - } + "docId": "kibDataPluginApi", + "section": "def-common.FilterMeta", + "text": "FilterMeta" + }, + "; query?: any; type: \"kibana_filter\"; }" ], "description": [], "children": [ - { - "type": "CompoundType", - "label": "input", - "isRequired": false, - "signature": [ - "Input" - ], - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 38 - } - }, { "type": "Uncategorized", - "label": "_", + "label": "input", "isRequired": true, "signature": [ - "object" + "null" ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 38 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 51 } }, { "type": "Object", - "label": "{ getSearchContext }", + "label": "args", "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" - }, - ">" + "Arguments" ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 38 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 51 } } ], "tags": [], "returnComment": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 38 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 51 } } ], "description": [], - "label": "kibana", + "label": "existsFilterFunction", "source": { - "path": "src/plugins/data/common/search/expressions/kibana.ts", - "lineNumber": 26 + "path": "src/plugins/data/common/search/expressions/exists_filter.ts", + "lineNumber": 27 }, "initialIsOpen": false }, { - "id": "def-common.kibanaContext", + "id": "def-common.fieldFunction", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaContext.name", + "id": "def-common.fieldFunction.name", "type": "string", "label": "name", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 32 - } + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 27 + }, + "signature": [ + "\"field\"" + ] }, { - "id": "def-common.kibanaContext.from", - "type": "Object", "tags": [], - "children": [ - { + "id": "def-common.fieldFunction.type", + "type": "string", + "label": "type", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 28 + }, + "signature": [ + "\"kibana_field\"" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.inputTypes", + "type": "Array", + "label": "inputTypes", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 29 + }, + "signature": [ + "\"null\"[]" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 30 + } + }, + { + "id": "def-common.fieldFunction.args", + "type": "Object", + "tags": [], + "children": [ + { + "id": "def-common.fieldFunction.args.name", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.fieldFunction.args.name.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 35 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.args.name.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 36 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.args.name.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 37 + } + } + ], + "description": [], + "label": "name", + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 34 + } + }, + { + "id": "def-common.fieldFunction.args.type", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.fieldFunction.args.type.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 42 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.args.type.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 43 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.args.type.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 44 + } + } + ], + "description": [], + "label": "type", + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 41 + } + }, + { + "id": "def-common.fieldFunction.args.script", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.fieldFunction.args.script.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 49 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.fieldFunction.args.script.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 50 + } + } + ], + "description": [], + "label": "script", + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 48 + } + } + ], + "description": [], + "label": "args", + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 33 + } + }, + { + "id": "def-common.fieldFunction.fn", + "type": "Function", + "label": "fn", + "signature": [ + "(input: null, args: Arguments) => ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_field\", ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataIndexPatternsPluginApi", + "section": "def-common.IndexPatternField", + "text": "IndexPatternField" + }, + ">" + ], + "description": [], + "children": [ + { + "type": "Uncategorized", + "label": "input", + "isRequired": true, + "signature": [ + "null" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 56 + } + }, + { + "type": "Object", + "label": "args", + "isRequired": true, + "signature": [ + "Arguments" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 56 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 56 + } + } + ], + "description": [], + "label": "fieldFunction", + "source": { + "path": "src/plugins/data/common/search/expressions/field.ts", + "lineNumber": 26 + }, + "initialIsOpen": false + }, + { + "id": "def-common.kibana", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibana.name", + "type": "string", + "label": "name", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 27 + }, + "signature": [ + "\"kibana\"" + ] + }, + { + "tags": [], + "id": "def-common.kibana.type", + "type": "string", + "label": "type", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 28 + }, + "signature": [ + "\"kibana_context\"" + ] + }, + { + "tags": [], + "id": "def-common.kibana.inputTypes", + "type": "Array", + "label": "inputTypes", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 30 + }, + "signature": [ + "(\"kibana_context\" | \"null\")[]" + ] + }, + { + "tags": [], + "id": "def-common.kibana.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 32 + } + }, + { + "id": "def-common.kibana.args", + "type": "Object", + "tags": [], + "children": [], + "description": [], + "label": "args", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 36 + } + }, + { + "id": "def-common.kibana.fn", + "type": "Function", + "label": "fn", + "signature": [ + "(input: Input, _: object, { getSearchContext }: ", + { + "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" + }, + ">) => ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"kibana_context\", ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.ExecutionContextSearch", + "text": "ExecutionContextSearch" + } + ], + "description": [], + "children": [ + { + "type": "CompoundType", + "label": "input", + "isRequired": false, + "signature": [ + "Input" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 38 + } + }, + { + "type": "Uncategorized", + "label": "_", + "isRequired": true, + "signature": [ + "object" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 38 + } + }, + { + "type": "Object", + "label": "{ getSearchContext }", + "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.ts", + "lineNumber": 38 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 38 + } + } + ], + "description": [], + "label": "kibana", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana.ts", + "lineNumber": 26 + }, + "initialIsOpen": false + }, + { + "id": "def-common.kibanaContext", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaContext.name", + "type": "string", + "label": "name", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 35 + } + }, + { + "id": "def-common.kibanaContext.from", + "type": "Object", + "tags": [], + "children": [ + { "id": "def-common.kibanaContext.from.null", "type": "Function", "children": [], "signature": [ - "() => { type: string; }" + "() => { type: string; }" + ], + "description": [], + "label": "null", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 37 + }, + "tags": [], + "returnComment": [] + } + ], + "description": [], + "label": "from", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 36 + } + }, + { + "id": "def-common.kibanaContext.to", + "type": "Object", + "tags": [], + "children": [ + { + "id": "def-common.kibanaContext.to.null", + "type": "Function", + "children": [], + "signature": [ + "() => { type: string; }" + ], + "description": [], + "label": "null", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 44 + }, + "tags": [], + "returnComment": [] + } + ], + "description": [], + "label": "to", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 43 + } + } + ], + "description": [], + "label": "kibanaContext", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", + "lineNumber": 34 + }, + "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", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaFilterFunction.name", + "type": "string", + "label": "name", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 26 + }, + "signature": [ + "\"kibanaFilter\"" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.type", + "type": "string", + "label": "type", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 27 + }, + "signature": [ + "\"kibana_filter\"" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.inputTypes", + "type": "Array", + "label": "inputTypes", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 28 + }, + "signature": [ + "\"null\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 29 + } + }, + { + "id": "def-common.kibanaFilterFunction.args", + "type": "Object", + "tags": [], + "children": [ + { + "id": "def-common.kibanaFilterFunction.args.query", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.query.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 34 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.query.aliases", + "type": "Array", + "label": "aliases", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 35 + }, + "signature": [ + "string[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.query.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 36 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.query.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 37 + } + } + ], + "description": [], + "label": "query", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 33 + } + }, + { + "id": "def-common.kibanaFilterFunction.args.negate", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.negate.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 42 + }, + "signature": [ + "\"boolean\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.negate.default", + "type": "boolean", + "label": "default", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 43 + }, + "signature": [ + "false" + ] + }, + { + "tags": [], + "id": "def-common.kibanaFilterFunction.args.negate.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 44 + } + } + ], + "description": [], + "label": "negate", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 41 + } + } + ], + "description": [], + "label": "args", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 32 + } + }, + { + "id": "def-common.kibanaFilterFunction.fn", + "type": "Function", + "label": "fn", + "signature": [ + "(input: null, args: Arguments) => any" + ], + "description": [], + "children": [ + { + "type": "Uncategorized", + "label": "input", + "isRequired": true, + "signature": [ + "null" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 50 + } + }, + { + "type": "Object", + "label": "args", + "isRequired": true, + "signature": [ + "Arguments" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 50 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 50 + } + } + ], + "description": [], + "label": "kibanaFilterFunction", + "source": { + "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", + "lineNumber": 25 + }, + "initialIsOpen": false + }, + { + "id": "def-common.kibanaTimerangeFunction", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.name", + "type": "string", + "label": "name", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 23 + }, + "signature": [ + "\"timerange\"" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.type", + "type": "string", + "label": "type", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 24 + }, + "signature": [ + "\"timerange\"" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.inputTypes", + "type": "Array", + "label": "inputTypes", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 25 + }, + "signature": [ + "\"null\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 26 + } + }, + { + "id": "def-common.kibanaTimerangeFunction.args", + "type": "Object", + "tags": [], + "children": [ + { + "id": "def-common.kibanaTimerangeFunction.args.from", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.from.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 31 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.from.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 32 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.from.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 33 + } + } + ], + "description": [], + "label": "from", + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 30 + } + }, + { + "id": "def-common.kibanaTimerangeFunction.args.to", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.to.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 38 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.to.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 39 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.to.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 40 + } + } + ], + "description": [], + "label": "to", + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 37 + } + }, + { + "id": "def-common.kibanaTimerangeFunction.args.mode", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.mode.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 45 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.mode.options", + "type": "Array", + "label": "options", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 46 + }, + "signature": [ + "(\"absolute\" | \"relative\")[]" + ] + }, + { + "tags": [], + "id": "def-common.kibanaTimerangeFunction.args.mode.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 47 + } + } + ], + "description": [], + "label": "mode", + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 44 + } + } + ], + "description": [], + "label": "args", + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 29 + } + }, + { + "id": "def-common.kibanaTimerangeFunction.fn", + "type": "Function", + "label": "fn", + "signature": [ + "(input: null, args: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataQueryPluginApi", + "section": "def-common.TimeRange", + "text": "TimeRange" + }, + ") => { type: \"timerange\"; from: string; to: string; mode: \"absolute\" | \"relative\" | undefined; }" + ], + "description": [], + "children": [ + { + "type": "Uncategorized", + "label": "input", + "isRequired": true, + "signature": [ + "null" ], "description": [], - "label": "null", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 34 - }, + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 53 + } + }, + { + "type": "Object", + "label": "args", + "isRequired": true, + "signature": [ + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataQueryPluginApi", + "section": "def-common.TimeRange", + "text": "TimeRange" + } + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 53 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 53 + } + } + ], + "description": [], + "label": "kibanaTimerangeFunction", + "source": { + "path": "src/plugins/data/common/search/expressions/timerange.ts", + "lineNumber": 22 + }, + "initialIsOpen": false + }, + { + "id": "def-common.kqlFunction", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.kqlFunction.name", + "type": "string", + "label": "name", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 25 + }, + "signature": [ + "\"kql\"" + ] + }, + { + "tags": [], + "id": "def-common.kqlFunction.type", + "type": "string", + "label": "type", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 26 + }, + "signature": [ + "\"kibana_query\"" + ] + }, + { + "tags": [], + "id": "def-common.kqlFunction.inputTypes", + "type": "Array", + "label": "inputTypes", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 27 + }, + "signature": [ + "\"null\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kqlFunction.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 28 + } + }, + { + "id": "def-common.kqlFunction.args", + "type": "Object", + "tags": [], + "children": [ + { + "id": "def-common.kqlFunction.args.q", + "type": "Object", "tags": [], - "returnComment": [] + "children": [ + { + "tags": [], + "id": "def-common.kqlFunction.args.q.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 33 + }, + "signature": [ + "\"string\"[]" + ] + }, + { + "tags": [], + "id": "def-common.kqlFunction.args.q.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 34 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.kqlFunction.args.q.aliases", + "type": "Array", + "label": "aliases", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 35 + }, + "signature": [ + "string[]" + ] + }, + { + "tags": [], + "id": "def-common.kqlFunction.args.q.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 36 + } + } + ], + "description": [], + "label": "q", + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 32 + } } ], "description": [], - "label": "from", + "label": "args", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 33 + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 31 } }, { - "id": "def-common.kibanaContext.to", - "type": "Object", - "tags": [], + "id": "def-common.kqlFunction.fn", + "type": "Function", + "label": "fn", + "signature": [ + "(input: null, args: Arguments) => { type: \"kibana_query\"; language: string; query: string; }" + ], + "description": [], "children": [ { - "id": "def-common.kibanaContext.to.null", - "type": "Function", - "children": [], + "type": "Uncategorized", + "label": "input", + "isRequired": true, "signature": [ - "() => { type: string; }" + "null" ], "description": [], - "label": "null", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 41 - }, - "tags": [], - "returnComment": [] + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 42 + } + }, + { + "type": "Object", + "label": "args", + "isRequired": true, + "signature": [ + "Arguments" + ], + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 42 + } } ], - "description": [], - "label": "to", + "tags": [], + "returnComment": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 40 + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 42 } } ], "description": [], - "label": "kibanaContext", + "label": "kqlFunction", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context_type.ts", - "lineNumber": 31 + "path": "src/plugins/data/common/search/expressions/kql.ts", + "lineNumber": 24 }, "initialIsOpen": false }, { - "id": "def-common.kibanaContextFunction", + "id": "def-common.luceneFunction", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaContextFunction.name", + "id": "def-common.luceneFunction.name", "type": "string", "label": "name", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 43 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 25 }, "signature": [ - "\"kibana_context\"" + "\"lucene\"" ] }, { "tags": [], - "id": "def-common.kibanaContextFunction.type", + "id": "def-common.luceneFunction.type", "type": "string", "label": "type", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 44 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 26 }, "signature": [ - "\"kibana_context\"" + "\"kibana_query\"" ] }, { "tags": [], - "id": "def-common.kibanaContextFunction.inputTypes", + "id": "def-common.luceneFunction.inputTypes", "type": "Array", "label": "inputTypes", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 45 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 27 }, "signature": [ - "(\"kibana_context\" | \"null\")[]" + "\"null\"[]" ] }, { "tags": [], - "id": "def-common.kibanaContextFunction.help", + "id": "def-common.luceneFunction.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 46 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 28 } }, { - "id": "def-common.kibanaContextFunction.args", + "id": "def-common.luceneFunction.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": 51 - }, - "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": 52 - }, - "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": 53 - }, - "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": 54 - } - } - ], - "description": [], - "label": "q", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 50 - } - }, - { - "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": 59 - }, - "signature": [ - "(\"string\" | \"null\")[]" - ] - }, - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.filters.default", - "type": "string", - "label": "default", - "description": [], - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 60 - } - }, - { - "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": 61 - } - } - ], - "description": [], - "label": "filters", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 58 - } - }, - { - "id": "def-common.kibanaContextFunction.args.timeRange", + "id": "def-common.luceneFunction.args.q", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaContextFunction.args.timeRange.types", + "id": "def-common.luceneFunction.args.q.types", "type": "Array", "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 66 - }, - "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": 67 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 33 }, "signature": [ - "null" + "\"string\"[]" ] }, { "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": 68 - } - } - ], - "description": [], - "label": "timeRange", - "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 65 - } - }, - { - "id": "def-common.kibanaContextFunction.args.savedSearchId", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.kibanaContextFunction.args.savedSearchId.types", - "type": "Array", - "label": "types", + "id": "def-common.luceneFunction.args.q.required", + "type": "boolean", + "label": "required", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 73 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 34 }, "signature": [ - "(\"string\" | \"null\")[]" + "true" ] }, { "tags": [], - "id": "def-common.kibanaContextFunction.args.savedSearchId.default", - "type": "Uncategorized", - "label": "default", + "id": "def-common.luceneFunction.args.q.aliases", + "type": "Array", + "label": "aliases", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 74 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 35 }, "signature": [ - "null" + "string[]" ] }, { "tags": [], - "id": "def-common.kibanaContextFunction.args.savedSearchId.help", + "id": "def-common.luceneFunction.args.q.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 75 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 36 } } ], "description": [], - "label": "savedSearchId", + "label": "q", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 72 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 32 } } ], "description": [], "label": "args", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 49 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 31 } }, { - "id": "def-common.kibanaContextFunction.fn", + "id": "def-common.luceneFunction.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" - } + "(input: null, args: Arguments) => { type: \"kibana_query\"; language: string; query: any; }" ], "description": [], "children": [ { - "type": "CompoundType", + "type": "Uncategorized", "label": "input", - "isRequired": false, + "isRequired": true, "signature": [ - "Input" + "null" ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 81 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 42 } }, { @@ -19892,105 +21960,201 @@ ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 81 + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 42 } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 42 + } + } + ], + "description": [], + "label": "luceneFunction", + "source": { + "path": "src/plugins/data/common/search/expressions/lucene.ts", + "lineNumber": 24 + }, + "initialIsOpen": false + }, + { + "tags": [], + "id": "def-common.migrateIncludeExcludeFormat", + "type": "Object", + "label": "migrateIncludeExcludeFormat", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/buckets/migrate_include_exclude_format.ts", + "lineNumber": 25 + }, + "signature": [ + "Partial<", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.BucketAggParam", + "text": "BucketAggParam" + }, + "<", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.IBucketAggConfig", + "text": "IBucketAggConfig" + }, + ">>" + ], + "initialIsOpen": false + }, + { + "id": "def-common.parentPipelineAggHelper", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.parentPipelineAggHelper.subtype", + "type": "string", + "label": "subtype", + "description": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "lineNumber": 35 + } + }, + { + "id": "def-common.parentPipelineAggHelper.params", + "type": "Function", + "label": "params", + "signature": [ + "() => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.MetricAggParam", + "text": "MetricAggParam" + }, + "<", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.IMetricAggConfig", + "text": "IMetricAggConfig" + }, + ">[]" + ], + "description": [], + "children": [], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "lineNumber": 36 + } + }, + { + "id": "def-common.parentPipelineAggHelper.getSerializedFormat", + "type": "Function", + "label": "getSerializedFormat", + "signature": [ + "(agg: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.IMetricAggConfig", + "text": "IMetricAggConfig" }, + ") => any" + ], + "description": [], + "children": [ { "type": "Object", - "label": "{ getSavedObject }", + "label": "agg", "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" - }, - ">" + "section": "def-common.IMetricAggConfig", + "text": "IMetricAggConfig" + } ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 81 + "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "lineNumber": 64 } } ], "tags": [], "returnComment": [], "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 81 + "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "lineNumber": 64 } } ], "description": [], - "label": "kibanaContextFunction", + "label": "parentPipelineAggHelper", "source": { - "path": "src/plugins/data/common/search/expressions/kibana_context.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", + "lineNumber": 34 }, "initialIsOpen": false }, { - "id": "def-common.kibanaTimerangeFunction", + "id": "def-common.phraseFilterFunction", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaTimerangeFunction.name", + "id": "def-common.phraseFilterFunction.name", "type": "string", "label": "name", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 23 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 29 }, "signature": [ - "\"timerange\"" + "\"rangeFilter\"" ] }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.type", + "id": "def-common.phraseFilterFunction.type", "type": "string", "label": "type", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 24 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 30 }, "signature": [ - "\"timerange\"" + "\"kibana_filter\"" ] }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.inputTypes", + "id": "def-common.phraseFilterFunction.inputTypes", "type": "Array", "label": "inputTypes", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 25 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 31 }, "signature": [ "\"null\"[]" @@ -19998,48 +22162,48 @@ }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.help", + "id": "def-common.phraseFilterFunction.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 26 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 32 } }, { - "id": "def-common.kibanaTimerangeFunction.args", + "id": "def-common.phraseFilterFunction.args", "type": "Object", "tags": [], "children": [ { - "id": "def-common.kibanaTimerangeFunction.args.from", + "id": "def-common.phraseFilterFunction.args.field", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.from.types", + "id": "def-common.phraseFilterFunction.args.field.types", "type": "Array", "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 31 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 37 }, "signature": [ - "\"string\"[]" + "\"kibana_field\"[]" ] }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.from.required", + "id": "def-common.phraseFilterFunction.args.field.required", "type": "boolean", "label": "required", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 32 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 38 }, "signature": [ "true" @@ -20047,37 +22211,37 @@ }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.from.help", + "id": "def-common.phraseFilterFunction.args.field.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 33 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 39 } } ], "description": [], - "label": "from", + "label": "field", "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 30 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 36 } }, { - "id": "def-common.kibanaTimerangeFunction.args.to", + "id": "def-common.phraseFilterFunction.args.phrase", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.to.types", + "id": "def-common.phraseFilterFunction.args.phrase.types", "type": "Array", "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 38 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 44 }, "signature": [ "\"string\"[]" @@ -20085,13 +22249,27 @@ }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.to.required", + "id": "def-common.phraseFilterFunction.args.phrase.multi", + "type": "boolean", + "label": "multi", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 45 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.phraseFilterFunction.args.phrase.required", "type": "boolean", "label": "required", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 39 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 46 }, "signature": [ "true" @@ -20099,97 +22277,105 @@ }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.to.help", + "id": "def-common.phraseFilterFunction.args.phrase.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 40 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 47 } } ], "description": [], - "label": "to", + "label": "phrase", "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 37 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 43 } }, { - "id": "def-common.kibanaTimerangeFunction.args.mode", + "id": "def-common.phraseFilterFunction.args.negate", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.mode.types", + "id": "def-common.phraseFilterFunction.args.negate.types", "type": "Array", "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 45 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 52 }, "signature": [ - "\"string\"[]" + "\"boolean\"[]" ] }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.mode.options", - "type": "Array", - "label": "options", + "id": "def-common.phraseFilterFunction.args.negate.default", + "type": "boolean", + "label": "default", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 46 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 53 }, "signature": [ - "(\"absolute\" | \"relative\")[]" + "false" ] }, { "tags": [], - "id": "def-common.kibanaTimerangeFunction.args.mode.help", + "id": "def-common.phraseFilterFunction.args.negate.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 47 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 54 } } ], "description": [], - "label": "mode", + "label": "negate", "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 44 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 51 } } ], "description": [], "label": "args", "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 29 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 35 } }, { - "id": "def-common.kibanaTimerangeFunction.fn", + "id": "def-common.phraseFilterFunction.fn", "type": "Function", "label": "fn", "signature": [ - "(input: null, args: ", + "(input: null, args: Arguments) => { $state?: ", { "pluginId": "data", "scope": "common", - "docId": "kibDataQueryPluginApi", - "section": "def-common.TimeRange", - "text": "TimeRange" + "docId": "kibDataPluginApi", + "section": "def-common.FilterState", + "text": "FilterState" }, - ") => { type: \"timerange\"; from: string; to: string; mode: \"absolute\" | \"relative\" | undefined; }" + " | undefined; meta: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.FilterMeta", + "text": "FilterMeta" + }, + "; query?: any; type: \"kibana_filter\"; }" ], "description": [], "children": [ @@ -20202,8 +22388,8 @@ ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 53 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 60 } }, { @@ -20211,79 +22397,73 @@ "label": "args", "isRequired": true, "signature": [ - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataQueryPluginApi", - "section": "def-common.TimeRange", - "text": "TimeRange" - } + "Arguments" ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 53 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 60 } } ], "tags": [], "returnComment": [], "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 53 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 60 } } ], "description": [], - "label": "kibanaTimerangeFunction", + "label": "phraseFilterFunction", "source": { - "path": "src/plugins/data/common/search/expressions/timerange.ts", - "lineNumber": 22 + "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", + "lineNumber": 28 }, "initialIsOpen": false }, { - "id": "def-common.kqlFunction", + "id": "def-common.rangeFilterFunction", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kqlFunction.name", + "id": "def-common.rangeFilterFunction.name", "type": "string", "label": "name", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 25 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 30 }, "signature": [ - "\"kql\"" + "\"rangeFilter\"" ] }, { "tags": [], - "id": "def-common.kqlFunction.type", + "id": "def-common.rangeFilterFunction.type", "type": "string", "label": "type", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 26 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 31 }, "signature": [ - "\"kibana_query\"" + "\"kibana_filter\"" ] }, { "tags": [], - "id": "def-common.kqlFunction.inputTypes", + "id": "def-common.rangeFilterFunction.inputTypes", "type": "Array", "label": "inputTypes", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 27 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 32 }, "signature": [ "\"null\"[]" @@ -20291,48 +22471,48 @@ }, { "tags": [], - "id": "def-common.kqlFunction.help", + "id": "def-common.rangeFilterFunction.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 28 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 33 } }, { - "id": "def-common.kqlFunction.args", + "id": "def-common.rangeFilterFunction.args", "type": "Object", "tags": [], "children": [ { - "id": "def-common.kqlFunction.args.q", + "id": "def-common.rangeFilterFunction.args.field", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.kqlFunction.args.q.types", + "id": "def-common.rangeFilterFunction.args.field.types", "type": "Array", "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 33 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 38 }, "signature": [ - "\"string\"[]" + "\"kibana_field\"[]" ] }, { "tags": [], - "id": "def-common.kqlFunction.args.q.required", + "id": "def-common.rangeFilterFunction.args.field.required", "type": "boolean", "label": "required", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 34 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 39 }, "signature": [ "true" @@ -20340,51 +22520,157 @@ }, { "tags": [], - "id": "def-common.kqlFunction.args.q.aliases", + "id": "def-common.rangeFilterFunction.args.field.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 40 + } + } + ], + "description": [], + "label": "field", + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 37 + } + }, + { + "id": "def-common.rangeFilterFunction.args.range", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.rangeFilterFunction.args.range.types", "type": "Array", - "label": "aliases", + "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 35 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 45 }, "signature": [ - "string[]" + "\"kibana_range\"[]" ] }, { "tags": [], - "id": "def-common.kqlFunction.args.q.help", + "id": "def-common.rangeFilterFunction.args.range.required", + "type": "boolean", + "label": "required", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 46 + }, + "signature": [ + "true" + ] + }, + { + "tags": [], + "id": "def-common.rangeFilterFunction.args.range.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 36 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 47 } } ], "description": [], - "label": "q", + "label": "range", "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 32 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 44 + } + }, + { + "id": "def-common.rangeFilterFunction.args.negate", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.rangeFilterFunction.args.negate.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 52 + }, + "signature": [ + "\"boolean\"[]" + ] + }, + { + "tags": [], + "id": "def-common.rangeFilterFunction.args.negate.default", + "type": "boolean", + "label": "default", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 53 + }, + "signature": [ + "false" + ] + }, + { + "tags": [], + "id": "def-common.rangeFilterFunction.args.negate.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 54 + } + } + ], + "description": [], + "label": "negate", + "source": { + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 51 } } ], "description": [], "label": "args", "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 31 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 36 } }, { - "id": "def-common.kqlFunction.fn", + "id": "def-common.rangeFilterFunction.fn", "type": "Function", "label": "fn", "signature": [ - "(input: null, args: Arguments) => { type: \"kibana_query\"; language: string; query: string; }" + "(input: null, args: Arguments) => { $state?: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.FilterState", + "text": "FilterState" + }, + " | undefined; meta: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.FilterMeta", + "text": "FilterMeta" + }, + "; query?: any; type: \"kibana_filter\"; }" ], "description": [], "children": [ @@ -20397,8 +22683,8 @@ ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 60 } }, { @@ -20410,69 +22696,69 @@ ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 60 } } ], "tags": [], "returnComment": [], "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 60 } } ], "description": [], - "label": "kqlFunction", + "label": "rangeFilterFunction", "source": { - "path": "src/plugins/data/common/search/expressions/kql.ts", - "lineNumber": 24 + "path": "src/plugins/data/common/search/expressions/range_filter.ts", + "lineNumber": 29 }, "initialIsOpen": false }, { - "id": "def-common.luceneFunction", + "id": "def-common.rangeFunction", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.luceneFunction.name", + "id": "def-common.rangeFunction.name", "type": "string", "label": "name", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 25 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 29 }, "signature": [ - "\"lucene\"" + "\"range\"" ] }, { "tags": [], - "id": "def-common.luceneFunction.type", + "id": "def-common.rangeFunction.type", "type": "string", "label": "type", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 26 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 30 }, "signature": [ - "\"kibana_query\"" + "\"kibana_range\"" ] }, { "tags": [], - "id": "def-common.luceneFunction.inputTypes", + "id": "def-common.rangeFunction.inputTypes", "type": "Array", "label": "inputTypes", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 27 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 31 }, "signature": [ "\"null\"[]" @@ -20480,100 +22766,186 @@ }, { "tags": [], - "id": "def-common.luceneFunction.help", + "id": "def-common.rangeFunction.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 28 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 32 } }, { - "id": "def-common.luceneFunction.args", + "id": "def-common.rangeFunction.args", "type": "Object", "tags": [], "children": [ { - "id": "def-common.luceneFunction.args.q", + "id": "def-common.rangeFunction.args.gt", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.luceneFunction.args.q.types", + "id": "def-common.rangeFunction.args.gt.types", "type": "Array", "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 33 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 37 }, "signature": [ - "\"string\"[]" + "(\"string\" | \"number\")[]" ] }, { "tags": [], - "id": "def-common.luceneFunction.args.q.required", - "type": "boolean", - "label": "required", + "id": "def-common.rangeFunction.args.gt.help", + "type": "string", + "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 34 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 38 + } + } + ], + "description": [], + "label": "gt", + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 36 + } + }, + { + "id": "def-common.rangeFunction.args.lt", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.rangeFunction.args.lt.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 43 }, "signature": [ - "true" + "(\"string\" | \"number\")[]" ] }, { "tags": [], - "id": "def-common.luceneFunction.args.q.aliases", + "id": "def-common.rangeFunction.args.lt.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 44 + } + } + ], + "description": [], + "label": "lt", + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 42 + } + }, + { + "id": "def-common.rangeFunction.args.gte", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.rangeFunction.args.gte.types", "type": "Array", - "label": "aliases", + "label": "types", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 35 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 49 }, "signature": [ - "string[]" + "(\"string\" | \"number\")[]" ] }, { "tags": [], - "id": "def-common.luceneFunction.args.q.help", + "id": "def-common.rangeFunction.args.gte.help", "type": "string", "label": "help", "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 36 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 50 } } ], "description": [], - "label": "q", + "label": "gte", "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 32 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 48 + } + }, + { + "id": "def-common.rangeFunction.args.lte", + "type": "Object", + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.rangeFunction.args.lte.types", + "type": "Array", + "label": "types", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 55 + }, + "signature": [ + "(\"string\" | \"number\")[]" + ] + }, + { + "tags": [], + "id": "def-common.rangeFunction.args.lte.help", + "type": "string", + "label": "help", + "description": [], + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 56 + } + } + ], + "description": [], + "label": "lte", + "source": { + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 54 } } ], "description": [], "label": "args", "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 31 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 35 } }, { - "id": "def-common.luceneFunction.fn", + "id": "def-common.rangeFunction.fn", "type": "Function", "label": "fn", "signature": [ - "(input: null, args: Arguments) => { type: \"kibana_query\"; language: string; query: any; }" + "(input: null, args: Arguments) => { gt?: string | number | undefined; lt?: string | number | undefined; gte?: string | number | undefined; lte?: string | number | undefined; type: \"kibana_range\"; }" ], "description": [], "children": [ @@ -20586,8 +22958,8 @@ ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 62 } }, { @@ -20599,80 +22971,49 @@ ], "description": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 62 } } ], "tags": [], "returnComment": [], "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 42 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 62 } } ], "description": [], - "label": "luceneFunction", - "source": { - "path": "src/plugins/data/common/search/expressions/lucene.ts", - "lineNumber": 24 - }, - "initialIsOpen": false - }, - { - "tags": [], - "id": "def-common.migrateIncludeExcludeFormat", - "type": "Object", - "label": "migrateIncludeExcludeFormat", - "description": [], + "label": "rangeFunction", "source": { - "path": "src/plugins/data/common/search/aggs/buckets/migrate_include_exclude_format.ts", - "lineNumber": 25 + "path": "src/plugins/data/common/search/expressions/range.ts", + "lineNumber": 28 }, - "signature": [ - "Partial<", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.BucketAggParam", - "text": "BucketAggParam" - }, - "<", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.IBucketAggConfig", - "text": "IBucketAggConfig" - }, - ">>" - ], "initialIsOpen": false }, { - "id": "def-common.parentPipelineAggHelper", + "id": "def-common.siblingPipelineAggHelper", "type": "Object", "tags": [], "children": [ { "tags": [], - "id": "def-common.parentPipelineAggHelper.subtype", + "id": "def-common.siblingPipelineAggHelper.subtype", "type": "string", "label": "subtype", "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", - "lineNumber": 34 + "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", + "lineNumber": 42 } }, { - "id": "def-common.parentPipelineAggHelper.params", + "id": "def-common.siblingPipelineAggHelper.params", "type": "Function", "label": "params", "signature": [ - "() => ", + "(bucketFilter?: string[]) => ", { "pluginId": "data", "scope": "common", @@ -20691,113 +23032,26 @@ ">[]" ], "description": [], - "children": [], - "tags": [], - "returnComment": [], - "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", - "lineNumber": 35 - } - }, - { - "id": "def-common.parentPipelineAggHelper.getSerializedFormat", - "type": "Function", - "label": "getSerializedFormat", - "signature": [ - "(agg: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.IMetricAggConfig", - "text": "IMetricAggConfig" - }, - ") => any" - ], - "description": [], "children": [ { - "type": "Object", - "label": "agg", + "type": "Array", + "label": "bucketFilter", "isRequired": true, "signature": [ - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.IMetricAggConfig", - "text": "IMetricAggConfig" - } + "string[]" ], "description": [], "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", - "lineNumber": 63 + "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", + "lineNumber": 43 } } ], "tags": [], "returnComment": [], - "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", - "lineNumber": 63 - } - } - ], - "description": [], - "label": "parentPipelineAggHelper", - "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts", - "lineNumber": 33 - }, - "initialIsOpen": false - }, - { - "id": "def-common.siblingPipelineAggHelper", - "type": "Object", - "tags": [], - "children": [ - { - "tags": [], - "id": "def-common.siblingPipelineAggHelper.subtype", - "type": "string", - "label": "subtype", - "description": [], - "source": { - "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", - "lineNumber": 41 - } - }, - { - "id": "def-common.siblingPipelineAggHelper.params", - "type": "Function", - "label": "params", - "signature": [ - "() => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.MetricAggParam", - "text": "MetricAggParam" - }, - "<", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.IMetricAggConfig", - "text": "IMetricAggConfig" - }, - ">[]" - ], - "description": [], - "children": [], - "tags": [], - "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", - "lineNumber": 42 + "lineNumber": 43 } }, { @@ -20833,7 +23087,7 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", - "lineNumber": 77 + "lineNumber": 79 } } ], @@ -20841,7 +23095,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", - "lineNumber": 77 + "lineNumber": 79 } } ], @@ -20849,7 +23103,7 @@ "label": "siblingPipelineAggHelper", "source": { "path": "src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts", - "lineNumber": 40 + "lineNumber": 41 }, "initialIsOpen": false } diff --git a/api_docs/expressions.json b/api_docs/expressions.json index f80d9b6c4cdd3..eefffb009be2a 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -21952,6 +21952,41 @@ "returnComment": [], "initialIsOpen": false }, + { + "id": "def-common.createMockContext", + "type": "Function", + "children": [], + "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" + }, + ", ", + "SerializableState", + ">" + ], + "description": [], + "label": "createMockContext", + "source": { + "path": "src/plugins/expressions/common/util/test_utils.ts", + "lineNumber": 11 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + }, { "id": "def-common.format", "type": "Function", @@ -22465,6 +22500,52 @@ "tags": [], "returnComment": [], "initialIsOpen": false + }, + { + "id": "def-common.unboxExpressionValue", + "type": "Function", + "label": "unboxExpressionValue", + "signature": [ + "({\n type,\n ...value\n}: ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + ") => T" + ], + "description": [], + "children": [ + { + "type": "CompoundType", + "label": "{\n type,\n ...value\n}", + "isRequired": true, + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "" + ], + "description": [], + "source": { + "path": "src/plugins/expressions/common/expression_types/unbox_expression_value.ts", + "lineNumber": 11 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/expressions/common/expression_types/unbox_expression_value.ts", + "lineNumber": 11 + }, + "initialIsOpen": false } ], "interfaces": [ diff --git a/api_docs/fleet.json b/api_docs/fleet.json index d9774d14e4c96..ed51f88ee9d5d 100644 --- a/api_docs/fleet.json +++ b/api_docs/fleet.json @@ -2515,7 +2515,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 94 + "lineNumber": 95 }, "signature": [ { @@ -2535,7 +2535,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 95 + "lineNumber": 96 }, "signature": [ { @@ -2556,7 +2556,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 96 + "lineNumber": 97 }, "signature": [ { @@ -2577,7 +2577,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 97 + "lineNumber": 98 }, "signature": [ { @@ -2597,7 +2597,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 98 + "lineNumber": 99 }, "signature": [ { @@ -2618,7 +2618,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 99 + "lineNumber": 100 }, "signature": [ "Pick<", @@ -2635,7 +2635,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 93 + "lineNumber": 94 }, "initialIsOpen": false }, @@ -2735,7 +2735,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 137 + "lineNumber": 140 }, "signature": [ "[\"packagePolicyCreate\", (newPackagePolicy: ", @@ -2837,7 +2837,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 122 + "lineNumber": 125 }, "signature": [ "void" @@ -2862,7 +2862,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 161 + "lineNumber": 164 }, "signature": [ { @@ -2882,7 +2882,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 162 + "lineNumber": 165 }, "signature": [ { @@ -2902,7 +2902,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 163 + "lineNumber": 166 }, "signature": [ { @@ -2924,7 +2924,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 167 + "lineNumber": 170 }, "signature": [ { @@ -2944,7 +2944,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 168 + "lineNumber": 171 }, "signature": [ { @@ -2966,7 +2966,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 173 + "lineNumber": 176 }, "signature": [ "(...args: ", @@ -2990,7 +2990,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 179 + "lineNumber": 182 }, "signature": [ "(packageName: string) => ", @@ -3006,7 +3006,7 @@ ], "source": { "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 160 + "lineNumber": 163 }, "lifecycle": "start", "initialIsOpen": true @@ -3274,7 +3274,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/index.ts", - "lineNumber": 38 + "lineNumber": 39 }, "signature": [ "(o: T) => [keyof T, T[keyof T]][]" @@ -4486,59 +4486,44 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 86 + "lineNumber": 91 } }, { "tags": [], - "id": "def-common.BulkInstallPackageInfo.newVersion", + "id": "def-common.BulkInstallPackageInfo.version", "type": "string", - "label": "newVersion", + "label": "version", "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 87 + "lineNumber": 92 } }, { "tags": [], - "id": "def-common.BulkInstallPackageInfo.oldVersion", - "type": "CompoundType", - "label": "oldVersion", - "description": [], - "source": { - "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 89 - }, - "signature": [ - "string | null" - ] - }, - { - "tags": [], - "id": "def-common.BulkInstallPackageInfo.assets", - "type": "Array", - "label": "assets", + "id": "def-common.BulkInstallPackageInfo.result", + "type": "Object", + "label": "result", "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 90 + "lineNumber": 93 }, "signature": [ { "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.AssetReference", - "text": "AssetReference" - }, - "[]" + "section": "def-common.InstallResult", + "text": "InstallResult" + } ] } ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 85 + "lineNumber": 90 }, "initialIsOpen": false }, @@ -4557,7 +4542,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 98 + "lineNumber": 101 }, "signature": [ "{ packages: string[]; }" @@ -4566,7 +4551,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 97 + "lineNumber": 100 }, "initialIsOpen": false }, @@ -4585,7 +4570,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 94 + "lineNumber": 97 }, "signature": [ "(", @@ -4610,7 +4595,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 93 + "lineNumber": 96 }, "initialIsOpen": false }, @@ -5235,7 +5220,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 108 + "lineNumber": 111 }, "signature": [ "{ pkgkey: string; }" @@ -5244,7 +5229,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 107 + "lineNumber": 110 }, "initialIsOpen": false }, @@ -5263,7 +5248,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 114 + "lineNumber": 117 }, "signature": [ { @@ -5279,7 +5264,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 113 + "lineNumber": 116 }, "initialIsOpen": false }, @@ -5507,7 +5492,7 @@ "lineNumber": 15 }, "signature": [ - "{ enabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; maxConcurrentConnections: number; kibana: { host?: string | string[] | undefined; ca_sha256?: string | undefined; }; elasticsearch: { host?: string | undefined; ca_sha256?: string | undefined; }; agentPolicyRolloutRateLimitIntervalMs: number; agentPolicyRolloutRateLimitRequestPerInterval: number; }" + "{ enabled: boolean; fleetServerEnabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; maxConcurrentConnections: number; kibana: { host?: string | string[] | undefined; ca_sha256?: string | undefined; }; elasticsearch: { host?: string | undefined; ca_sha256?: string | undefined; }; agentPolicyRolloutRateLimitIntervalMs: number; agentPolicyRolloutRateLimitRequestPerInterval: number; }" ] } ], @@ -8609,6 +8594,55 @@ }, "initialIsOpen": false }, + { + "id": "def-common.InstallResult", + "type": "Interface", + "label": "InstallResult", + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-common.InstallResult.assets", + "type": "Array", + "label": "assets", + "description": [], + "source": { + "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", + "lineNumber": 86 + }, + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.AssetReference", + "text": "AssetReference" + }, + "[]" + ] + }, + { + "tags": [], + "id": "def-common.InstallResult.status", + "type": "CompoundType", + "label": "status", + "description": [], + "source": { + "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", + "lineNumber": 87 + }, + "signature": [ + "\"installed\" | \"already_installed\"" + ] + } + ], + "source": { + "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", + "lineNumber": 85 + }, + "initialIsOpen": false + }, { "id": "def-common.InstallScriptRequest", "type": "Interface", @@ -8824,13 +8858,13 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 104 + "lineNumber": 107 } } ], "source": { "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", - "lineNumber": 103 + "lineNumber": 106 }, "initialIsOpen": false }, @@ -13209,7 +13243,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 13 + "lineNumber": 12 }, "signature": [ "\"fleet_server\"" @@ -13246,21 +13280,6 @@ ], "initialIsOpen": false }, - { - "tags": [], - "id": "def-common.INDEX_PATTERN_SAVED_OBJECT_TYPE", - "type": "string", - "label": "INDEX_PATTERN_SAVED_OBJECT_TYPE", - "description": [], - "source": { - "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 10 - }, - "signature": [ - "\"index-pattern\"" - ], - "initialIsOpen": false - }, { "tags": [], "id": "def-common.INSTALL_SCRIPT_API_ROUTES", @@ -13460,7 +13479,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 11 + "lineNumber": 10 }, "signature": [ "60000" @@ -14087,7 +14106,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/index.ts", - "lineNumber": 43 + "lineNumber": 44 }, "signature": [ "T[keyof T]" @@ -14443,7 +14462,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 24 + "lineNumber": 23 }, "signature": [ "{ readonly Input: \"input\"; }" @@ -15151,7 +15170,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 28 + "lineNumber": 27 }, "signature": [ "{ readonly Logs: \"logs\"; readonly Metrics: \"metrics\"; }" @@ -15481,7 +15500,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 22 + "lineNumber": 21 }, "signature": [ "{ readonly System: \"system\"; readonly Endpoint: \"endpoint\"; readonly ElasticAgent: \"elastic_agent\"; }" @@ -16058,7 +16077,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 33 + "lineNumber": 32 }, "signature": [ "{ readonly Installed: \"installed\"; readonly NotInstalled: \"not_installed\"; }" @@ -16416,7 +16435,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/constants/epm.ts", - "lineNumber": 15 + "lineNumber": 14 }, "signature": [ "{ readonly System: \"system\"; readonly Endpoint: \"endpoint\"; readonly ElasticAgent: \"elastic_agent\"; }" diff --git a/api_docs/lens.json b/api_docs/lens.json index abebd217ad7d5..e586016c22fc3 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -288,7 +288,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts", - "lineNumber": 44 + "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" @@ -302,7 +302,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts", - "lineNumber": 45 + "lineNumber": 47 }, "signature": [ "string | undefined" @@ -311,7 +311,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts", - "lineNumber": 43 + "lineNumber": 45 }, "initialIsOpen": false }, @@ -1318,7 +1318,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 125 + "lineNumber": 127 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"avg\"; }" @@ -1475,7 +1475,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 127 + "lineNumber": 129 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"max\"; }" @@ -1490,7 +1490,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 128 + "lineNumber": 130 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"median\"; }" @@ -1505,7 +1505,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 126 + "lineNumber": 128 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"min\"; }" @@ -1538,7 +1538,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts", - "lineNumber": 405 + "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\"" @@ -1598,7 +1598,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx", - "lineNumber": 124 + "lineNumber": 126 }, "signature": [ "BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"sum\"; }" diff --git a/api_docs/observability.json b/api_docs/observability.json index a3d1bc950cb53..439fd18db6469 100644 --- a/api_docs/observability.json +++ b/api_docs/observability.json @@ -2351,7 +2351,7 @@ "description": [], "source": { "path": "x-pack/plugins/observability/server/plugin.ts", - "lineNumber": 22 + "lineNumber": 23 }, "signature": [ "LazyScopedAnnotationsClientFactory" @@ -2360,7 +2360,7 @@ ], "source": { "path": "x-pack/plugins/observability/server/plugin.ts", - "lineNumber": 21 + "lineNumber": 22 }, "lifecycle": "setup", "initialIsOpen": true diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md new file mode 100644 index 0000000000000..71e3e025b931d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggFunctionsMapping](./kibana-plugin-plugins-data-public.aggfunctionsmapping.md) > [aggFilteredMetric](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md) + +## AggFunctionsMapping.aggFilteredMetric property + +Signature: + +```typescript +aggFilteredMetric: ReturnType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md index b175b8d473d34..05388e2b86d7b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md @@ -28,6 +28,7 @@ export interface AggFunctionsMapping | [aggDateRange](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggdaterange.md) | ReturnType<typeof aggDateRange> | | | [aggDerivative](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggderivative.md) | ReturnType<typeof aggDerivative> | | | [aggFilter](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilter.md) | ReturnType<typeof aggFilter> | | +| [aggFilteredMetric](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md) | ReturnType<typeof aggFilteredMetric> | | | [aggFilters](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilters.md) | ReturnType<typeof aggFilters> | | | [aggGeoBounds](./kibana-plugin-plugins-data-public.aggfunctionsmapping.agggeobounds.md) | ReturnType<typeof aggGeoBounds> | | | [aggGeoCentroid](./kibana-plugin-plugins-data-public.aggfunctionsmapping.agggeocentroid.md) | ReturnType<typeof aggGeoCentroid> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md index 637717692a38c..3b5cecf1a0b82 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md @@ -20,6 +20,7 @@ export declare enum METRIC_TYPES | COUNT | "count" | | | CUMULATIVE\_SUM | "cumulative_sum" | | | DERIVATIVE | "derivative" | | +| FILTERED\_METRIC | "filtered_metric" | | | GEO\_BOUNDS | "geo_bounds" | | | GEO\_CENTROID | "geo_centroid" | | | MAX | "max" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md new file mode 100644 index 0000000000000..9885a0afa40c6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggFunctionsMapping](./kibana-plugin-plugins-data-server.aggfunctionsmapping.md) > [aggFilteredMetric](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md) + +## AggFunctionsMapping.aggFilteredMetric property + +Signature: + +```typescript +aggFilteredMetric: ReturnType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md index f059bb2914847..86bf797572b09 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md @@ -28,6 +28,7 @@ export interface AggFunctionsMapping | [aggDateRange](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggdaterange.md) | ReturnType<typeof aggDateRange> | | | [aggDerivative](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggderivative.md) | ReturnType<typeof aggDerivative> | | | [aggFilter](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilter.md) | ReturnType<typeof aggFilter> | | +| [aggFilteredMetric](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md) | ReturnType<typeof aggFilteredMetric> | | | [aggFilters](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilters.md) | ReturnType<typeof aggFilters> | | | [aggGeoBounds](./kibana-plugin-plugins-data-server.aggfunctionsmapping.agggeobounds.md) | ReturnType<typeof aggGeoBounds> | | | [aggGeoCentroid](./kibana-plugin-plugins-data-server.aggfunctionsmapping.agggeocentroid.md) | ReturnType<typeof aggGeoCentroid> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md index 49df98b6d70a1..250173d11a056 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md @@ -20,6 +20,7 @@ export declare enum METRIC_TYPES | COUNT | "count" | | | CUMULATIVE\_SUM | "cumulative_sum" | | | DERIVATIVE | "derivative" | | +| FILTERED\_METRIC | "filtered_metric" | | | GEO\_BOUNDS | "geo_bounds" | | | GEO\_CENTROID | "geo_centroid" | | | MAX | "max" | | diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index f62fedc13b32a..0e9cf6aeb1f2f 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -232,7 +232,9 @@ export class AggConfig { const output = this.write(aggConfigs) as any; const configDsl = {} as any; - configDsl[this.type.dslName || this.type.name] = output.params; + if (!this.type.hasNoDslParams) { + configDsl[this.type.dslName || this.type.name] = output.params; + } // if the config requires subAggs, write them to the dsl as well if (this.subAggs.length) { diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index c9986b7e93bed..03c702aa72fb5 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -21,7 +21,14 @@ import { TimeRange } from '../../../common'; function removeParentAggs(obj: any) { for (const prop in obj) { if (prop === 'parentAggs') delete obj[prop]; - else if (typeof obj[prop] === 'object') removeParentAggs(obj[prop]); + else if (typeof obj[prop] === 'object') { + const hasParentAggsKey = 'parentAggs' in obj[prop]; + removeParentAggs(obj[prop]); + // delete object if parentAggs was the last key + if (hasParentAggsKey && Object.keys(obj[prop]).length === 0) { + delete obj[prop]; + } + } } } @@ -193,10 +200,12 @@ export class AggConfigs { // advance the cursor and nest under the previous agg, or // put it on the same level if the previous agg doesn't accept // sub aggs - dslLvlCursor = prevDsl.aggs || dslLvlCursor; + dslLvlCursor = prevDsl?.aggs || dslLvlCursor; } - const dsl = (dslLvlCursor[config.id] = config.toDsl(this)); + const dsl = config.type.hasNoDslParams + ? config.toDsl(this) + : (dslLvlCursor[config.id] = config.toDsl(this)); let subAggs: any; parseParentAggs(dslLvlCursor, dsl); @@ -206,6 +215,11 @@ export class AggConfigs { subAggs = dsl.aggs || (dsl.aggs = {}); } + if (subAggs) { + _.each(subAggs, (agg) => { + parseParentAggs(subAggs, agg); + }); + } if (subAggs && nestedMetrics) { nestedMetrics.forEach((agg: any) => { subAggs[agg.config.id] = agg.dsl; diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 4583be17478e3..33fdc45a605b7 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -32,6 +32,7 @@ export interface AggTypeConfig< makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); ordered?: any; hasNoDsl?: boolean; + hasNoDslParams?: boolean; params?: Array>; valueType?: DatatableColumnType; getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); @@ -129,6 +130,12 @@ export class AggType< * @type {Boolean} */ hasNoDsl: boolean; + /** + * Flag that prevents params from this aggregation from being included in the dsl. Sibling and parent aggs are still written. + * + * @type {Boolean} + */ + hasNoDslParams: boolean; /** * The method to create a filter representation of the bucket * @param {object} aggConfig The instance of the aggConfig @@ -232,6 +239,7 @@ export class AggType< this.makeLabel = config.makeLabel || constant(this.name); this.ordered = config.ordered; this.hasNoDsl = !!config.hasNoDsl; + this.hasNoDslParams = !!config.hasNoDslParams; if (config.createFilter) { this.createFilter = config.createFilter; diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index a7af68a5ad8b4..d02f8e1fc5af4 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -44,6 +44,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.SUM_BUCKET, fn: metrics.getBucketSumMetricAgg }, { name: METRIC_TYPES.MIN_BUCKET, fn: metrics.getBucketMinMetricAgg }, { name: METRIC_TYPES.MAX_BUCKET, fn: metrics.getBucketMaxMetricAgg }, + { name: METRIC_TYPES.FILTERED_METRIC, fn: metrics.getFilteredMetricAgg }, { name: METRIC_TYPES.GEO_BOUNDS, fn: metrics.getGeoBoundsMetricAgg }, { name: METRIC_TYPES.GEO_CENTROID, fn: metrics.getGeoCentroidMetricAgg }, ], @@ -80,6 +81,7 @@ export const getAggTypesFunctions = () => [ metrics.aggBucketMax, metrics.aggBucketMin, metrics.aggBucketSum, + metrics.aggFilteredMetric, metrics.aggCardinality, metrics.aggCount, metrics.aggCumulativeSum, diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index 92e6168b169c6..bba67640890ad 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -97,6 +97,7 @@ describe('Aggs service', () => { "sum_bucket", "min_bucket", "max_bucket", + "filtered_metric", "geo_bounds", "geo_centroid", ] @@ -142,6 +143,7 @@ describe('Aggs service', () => { "sum_bucket", "min_bucket", "max_bucket", + "filtered_metric", "geo_bounds", "geo_centroid", ] diff --git a/src/plugins/data/common/search/aggs/buckets/filter.ts b/src/plugins/data/common/search/aggs/buckets/filter.ts index 14a8f84c2cb9f..900848bb9517f 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter.ts @@ -6,12 +6,15 @@ * Side Public License, v 1. */ +import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { GeoBoundingBox } from './lib/geo_point'; import { aggFilterFnName } from './filter_fn'; import { BaseAggParams } from '../types'; +import { Query } from '../../../types'; +import { buildEsQuery, getEsQueryConfig } from '../../../es_query'; const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', { defaultMessage: 'Filter', @@ -21,7 +24,7 @@ export interface AggParamsFilter extends BaseAggParams { geo_bounding_box?: GeoBoundingBox; } -export const getFilterBucketAgg = () => +export const getFilterBucketAgg = ({ getConfig }: { getConfig: (key: string) => any }) => new BucketAggType({ name: BUCKET_TYPES.FILTER, expressionName: aggFilterFnName, @@ -31,5 +34,27 @@ export const getFilterBucketAgg = () => { name: 'geo_bounding_box', }, + { + name: 'filter', + write(aggConfig, output) { + const filter: Query = aggConfig.params.filter; + + const input = cloneDeep(filter); + + if (!input) { + return; + } + + const esQueryConfigs = getEsQueryConfig({ get: getConfig }); + const query = buildEsQuery(aggConfig.getIndexPattern(), [input], [], esQueryConfigs); + + if (!query) { + console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console + return; + } + + output.params = query; + }, + }, ], }); diff --git a/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts index 0b9f2915e9aa4..8b4642bf595cd 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts @@ -23,6 +23,7 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, + "filter": undefined, "geo_bounding_box": undefined, "json": undefined, }, @@ -46,6 +47,7 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, + "filter": undefined, "geo_bounding_box": Object { "wkt": "BBOX (-74.1, -71.12, 40.73, 40.01)", }, @@ -57,6 +59,25 @@ describe('agg_expression_functions', () => { `); }); + test('correctly parses filter string argument', () => { + const actual = fn({ + filter: '{ "language": "kuery", "query": "a: b" }', + }); + + expect(actual.value.params.filter).toEqual({ language: 'kuery', query: 'a: b' }); + }); + + test('errors out if geo_bounding_box is used together with filter', () => { + expect(() => + fn({ + filter: '{ "language": "kuery", "query": "a: b" }', + geo_bounding_box: JSON.stringify({ + wkt: 'BBOX (-74.1, -71.12, 40.73, 40.01)', + }), + }) + ).toThrow(); + }); + test('correctly parses json string argument', () => { const actual = fn({ json: '{ "foo": true }', diff --git a/src/plugins/data/common/search/aggs/buckets/filter_fn.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts index 468b063046549..4c68251f5e42e 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts @@ -17,7 +17,7 @@ export const aggFilterFnName = 'aggFilter'; type Input = any; type AggArgs = AggExpressionFunctionArgs; -type Arguments = Assign; +type Arguments = Assign; type Output = AggExpressionType; type FunctionDefinition = ExpressionFunctionDefinition< @@ -59,6 +59,13 @@ export const aggFilter = (): FunctionDefinition => ({ defaultMessage: 'Filter results based on a point location within a bounding box', }), }, + filter: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.filter.filter.help', { + defaultMessage: + 'Filter results based on a kql or lucene query. Do not use together with geo_bounding_box', + }), + }, json: { types: ['string'], help: i18n.translate('data.search.aggs.buckets.filter.json.help', { @@ -75,6 +82,13 @@ export const aggFilter = (): FunctionDefinition => ({ fn: (input, args) => { const { id, enabled, schema, ...rest } = args; + const geoBoundingBox = getParsedValue(args, 'geo_bounding_box'); + const filter = getParsedValue(args, 'filter'); + + if (geoBoundingBox && filter) { + throw new Error("filter and geo_bounding_box can't be used together"); + } + return { type: 'agg_type', value: { @@ -84,7 +98,8 @@ export const aggFilter = (): FunctionDefinition => ({ type: BUCKET_TYPES.FILTER, params: { ...rest, - geo_bounding_box: getParsedValue(args, 'geo_bounding_box'), + geo_bounding_box: geoBoundingBox, + filter, }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts new file mode 100644 index 0000000000000..b27e4dd1494be --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { METRIC_TYPES } from './metric_agg_types'; + +describe('filtered metric agg type', () => { + let aggConfigs: IAggConfigs; + + beforeEach(() => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: 'bytes', + }; + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: METRIC_TYPES.FILTERED_METRIC, + type: METRIC_TYPES.FILTERED_METRIC, + schema: 'metric', + params: { + customBucket: { + type: 'filter', + params: { + filter: { language: 'kuery', query: 'a: b' }, + }, + }, + customMetric: { + type: 'cardinality', + params: { + field: 'bytes', + }, + }, + }, + }, + ], + { + typesRegistry, + } + ); + }); + + it('converts the response', () => { + const agg = aggConfigs.getResponseAggs()[0]; + + expect( + agg.getValue({ + 'filtered_metric-bucket': { + 'filtered_metric-metric': { + value: 10, + }, + }, + }) + ).toEqual(10); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric.ts new file mode 100644 index 0000000000000..aa2417bbf8415 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { MetricAggType } from './metric_agg_type'; +import { makeNestedLabel } from './lib/make_nested_label'; +import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; +import { METRIC_TYPES } from './metric_agg_types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; +import { aggFilteredMetricFnName } from './filtered_metric_fn'; + +export interface AggParamsFilteredMetric extends BaseAggParams { + customMetric?: AggConfigSerialized; + customBucket?: AggConfigSerialized; +} + +const filteredMetricLabel = i18n.translate('data.search.aggs.metrics.filteredMetricLabel', { + defaultMessage: 'filtered', +}); + +const filteredMetricTitle = i18n.translate('data.search.aggs.metrics.filteredMetricTitle', { + defaultMessage: 'Filtered metric', +}); + +export const getFilteredMetricAgg = () => { + const { subtype, params, getSerializedFormat } = siblingPipelineAggHelper; + + return new MetricAggType({ + name: METRIC_TYPES.FILTERED_METRIC, + expressionName: aggFilteredMetricFnName, + title: filteredMetricTitle, + makeLabel: (agg) => makeNestedLabel(agg, filteredMetricLabel), + subtype, + params: [...params(['filter'])], + hasNoDslParams: true, + getSerializedFormat, + getValue(agg, bucket) { + const customMetric = agg.getParam('customMetric'); + const customBucket = agg.getParam('customBucket'); + return customMetric.getValue(bucket[customBucket.id]); + }, + getValueBucketPath(agg) { + const customBucket = agg.getParam('customBucket'); + const customMetric = agg.getParam('customMetric'); + if (customMetric.type.name === 'count') { + return customBucket.getValueBucketPath(); + } + return `${customBucket.getValueBucketPath()}>${customMetric.getValueBucketPath()}`; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.test.ts new file mode 100644 index 0000000000000..22e97fe18b604 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.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 { functionWrapper } from '../test_helpers'; +import { aggFilteredMetric } from './filtered_metric_fn'; + +describe('agg_expression_functions', () => { + describe('aggFilteredMetric', () => { + const fn = functionWrapper(aggFilteredMetric()); + + test('handles customMetric and customBucket as a subexpression', () => { + const actual = fn({ + customMetric: fn({}), + customBucket: fn({}), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "customBucket": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + }, + "schema": undefined, + "type": "filtered_metric", + }, + "customLabel": undefined, + "customMetric": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customBucket": undefined, + "customLabel": undefined, + "customMetric": undefined, + }, + "schema": undefined, + "type": "filtered_metric", + }, + } + `); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.ts new file mode 100644 index 0000000000000..6a7ff5fa5fd40 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric_fn.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggFilteredMetricFnName = 'aggFilteredMetric'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Arguments = Assign< + AggArgs, + { customBucket?: AggExpressionType; customMetric?: AggExpressionType } +>; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggFilteredMetricFnName, + Input, + Arguments, + Output +>; + +export const aggFilteredMetric = (): FunctionDefinition => ({ + name: aggFilteredMetricFnName, + help: i18n.translate('data.search.aggs.function.metrics.filtered_metric.help', { + defaultMessage: 'Generates a serialized agg config for a filtered metric agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.filtered_metric.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.filtered_metric.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.filtered_metric.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + customBucket: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.filtered_metric.customBucket.help', { + defaultMessage: + 'Agg config to use for building sibling pipeline aggregations. Has to be a filter aggregation', + }), + }, + customMetric: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.metrics.filtered_metric.customMetric.help', { + defaultMessage: 'Agg config to use for building sibling pipeline aggregations', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.filtered_metric.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.FILTERED_METRIC, + params: { + ...rest, + customBucket: args.customBucket?.value, + customMetric: args.customMetric?.value, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index 4ab3b021ef93a..7038673d5d7c4 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -16,6 +16,8 @@ export * from './bucket_min_fn'; export * from './bucket_min'; export * from './bucket_sum_fn'; export * from './bucket_sum'; +export * from './filtered_metric_fn'; +export * from './filtered_metric'; export * from './cardinality_fn'; export * from './cardinality'; export * from './count'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index b72a6ca9f73ba..d51038d8a15e8 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -21,6 +21,7 @@ const metricAggFilter = [ '!std_dev', '!geo_bounds', '!geo_centroid', + '!filtered_metric', ]; export const parentPipelineType = i18n.translate( diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index d91ceae414f35..c0d1be4f47f9b 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -27,6 +27,7 @@ const metricAggFilter: string[] = [ '!cumulative_sum', '!geo_bounds', '!geo_centroid', + '!filtered_metric', ]; const bucketAggFilter: string[] = []; @@ -39,12 +40,12 @@ export const siblingPipelineType = i18n.translate( export const siblingPipelineAggHelper = { subtype: siblingPipelineType, - params() { + params(bucketFilter = bucketAggFilter) { return [ { name: 'customBucket', type: 'agg', - allowedAggs: bucketAggFilter, + allowedAggs: bucketFilter, default: null, makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); @@ -69,7 +70,8 @@ export const siblingPipelineAggHelper = { modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( 'customMetric' ), - write: siblingPipelineAggWriter, + write: (agg: IMetricAggConfig, output: Record) => + siblingPipelineAggWriter(agg, output), }, ] as Array>; }, diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index b49004be2db01..3b6c9d8a0d55d 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -8,6 +8,7 @@ export enum METRIC_TYPES { AVG = 'avg', + FILTERED_METRIC = 'filtered_metric', CARDINALITY = 'cardinality', AVG_BUCKET = 'avg_bucket', MAX_BUCKET = 'max_bucket', diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index 48ded7fa7a7fc..e57410962fc08 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -41,6 +41,7 @@ import { AggParamsBucketMax, AggParamsBucketMin, AggParamsBucketSum, + AggParamsFilteredMetric, AggParamsCardinality, AggParamsCumulativeSum, AggParamsDateHistogram, @@ -84,6 +85,7 @@ import { getCalculateAutoTimeExpression, METRIC_TYPES, AggConfig, + aggFilteredMetric, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -188,6 +190,7 @@ export interface AggParamsMapping { [METRIC_TYPES.MAX_BUCKET]: AggParamsBucketMax; [METRIC_TYPES.MIN_BUCKET]: AggParamsBucketMin; [METRIC_TYPES.SUM_BUCKET]: AggParamsBucketSum; + [METRIC_TYPES.FILTERED_METRIC]: AggParamsFilteredMetric; [METRIC_TYPES.CUMULATIVE_SUM]: AggParamsCumulativeSum; [METRIC_TYPES.DERIVATIVE]: AggParamsDerivative; [METRIC_TYPES.MOVING_FN]: AggParamsMovingAvg; @@ -217,6 +220,7 @@ export interface AggFunctionsMapping { aggBucketMax: ReturnType; aggBucketMin: ReturnType; aggBucketSum: ReturnType; + aggFilteredMetric: ReturnType; aggCardinality: ReturnType; aggCount: ReturnType; aggCumulativeSum: ReturnType; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index a61b8f400d285..5dc5a8ab2ce93 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -329,6 +329,10 @@ export interface AggFunctionsMapping { // // (undocumented) aggFilter: ReturnType; + // Warning: (ae-forgotten-export) The symbol "aggFilteredMetric" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggFilteredMetric: ReturnType; // Warning: (ae-forgotten-export) The symbol "aggFilters" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -711,7 +715,7 @@ export const ES_SEARCH_STRATEGY = "es"; // Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_34, Arguments_20, Output_34>; +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts @@ -720,7 +724,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e // Warning: (ae-missing-release-tag) "EsdslExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2; +export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -861,7 +865,7 @@ export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', Ex // Warning: (ae-missing-release-tag) "ExpressionFunctionKibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_21, Promise, ExecutionContext>; +export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_22, Promise, ExecutionContext>; // Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1830,6 +1834,8 @@ export enum METRIC_TYPES { // (undocumented) DERIVATIVE = "derivative", // (undocumented) + FILTERED_METRIC = "filtered_metric", + // (undocumented) GEO_BOUNDS = "geo_bounds", // (undocumented) GEO_CENTROID = "geo_centroid", @@ -2649,7 +2655,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/aggs/types.ts:139:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:141:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 63c27eeaf0b11..7e9170b98f132 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(11); - expect(start.types.getAll().metrics.length).toBe(21); + expect(start.types.getAll().metrics.length).toBe(22); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(12); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(22); + expect(start.types.getAll().metrics.length).toBe(23); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 83f7c67eba057..2b7da7fe194ca 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -134,6 +134,10 @@ export interface AggFunctionsMapping { // // (undocumented) aggFilter: ReturnType; + // Warning: (ae-forgotten-export) The symbol "aggFilteredMetric" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggFilteredMetric: ReturnType; // Warning: (ae-forgotten-export) The symbol "aggFilters" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -405,7 +409,7 @@ export const ES_SEARCH_STRATEGY = "es"; // Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_34, Arguments_20, Output_34>; +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -485,7 +489,7 @@ export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', Ex // Warning: (ae-missing-release-tag) "ExpressionFunctionKibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_21, Promise, ExecutionContext>; +export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_22, Promise, ExecutionContext>; // Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1141,6 +1145,8 @@ export enum METRIC_TYPES { // (undocumented) DERIVATIVE = "derivative", // (undocumented) + FILTERED_METRIC = "filtered_metric", + // (undocumented) GEO_BOUNDS = "geo_bounds", // (undocumented) GEO_CENTROID = "geo_centroid", diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts index 732205fb31eab..9e2e248c6ccd5 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -63,6 +63,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition => '!moving_avg', '!cumulative_sum', '!geo_bounds', + '!filtered_metric', ], aggSettings: { top_hits: { diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index 020a317e0471b..2f30faa8e9a89 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -50,7 +50,7 @@ export const tableVisLegacyTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { defaultMessage: 'Metric', }), - aggFilter: ['!geo_centroid', '!geo_bounds'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], aggSettings: { top_hits: { allowStrings: true, diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 774742f02dde5..d645af3180b08 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -46,7 +46,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { defaultMessage: 'Metric', }), - aggFilter: ['!geo_centroid', '!geo_bounds'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], aggSettings: { top_hits: { allowStrings: true, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 6f2c6112064a9..4052ecbe21997 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -51,6 +51,7 @@ export const tagCloudVisTypeDefinition = { '!derivative', '!geo_bounds', '!geo_centroid', + '!filtered_metric', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index 172ce83b4f7c2..7e3ff8226fbb6 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -119,6 +119,7 @@ export const gaugeVisTypeDefinition: VisTypeDefinition = { '!moving_avg', '!cumulative_sum', '!geo_bounds', + '!filtered_metric', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index aaeae4f675f3f..468651bb4cf4c 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -83,6 +83,7 @@ export const goalVisTypeDefinition: VisTypeDefinition = { '!moving_avg', '!cumulative_sum', '!geo_bounds', + '!filtered_metric', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index 6f6160f3756fd..8d538399f68b2 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -94,6 +94,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition = { 'cardinality', 'std_dev', 'top_hits', + '!filtered_metric', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/area.ts b/src/plugins/vis_type_xy/public/vis_types/area.ts index a61c25bbc075a..dfe9bc2f42b84 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.ts +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -133,7 +133,7 @@ export const getAreaVisTypeDefinition = ( title: i18n.translate('visTypeXy.area.metricsTitle', { defaultMessage: 'Y-axis', }), - aggFilter: ['!geo_centroid', '!geo_bounds'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.ts b/src/plugins/vis_type_xy/public/vis_types/histogram.ts index 2c2a83b48802d..ba20502a3b9af 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -137,7 +137,7 @@ export const getHistogramVisTypeDefinition = ( defaultMessage: 'Y-axis', }), min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], defaults: [{ schema: 'metric', type: 'count' }], }, { diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index 75c4ddd75d0b3..62da0448e56bd 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -136,7 +136,7 @@ export const getHorizontalBarVisTypeDefinition = ( defaultMessage: 'Y-axis', }), min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], defaults: [{ schema: 'metric', type: 'count' }], }, { diff --git a/src/plugins/vis_type_xy/public/vis_types/line.ts b/src/plugins/vis_type_xy/public/vis_types/line.ts index 87165a20592e5..5a9eb5198df35 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.ts +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -132,7 +132,7 @@ export const getLineVisTypeDefinition = ( name: 'metric', title: i18n.translate('visTypeXy.line.metricTitle', { defaultMessage: 'Y-axis' }), min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], defaults: [{ schema: 'metric', type: 'count' }], }, { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx new file mode 100644 index 0000000000000..ea5eb14d9c20e --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLink, EuiText, EuiPopover, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; + +export function AdvancedOptions(props: { + options: Array<{ + title: string; + dataTestSubj: string; + onClick: () => void; + showInPopover: boolean; + inlineElement: React.ReactElement | null; + }>; +}) { + const [popoverOpen, setPopoverOpen] = useState(false); + const popoverOptions = props.options.filter((option) => option.showInPopover); + const inlineOptions = props.options + .filter((option) => option.inlineElement) + .map((option) => React.cloneElement(option.inlineElement!, { key: option.dataTestSubj })); + + return ( + <> + {popoverOptions.length > 0 && ( + + + { + setPopoverOpen(!popoverOpen); + }} + > + {i18n.translate('xpack.lens.indexPattern.advancedSettings', { + defaultMessage: 'Add advanced options', + })} + + } + isOpen={popoverOpen} + closePopover={() => { + setPopoverOpen(false); + }} + > + {popoverOptions.map(({ dataTestSubj, onClick, title }, index) => ( + + + { + setPopoverOpen(false); + onClick(); + }} + > + {title} + + + {popoverOptions.length - 1 !== index && } + + ))} + + + )} + {inlineOptions.length > 0 && ( + <> + + {inlineOptions} + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 08842fb755888..1fc755ec489c7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -32,6 +32,7 @@ import { resetIncomplete, FieldBasedIndexPatternColumn, canTransition, + DEFAULT_TIME_SCALE, } from '../operations'; import { mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -41,7 +42,9 @@ import { IndexPattern, IndexPatternLayer } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { FormatSelector } from './format_selector'; import { ReferenceEditor } from './reference_editor'; -import { TimeScaling } from './time_scaling'; +import { setTimeScaling, TimeScaling } from './time_scaling'; +import { defaultFilter, Filtering, setFilter } from './filtering'; +import { AdvancedOptions } from './advanced_options'; const operationPanels = getOperationDisplay(); @@ -156,6 +159,8 @@ export function DimensionEditor(props: DimensionEditorProps) { .filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type)); }, [fieldByOperation, operationWithoutField]); + const [filterByOpenInitially, setFilterByOpenInitally] = useState(false); + // Operations are compatible if they match inputs. They are always compatible in // the empty state. Field-based operations are not compatible with field-less operations. const operationsWithCompatibility = [...possibleOperations].map((operationType) => { @@ -458,11 +463,63 @@ export function DimensionEditor(props: DimensionEditorProps) { )} {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( - { + setStateWrapper( + setTimeScaling(columnId, state.layers[layerId], DEFAULT_TIME_SCALE) + ); + }, + showInPopover: Boolean( + operationDefinitionMap[selectedColumn.operationType].timeScalingMode && + operationDefinitionMap[selectedColumn.operationType].timeScalingMode !== + 'disabled' && + Object.values(state.layers[layerId].columns).some( + (col) => col.operationType === 'date_histogram' + ) && + !selectedColumn.timeScale + ), + inlineElement: ( + + ), + }, + { + title: i18n.translate('xpack.lens.indexPattern.filterBy.label', { + defaultMessage: 'Filter by', + }), + dataTestSubj: 'indexPattern-filter-by-enable', + onClick: () => { + setFilterByOpenInitally(true); + setStateWrapper(setFilter(columnId, state.layers[layerId], defaultFilter)); + }, + showInPopover: Boolean( + operationDefinitionMap[selectedColumn.operationType].filterable && + !selectedColumn.filter + ), + inlineElement: + operationDefinitionMap[selectedColumn.operationType].filterable && + selectedColumn.filter ? ( + + ) : null, + }, + ]} /> )} 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 a6d2361be21d4..d586818cb3c11 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 @@ -6,7 +6,7 @@ */ import { ReactWrapper, ShallowWrapper } from 'enzyme'; -import React, { ChangeEvent, MouseEvent } from 'react'; +import React, { ChangeEvent, MouseEvent, ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { EuiComboBox, @@ -15,6 +15,7 @@ import { EuiRange, EuiSelect, EuiButtonIcon, + EuiPopover, } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { @@ -30,10 +31,14 @@ import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { getFieldByNameFactory } from '../pure_helpers'; -import { TimeScaling } from './time_scaling'; import { DimensionEditor } from './dimension_editor'; +import { AdvancedOptions } from './advanced_options'; +import { Filtering } from './filtering'; jest.mock('../loader'); +jest.mock('../query_input', () => ({ + QueryInput: () => null, +})); jest.mock('../operations'); jest.mock('lodash', () => { const original = jest.requireActual('lodash'); @@ -1029,7 +1034,7 @@ describe('IndexPatternDimensionEditorPanel', () => { } it('should not show custom options if time scaling is not available', () => { - wrapper = mount( + wrapper = shallow( { })} /> ); - expect(wrapper.find('[data-test-subj="indexPattern-time-scaling"]')).toHaveLength(0); + expect( + wrapper + .find(DimensionEditor) + .dive() + .find(AdvancedOptions) + .dive() + .find('[data-test-subj="indexPattern-time-scaling-enable"]') + ).toHaveLength(0); }); it('should show custom options if time scaling is available', () => { - wrapper = mount(); + wrapper = shallow(); expect( wrapper - .find(TimeScaling) - .find('[data-test-subj="indexPattern-time-scaling-popover"]') - .exists() - ).toBe(true); + .find(DimensionEditor) + .dive() + .find(AdvancedOptions) + .dive() + .find('[data-test-subj="indexPattern-time-scaling-enable"]') + ).toHaveLength(1); }); it('should show current time scaling if set', () => { @@ -1066,7 +1080,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper .find(DimensionEditor) .dive() - .find(TimeScaling) + .find(AdvancedOptions) .dive() .find('[data-test-subj="indexPattern-time-scaling-enable"]') .prop('onClick')!({} as MouseEvent); @@ -1239,6 +1253,199 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); + describe('filtering', () => { + function getProps(colOverrides: Partial) { + return { + ...defaultProps, + state: getStateWithColumns({ + datecolumn: { + dataType: 'date', + isBucketed: true, + label: '', + customLabel: true, + operationType: 'date_histogram', + sourceField: 'ts', + params: { + interval: '1d', + }, + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + sourceField: 'Records', + ...colOverrides, + } as IndexPatternColumn, + }), + columnId: 'col2', + }; + } + + it('should not show custom options if time scaling is not available', () => { + wrapper = shallow( + + ); + expect( + wrapper + .find(DimensionEditor) + .dive() + .find(AdvancedOptions) + .dive() + .find('[data-test-subj="indexPattern-filter-by-enable"]') + ).toHaveLength(0); + }); + + it('should show custom options if filtering is available', () => { + wrapper = shallow(); + expect( + wrapper + .find(DimensionEditor) + .dive() + .find(AdvancedOptions) + .dive() + .find('[data-test-subj="indexPattern-filter-by-enable"]') + ).toHaveLength(1); + }); + + it('should show current filter if set', () => { + wrapper = mount( + + ); + expect( + (wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.value + ).toEqual({ language: 'kuery', query: 'a: b' }); + }); + + it('should allow to set filter initially', () => { + const props = getProps({}); + wrapper = shallow(); + wrapper + .find(DimensionEditor) + .dive() + .find(AdvancedOptions) + .dive() + .find('[data-test-subj="indexPattern-filter-by-enable"]') + .prop('onClick')!({} as MouseEvent); + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: { + language: 'kuery', + query: '', + }, + }), + }, + }, + }, + }, + { shouldRemoveDimension: false, shouldReplaceDimension: true } + ); + }); + + it('should carry over filter to other operation if possible', () => { + const props = getProps({ + filter: { language: 'kuery', query: 'a: b' }, + sourceField: 'bytes', + operationType: 'sum', + label: 'Sum of bytes per hour', + }); + wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') + .simulate('click'); + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: { language: 'kuery', query: 'a: b' }, + }), + }, + }, + }, + }, + { shouldRemoveDimension: false, shouldReplaceDimension: true } + ); + }); + + it('should allow to change filter', () => { + const props = getProps({ + filter: { language: 'kuery', query: 'a: b' }, + }); + wrapper = mount(); + (wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.onChange({ + language: 'kuery', + query: 'c: d', + }); + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: { language: 'kuery', query: 'c: d' }, + }), + }, + }, + }, + }, + { shouldRemoveDimension: false, shouldReplaceDimension: true } + ); + }); + + it('should allow to remove filter', () => { + const props = getProps({ + filter: { language: 'kuery', query: 'a: b' }, + }); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-filter-by-remove"]') + .find(EuiButtonIcon) + .prop('onClick')!( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any + ); + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: undefined, + }), + }, + }, + }, + }, + { shouldRemoveDimension: false, shouldReplaceDimension: true } + ); + }); + }); + it('should render invalid field if field reference is broken', () => { wrapper = mount( void; + isInitiallyOpen: boolean; +}) { + const [filterPopoverOpen, setFilterPopoverOpen] = useState(isInitiallyOpen); + const selectedOperation = operationDefinitionMap[selectedColumn.operationType]; + if (!selectedOperation.filterable || !selectedColumn.filter) { + return null; + } + + const isInvalid = !isQueryValid(selectedColumn.filter, indexPattern); + + return ( + + + + { + setFilterPopoverOpen(false); + }} + anchorClassName="eui-fullWidth" + panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" + button={ + + + {/* Empty for spacing */} + + { + setFilterPopoverOpen(!filterPopoverOpen); + }} + color={isInvalid ? 'danger' : 'text'} + title={i18n.translate('xpack.lens.indexPattern.filterBy.clickToEdit', { + defaultMessage: 'Click to edit', + })} + > + {selectedColumn.filter.query || + i18n.translate('xpack.lens.indexPattern.filterBy.emptyFilterQuery', { + defaultMessage: '(empty)', + })} + + + + + } + > + { + updateLayer(setFilter(columnId, layer, newQuery)); + }} + isInvalid={false} + onSubmit={() => {}} + /> + + + + { + updateLayer(setFilter(columnId, layer, undefined)); + }} + iconType="cross" + /> + + + + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx index a9362060b2dd0..bf5b64bf3d615 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx @@ -7,23 +7,11 @@ import { EuiToolTip } from '@elastic/eui'; import { EuiIcon } from '@elastic/eui'; -import { - EuiLink, - EuiFormRow, - EuiSelect, - EuiFlexItem, - EuiFlexGroup, - EuiButtonIcon, - EuiText, - EuiPopover, - EuiButtonEmpty, - EuiSpacer, -} from '@elastic/eui'; +import { EuiFormRow, EuiSelect, EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; +import React from 'react'; import { adjustTimeScaleLabelSuffix, - DEFAULT_TIME_SCALE, IndexPatternColumn, operationDefinitionMap, } from '../operations'; @@ -64,7 +52,6 @@ export function TimeScaling({ layer: IndexPatternLayer; updateLayer: (newLayer: IndexPatternLayer) => void; }) { - const [popoverOpen, setPopoverOpen] = useState(false); const hasDateHistogram = layer.columnOrder.some( (colId) => layer.columns[colId].operationType === 'date_histogram' ); @@ -72,56 +59,12 @@ export function TimeScaling({ if ( !selectedOperation.timeScalingMode || selectedOperation.timeScalingMode === 'disabled' || - !hasDateHistogram + !hasDateHistogram || + !selectedColumn.timeScale ) { return null; } - if (!selectedColumn.timeScale) { - return ( - - - { - setPopoverOpen(!popoverOpen); - }} - > - {i18n.translate('xpack.lens.indexPattern.timeScale.advancedSettings', { - defaultMessage: 'Add advanced options', - })} - - } - isOpen={popoverOpen} - closePopover={() => { - setPopoverOpen(false); - }} - > - - { - setPopoverOpen(false); - updateLayer(setTimeScaling(columnId, layer, DEFAULT_TIME_SCALE)); - }} - > - {i18n.translate('xpack.lens.indexPattern.timeScale.enableTimeScale', { - defaultMessage: 'Normalize by unit', - })} - - - - - ); - } - return ( { ]); }); + it('should wrap filtered metrics in filtered metric aggregation', async () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + timeScale: 'h', + filter: { + language: 'kuery', + query: 'bytes > 5', + }, + }, + col2: { + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'avg', + timeScale: 'h', + }, + col3: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.aggs[0]).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "customBucket": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "filter": Array [ + "{\\"language\\":\\"kuery\\",\\"query\\":\\"bytes > 5\\"}", + ], + "id": Array [ + "col1-filter", + ], + "schema": Array [ + "bucket", + ], + }, + "function": "aggFilter", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "customMetric": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "col1-metric", + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggCount", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "enabled": Array [ + true, + ], + "id": Array [ + "col1", + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggFilteredMetric", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + it('should add time_scale and format function if time scale is set and supported', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index b02926fe03fc3..331aa528e6d55 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -82,6 +82,7 @@ export const counterRateOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale, + filter: previousColumn?.filter, params: getFormatFromPreviousColumn(previousColumn), }; }, @@ -106,4 +107,5 @@ export const counterRateOperation: OperationDefinition< )?.join(', '); }, timeScalingMode: 'mandatory', + filterable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 2bf46e3acad1b..1664f3639b598 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -77,6 +77,7 @@ export const cumulativeSumOperation: OperationDefinition< operationType: 'cumulative_sum', isBucketed: false, scale: 'ratio', + filter: previousColumn?.filter, references: referenceIds, params: getFormatFromPreviousColumn(previousColumn), }; @@ -101,4 +102,5 @@ export const cumulativeSumOperation: OperationDefinition< }) )?.join(', '); }, + filterable: true, }; 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/derivative.tsx index 2d4b69a151115..5fb0bc3a83528 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/derivative.tsx @@ -83,6 +83,7 @@ export const derivativeOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale: previousColumn?.timeScale, + filter: previousColumn?.filter, params: getFormatFromPreviousColumn(previousColumn), }; }, @@ -108,4 +109,5 @@ export const derivativeOperation: OperationDefinition< )?.join(', '); }, timeScalingMode: 'optional', + filterable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index f9dfc962ce8b7..0af750a1fd481 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -89,6 +89,7 @@ export const movingAverageOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale: previousColumn?.timeScale, + filter: previousColumn?.filter, params: { window: 5, ...getFormatFromPreviousColumn(previousColumn), @@ -119,6 +120,7 @@ export const movingAverageOperation: OperationDefinition< )?.join(', '); }, timeScalingMode: 'optional', + filterable: true, }; function MovingAverageParamEditor({ 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 80885a58e17f5..513bac14af7a3 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 @@ -70,6 +70,7 @@ export const cardinalityOperation: OperationDefinition ofName(getSafeName(column.sourceField, indexPattern)), buildColumn({ field, previousColumn }) { return { @@ -79,6 +80,7 @@ export const cardinalityOperation: OperationDefinition ({ isQueryValid: () => true, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index 569e394335648..f5428bf24348f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -8,13 +8,12 @@ import './filter_popover.scss'; import React, { MouseEventHandler, useEffect, useState } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FilterValue, defaultLabel, isQueryValid } from '.'; import { IndexPattern } from '../../../types'; -import { QueryStringInput, Query } from '../../../../../../../../src/plugins/data/public'; +import { Query } from '../../../../../../../../src/plugins/data/public'; import { LabelInput } from '../shared_components'; +import { QueryInput } from '../../../query_input'; export const FilterPopover = ({ filter, @@ -94,54 +93,3 @@ export const FilterPopover = ({ ); }; - -export const QueryInput = ({ - value, - onChange, - indexPattern, - isInvalid, - onSubmit, -}: { - value: Query; - onChange: (input: Query) => void; - indexPattern: IndexPattern; - isInvalid: boolean; - onSubmit: () => void; -}) => { - const [inputValue, setInputValue] = useState(value); - - useDebounce(() => onChange(inputValue), 256, [inputValue]); - - const handleInputChange = (input: Query) => { - setInputValue(input); - }; - - return ( - { - if (inputValue.query) { - onSubmit(); - } - }} - placeholder={ - inputValue.language === 'kuery' - ? i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderKql', { - defaultMessage: '{example}', - values: { example: 'method : "GET" or status : "404"' }, - }) - : i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderLucene', { - defaultMessage: '{example}', - values: { example: 'method:GET OR status:404' }, - }) - } - languageSwitcherPopoverAnchorPosition="rightDown" - /> - ); -}; 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 433c69466dc00..cdb93048c9a58 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 @@ -230,6 +230,7 @@ interface BaseOperationDefinitionProps { * If set to optional, time scaling won't be enabled by default and can be removed. */ timeScalingMode?: TimeScalingMode; + filterable?: boolean; getHelpMessage?: (props: HelpProps) => React.ReactNode; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 54d884c83020d..4f5c897fb5378 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -161,12 +161,14 @@ export const lastValueOperation: OperationDefinition { return buildExpressionFunction('aggTopHit', { id: columnId, 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 b24b08c0be0c3..1767b76e88202 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 @@ -99,6 +99,7 @@ function buildMetricOperation>({ isBucketed: false, scale: 'ratio', timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, + filter: previousColumn?.filter, params: getFormatFromPreviousColumn(previousColumn), } as T), onFieldChange: (oldColumn, field) => { @@ -118,6 +119,7 @@ function buildMetricOperation>({ }, getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), + filterable: true, } as OperationDefinition; } 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 4f915160a52a8..4a05e6f372d30 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 @@ -967,6 +967,49 @@ describe('state_helpers', () => { ); }); + it('should remove filter from the wrapped column if it gets wrapped (case new1)', () => { + const expectedColumn = { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, + }; + + const testFilter = { language: 'kuery', query: '' }; + + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { col1: { ...expectedColumn, filter: testFilter } }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + op: 'testReference' as OperationType, + visualizationGroups: [], + }); + + expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( + expect.objectContaining({ + referenceIds: ['id1'], + previousColumn: expect.objectContaining({ + // filter should be passed to the buildColumn function of the target operation + filter: testFilter, + }), + }) + ); + expect(result.columns).toEqual( + expect.objectContaining({ + // filter should be stripped from the original column + id1: expectedColumn, + col1: expect.any(Object), + }) + ); + }); + it('should create a new no-input operation to use as reference (case new2)', () => { // @ts-expect-error this function is not valid operationDefinitionMap.testReference.requiredReferences = [ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 3a67e8e464323..7853b7da7956e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -509,7 +509,18 @@ function applyReferenceTransition({ if (!hasExactMatch && isColumnValidAsReference({ validation, column: previousColumn })) { hasExactMatch = true; - const newLayer = { ...layer, columns: { ...layer.columns, [newId]: { ...previousColumn } } }; + const newLayer = { + ...layer, + columns: { + ...layer.columns, + [newId]: { + ...previousColumn, + // drop the filter for the referenced column because the wrapping operation + // is filterable as well and will handle it one level higher. + filter: operationDefinition.filterable ? undefined : previousColumn.filter, + }, + }, + }; layer = { ...layer, columnOrder: getColumnOrder(newLayer), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts index d5ea34f003561..429d881341e79 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -32,6 +32,7 @@ export const createMockedReferenceOperation = () => { references: args.referenceIds, }; }), + filterable: true, isTransferable: jest.fn(), toExpression: jest.fn().mockReturnValue([]), getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx new file mode 100644 index 0000000000000..50941148342c3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern } from './types'; +import { QueryStringInput, Query } from '../../../../../src/plugins/data/public'; + +export const QueryInput = ({ + value, + onChange, + indexPattern, + isInvalid, + onSubmit, + disableAutoFocus, +}: { + value: Query; + onChange: (input: Query) => void; + indexPattern: IndexPattern; + isInvalid: boolean; + onSubmit: () => void; + disableAutoFocus?: boolean; +}) => { + const [inputValue, setInputValue] = useState(value); + + useDebounce(() => onChange(inputValue), 256, [inputValue]); + + const handleInputChange = (input: Query) => { + setInputValue(input); + }; + + return ( + { + if (inputValue.query) { + onSubmit(); + } + }} + placeholder={ + inputValue.language === 'kuery' + ? i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderKql', { + defaultMessage: '{example}', + values: { example: 'method : "GET" or status : "404"' }, + }) + : i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderLucene', { + defaultMessage: '{example}', + values: { example: 'method:GET OR status:404' }, + }) + } + languageSwitcherPopoverAnchorPosition="rightDown" + /> + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 6a0fbb3006847..d786d781199b6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -7,6 +7,7 @@ import type { IUiSettingsClient } from 'kibana/public'; import { + AggFunctionsMapping, EsaggsExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition, } from '../../../../../src/plugins/data/public'; @@ -29,11 +30,32 @@ function getExpressionForLayer( indexPattern: IndexPattern, uiSettings: IUiSettingsClient ): ExpressionAstExpression | null { - const { columns, columnOrder } = layer; + const { columnOrder } = layer; if (columnOrder.length === 0 || !indexPattern) { return null; } + const columns = { ...layer.columns }; + Object.keys(columns).forEach((columnId) => { + const column = columns[columnId]; + const rootDef = operationDefinitionMap[column.operationType]; + if ( + 'references' in column && + rootDef.filterable && + rootDef.input === 'fullReference' && + column.filter + ) { + // inherit filter to all referenced operations + column.references.forEach((referenceColumnId) => { + const referencedColumn = columns[referenceColumnId]; + const referenceDef = operationDefinitionMap[column.operationType]; + if (referenceDef.filterable) { + columns[referenceColumnId] = { ...referencedColumn, filter: column.filter }; + } + }); + } + }); + const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const); if (columnEntries.length) { @@ -44,10 +66,37 @@ function getExpressionForLayer( if (def.input === 'fullReference') { expressions.push(...def.toExpression(layer, colId, indexPattern)); } else { + const wrapInFilter = Boolean(def.filterable && col.filter); + let aggAst = def.toEsAggsFn( + col, + wrapInFilter ? `${colId}-metric` : colId, + indexPattern, + layer, + uiSettings + ); + if (wrapInFilter) { + aggAst = buildExpressionFunction( + 'aggFilteredMetric', + { + id: colId, + enabled: true, + schema: 'metric', + customBucket: buildExpression([ + buildExpressionFunction('aggFilter', { + id: `${colId}-filter`, + enabled: true, + schema: 'bucket', + filter: JSON.stringify(col.filter), + }), + ]), + customMetric: buildExpression({ type: 'expression', chain: [aggAst] }), + } + ).toAst(); + } aggs.push( buildExpression({ type: 'expression', - chain: [def.toEsAggsFn(col, colId, indexPattern, layer, uiSettings)], + chain: [aggAst], }) ); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c07210f2e3eff..9b7a25ff62661 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12095,7 +12095,6 @@ "xpack.lens.indexPattern.terms.otherLabel": "その他", "xpack.lens.indexPattern.terms.size": "値の数", "xpack.lens.indexPattern.termsOf": "{name} のトップの値", - "xpack.lens.indexPattern.timeScale.advancedSettings": "高度なオプションを追加", "xpack.lens.indexPattern.timeScale.enableTimeScale": "単位で正規化", "xpack.lens.indexPattern.timeScale.label": "単位で正規化", "xpack.lens.indexPattern.timeScale.tooltip": "基本の日付間隔に関係なく、常に指定された時間単位のレートとして表示されるように値を正規化します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8a027942c4275..7bb27ee6626b8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12253,7 +12253,6 @@ "xpack.lens.indexPattern.terms.otherLabel": "其他", "xpack.lens.indexPattern.terms.size": "值数目", "xpack.lens.indexPattern.termsOf": "{name} 排名最前值", - "xpack.lens.indexPattern.timeScale.advancedSettings": "添加高级选项", "xpack.lens.indexPattern.timeScale.enableTimeScale": "按单位标准化", "xpack.lens.indexPattern.timeScale.label": "按单位标准化", "xpack.lens.indexPattern.timeScale.tooltip": "将值标准化为始终显示为每指定时间单位速率,无论基础日期时间间隔是多少。", From c218ce292af1cc90ce619fbc4f1b7459e3255d4b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Mar 2021 11:47:56 +0100 Subject: [PATCH 17/88] [Lens] Add ability to remove runtime field (#94949) --- .../indexpattern_datasource/datapanel.tsx | 46 ++++++++++-- .../indexpattern_datasource/field_item.tsx | 65 +++++++++++++---- .../indexpattern_datasource/field_list.tsx | 4 ++ .../fields_accordion.tsx | 4 ++ x-pack/plugins/lens/server/usage/schema.ts | 38 ++++++++++ .../schema/xpack_plugins.json | 72 +++++++++++++++++++ .../functional/apps/lens/runtime_fields.ts | 6 ++ .../test/functional/page_objects/lens_page.ts | 15 ++++ 8 files changed, 230 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 68a3e9056b05d..2cad77b003454 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -496,6 +496,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ }; }, []); + const refreshFieldList = useCallback(async () => { + const newlyMappedIndexPattern = await loadIndexPatterns({ + indexPatternsService: data.indexPatterns, + cache: {}, + patterns: [currentIndexPattern.id], + }); + onUpdateIndexPattern(newlyMappedIndexPattern[currentIndexPattern.id]); + }, [data, currentIndexPattern, onUpdateIndexPattern]); + const editField = useMemo( () => editPermission @@ -509,17 +518,39 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ fieldName, onSave: async () => { trackUiEvent(`save_field_${uiAction}`); - const newlyMappedIndexPattern = await loadIndexPatterns({ - indexPatternsService: data.indexPatterns, - cache: {}, - patterns: [currentIndexPattern.id], - }); - onUpdateIndexPattern(newlyMappedIndexPattern[currentIndexPattern.id]); + await refreshFieldList(); + }, + }); + } + : undefined, + [data, indexPatternFieldEditor, currentIndexPattern, editPermission, refreshFieldList] + ); + + const removeField = useMemo( + () => + editPermission + ? async (fieldName: string) => { + trackUiEvent('open_field_delete_modal'); + const indexPatternInstance = await data.indexPatterns.get(currentIndexPattern.id); + closeFieldEditor.current = indexPatternFieldEditor.openDeleteModal({ + ctx: { + indexPattern: indexPatternInstance, + }, + fieldName, + onDelete: async () => { + trackUiEvent('delete_field'); + await refreshFieldList(); }, }); } : undefined, - [data, indexPatternFieldEditor, currentIndexPattern, editPermission, onUpdateIndexPattern] + [ + currentIndexPattern.id, + data.indexPatterns, + editPermission, + indexPatternFieldEditor, + refreshFieldList, + ] ); const addField = useMemo( @@ -765,6 +796,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} editField={editField} + removeField={removeField} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 3094b6463fe15..8ae62e4d843c2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -73,6 +73,7 @@ export interface FieldItemProps { groupIndex: number; dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; editField?: (name: string) => void; + removeField?: (name: string) => void; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; } @@ -107,6 +108,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { groupIndex, dropOntoWorkspace, editField, + removeField, } = props; const [infoIsOpen, setOpen] = useState(false); @@ -122,6 +124,17 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { [editField, setOpen] ); + const closeAndRemove = useMemo( + () => + removeField + ? (name: string) => { + removeField(name); + setOpen(false); + } + : undefined, + [removeField, setOpen] + ); + const dropOntoWorkspaceAndClose = useCallback( (droppedField: DragDropIdentifier) => { dropOntoWorkspace(droppedField); @@ -270,6 +283,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { {...state} {...props} editField={closeAndEdit} + removeField={closeAndRemove} dropOntoWorkspace={dropOntoWorkspaceAndClose} /> @@ -285,12 +299,14 @@ function FieldPanelHeader({ hasSuggestionForField, dropOntoWorkspace, editField, + removeField, }: { field: IndexPatternField; indexPatternId: string; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; editField?: (name: string) => void; + removeField?: (name: string) => void; }) { const draggableField = { indexPatternId, @@ -302,7 +318,7 @@ function FieldPanelHeader({ }; return ( - +
{field.displayName}
@@ -315,20 +331,41 @@ function FieldPanelHeader({ field={draggableField} /> {editField && ( - - editField(field.name)} - iconType="pencil" - data-test-subj="lnsFieldListPanelEdit" - aria-label={i18n.translate('xpack.lens.indexPattern.editFieldLabel', { + + - + > + editField(field.name)} + iconType="pencil" + data-test-subj="lnsFieldListPanelEdit" + aria-label={i18n.translate('xpack.lens.indexPattern.editFieldLabel', { + defaultMessage: 'Edit index pattern field', + })} + /> + +
+ )} + {removeField && field.runtime && ( + + + removeField(field.name)} + iconType="trash" + data-test-subj="lnsFieldListPanelRemove" + color="danger" + aria-label={i18n.translate('xpack.lens.indexPattern.removeFieldLabel', { + defaultMessage: 'Remove index pattern field', + })} + /> + + )}
); @@ -347,6 +384,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { data: { fieldFormats }, dropOntoWorkspace, editField, + removeField, hasSuggestionForField, hideDetails, } = props; @@ -379,6 +417,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} editField={editField} + removeField={removeField} /> ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index 01ba0726d9e4d..ceeb1f5b1caf3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -53,6 +53,7 @@ export const FieldList = React.memo(function FieldList({ dropOntoWorkspace, hasSuggestionForField, editField, + removeField, }: { exists: (field: IndexPatternField) => boolean; fieldGroups: FieldGroups; @@ -68,6 +69,7 @@ export const FieldList = React.memo(function FieldList({ dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; editField?: (name: string) => void; + removeField?: (name: string) => void; }) { const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); @@ -144,6 +146,7 @@ export const FieldList = React.memo(function FieldList({ exists={exists(field)} field={field} editField={editField} + removeField={removeField} hideDetails={true} key={field.name} itemIndex={index} @@ -169,6 +172,7 @@ export const FieldList = React.memo(function FieldList({ helpTooltip={fieldGroup.helpText} exists={exists} editField={editField} + removeField={removeField} hideDetails={fieldGroup.hideDetails} hasLoaded={!!hasSyncedExistingFields} fieldsCount={fieldGroup.fields.length} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 74ea13a81539f..a00f25b04651b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -55,6 +55,7 @@ export interface FieldsAccordionProps { dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; editField?: (name: string) => void; + removeField?: (name: string) => void; } export const FieldsAccordion = memo(function InnerFieldsAccordion({ @@ -76,6 +77,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ dropOntoWorkspace, hasSuggestionForField, editField, + removeField, }: FieldsAccordionProps) { const renderField = useCallback( (field: IndexPatternField, index) => ( @@ -90,6 +92,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} editField={editField} + removeField={removeField} /> ), [ @@ -100,6 +103,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ hasSuggestionForField, groupIndex, editField, + removeField, ] ); diff --git a/x-pack/plugins/lens/server/usage/schema.ts b/x-pack/plugins/lens/server/usage/schema.ts index 158e62ee8cfd8..5c15855aca48a 100644 --- a/x-pack/plugins/lens/server/usage/schema.ts +++ b/x-pack/plugins/lens/server/usage/schema.ts @@ -34,6 +34,44 @@ const eventsSchema: MakeSchemaFrom = { xy_change_layer_display: { type: 'long' }, xy_layer_removed: { type: 'long' }, xy_layer_added: { type: 'long' }, + open_field_editor_edit: { + type: 'long', + _meta: { + description: + 'Number of times the user opened the editor flyout to edit a field from within Lens.', + }, + }, + open_field_editor_add: { + type: 'long', + _meta: { + description: + 'Number of times the user opened the editor flyout to add a field from within Lens.', + }, + }, + save_field_edit: { + type: 'long', + _meta: { + description: 'Number of times the user edited a field from within Lens.', + }, + }, + save_field_add: { + type: 'long', + _meta: { + description: 'Number of times the user added a field from within Lens.', + }, + }, + open_field_delete_modal: { + type: 'long', + _meta: { + description: 'Number of times the user opened the field delete modal from within Lens.', + }, + }, + delete_field: { + type: 'long', + _meta: { + description: 'Number of times the user deleted a field from within Lens.', + }, + }, indexpattern_dimension_operation_terms: { type: 'long', _meta: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index ed8e44072b914..6bb152433f2fb 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2033,6 +2033,42 @@ "indexpattern_dimension_operation_moving_average": { "type": "long", "_meta": { "description": "Number of times the moving average function was selected" } + }, + "open_field_editor_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + } + }, + "open_field_editor_add": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to add a field from within Lens." + } + }, + "save_field_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user edited a field from within Lens." + } + }, + "save_field_add": { + "type": "long", + "_meta": { + "description": "Number of times the user added a field from within Lens." + } + }, + "open_field_delete_modal": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the field delete modal from within Lens." + } + }, + "delete_field": { + "type": "long", + "_meta": { + "description": "Number of times the user deleted a field from within Lens." + } } } }, @@ -2198,6 +2234,42 @@ "indexpattern_dimension_operation_moving_average": { "type": "long", "_meta": { "description": "Number of times the moving average function was selected" } + }, + "open_field_editor_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + } + }, + "open_field_editor_add": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to add a field from within Lens." + } + }, + "save_field_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user edited a field from within Lens." + } + }, + "save_field_add": { + "type": "long", + "_meta": { + "description": "Number of times the user added a field from within Lens." + } + }, + "open_field_delete_modal": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the field delete modal from within Lens." + } + }, + "delete_field": { + "type": "long", + "_meta": { + "description": "Number of times the user deleted a field from within Lens." + } } } }, diff --git a/x-pack/test/functional/apps/lens/runtime_fields.ts b/x-pack/test/functional/apps/lens/runtime_fields.ts index 9b8ef3a8b6905..53fe9eab0e654 100644 --- a/x-pack/test/functional/apps/lens/runtime_fields.ts +++ b/x-pack/test/functional/apps/lens/runtime_fields.ts @@ -62,5 +62,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('abc'); }); + + it('should able to remove field', async () => { + await PageObjects.lens.clickField('runtimefield2'); + await PageObjects.lens.removeField(); + await PageObjects.lens.waitForFieldMissing('runtimefield2'); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 3858b5834d483..3c2f7f1268331 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -188,6 +188,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async removeField() { + await retry.try(async () => { + await testSubjects.click('lnsFieldListPanelRemove'); + await testSubjects.missingOrFail('lnsFieldListPanelRemove'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmModalConfirmButton'); + }); + }, + async searchField(name: string) { await testSubjects.setValue('lnsIndexPatternFieldSearch', name); }, @@ -198,6 +207,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async waitForFieldMissing(field: string) { + await retry.try(async () => { + await testSubjects.missingOrFail(`lnsFieldListPanelField-${field}`); + }); + }, + /** * Copies field to chosen destination that is defined by distance of `steps` * (right arrow presses) from it From cb61b4a8fd07069396c9697a1d0d386dcf65e75f Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 24 Mar 2021 12:09:19 +0100 Subject: [PATCH 18/88] [Discover] Move doc viewer table row actions to left (#95064) * Move doc viewer table actions to left * Improve SCSS --- .../components/doc_viewer/doc_viewer.scss | 6 +-- .../components/table/table_row.tsx | 40 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index 95a50b54b5364..f5a4180207c33 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -1,5 +1,5 @@ .kbnDocViewerTable { - @include euiBreakpoint('xs', 's') { + @include euiBreakpoint('xs', 's','m') { table-layout: fixed; } } @@ -52,7 +52,7 @@ white-space: nowrap; } .kbnDocViewer__buttons { - width: 96px; + width: 108px; // Show all icons if one is focused, &:focus-within { @@ -64,7 +64,7 @@ .kbnDocViewer__field { width: $euiSize * 10; - @include euiBreakpoint('xs', 's') { + @include euiBreakpoint('xs', 's', 'm') { width: $euiSize * 6; } } diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 731dbeed85cc8..5c6ae49770bc7 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -54,6 +54,26 @@ export function DocViewTableRow({ const key = field ? field : fieldMapping?.displayName; return ( + {typeof onFilter === 'function' && ( + + onFilter(fieldMapping, valueRaw, '+')} + /> + onFilter(fieldMapping, valueRaw, '-')} + /> + {typeof onToggleColumn === 'function' && ( + + )} + onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + + )} {field ? ( - {typeof onFilter === 'function' && ( - - onFilter(fieldMapping, valueRaw, '+')} - /> - onFilter(fieldMapping, valueRaw, '-')} - /> - {typeof onToggleColumn === 'function' && ( - - )} - onFilter('_exists_', field, '+')} - scripted={fieldMapping && fieldMapping.scripted} - /> - - )} ); } From e6f477021334e468770c3a3876041e448543d1a9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Mar 2021 13:24:31 +0100 Subject: [PATCH 19/88] [Lens] Add telemetry for popovers (#94903) --- .../public/indexpattern_datasource/help_popover.tsx | 8 +++++++- .../definitions/calculations/moving_average.tsx | 6 +++++- .../operations/definitions/date_histogram.tsx | 6 +++++- .../operations/definitions/ranges/range_editor.tsx | 6 +++++- x-pack/plugins/lens/server/usage/schema.ts | 4 ++++ .../schema/xpack_plugins.json | 12 ++++++++++++ 6 files changed, 38 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/help_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/help_popover.tsx index ca126ff4e40bf..0e4c1897743b4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/help_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/help_popover.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect } from 'react'; import { EuiIcon, EuiLink, @@ -15,6 +15,7 @@ import { EuiPopoverTitle, EuiText, } from '@elastic/eui'; +import { trackUiEvent } from '../lens_ui_telemetry'; import './help_popover.scss'; export const HelpPopoverButton = ({ @@ -50,6 +51,11 @@ export const HelpPopover = ({ isOpen: EuiPopoverProps['isOpen']; title?: string; }) => { + useEffect(() => { + if (isOpen) { + trackUiEvent('open_help_popover'); + } + }, [isOpen]); return ( { setIsPopoverOpen(!isPopoverOpen)}> + { + setIsPopoverOpen(!isPopoverOpen); + }} + > {i18n.translate('xpack.lens.indexPattern.movingAverage.helpText', { defaultMessage: 'How it works', })} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 8d40ed9b7f066..bd7a270fd7ad8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -355,7 +355,11 @@ const AutoDateHistogramPopover = ({ data }: { data: DataPublicPluginStart }) => setIsPopoverOpen(!isPopoverOpen)}> + { + setIsPopoverOpen(!isPopoverOpen); + }} + > {i18n.translate('xpack.lens.indexPattern.dateHistogram.autoHelpText', { defaultMessage: 'How it works', })} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx index 269c59822fefc..4851b6ff3ec97 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -33,7 +33,11 @@ const GranularityHelpPopover = () => { setIsPopoverOpen(!isPopoverOpen)}> + { + setIsPopoverOpen(!isPopoverOpen); + }} + > {i18n.translate('xpack.lens.indexPattern.ranges.granularityHelpText', { defaultMessage: 'How it works', })} diff --git a/x-pack/plugins/lens/server/usage/schema.ts b/x-pack/plugins/lens/server/usage/schema.ts index 5c15855aca48a..ab3945a0162a6 100644 --- a/x-pack/plugins/lens/server/usage/schema.ts +++ b/x-pack/plugins/lens/server/usage/schema.ts @@ -10,6 +10,10 @@ import { LensUsage } from './types'; const eventsSchema: MakeSchemaFrom = { app_query_change: { type: 'long' }, + open_help_popover: { + type: 'long', + _meta: { description: 'Number of times the user opened one of the in-product help popovers.' }, + }, indexpattern_field_info_click: { type: 'long' }, loaded: { type: 'long' }, app_filters_updated: { type: 'long' }, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 6bb152433f2fb..36488fceb2a1d 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1876,6 +1876,12 @@ "app_query_change": { "type": "long" }, + "open_help_popover": { + "type": "long", + "_meta": { + "description": "Number of times the user opened one of the in-product help popovers." + } + }, "indexpattern_field_info_click": { "type": "long" }, @@ -2077,6 +2083,12 @@ "app_query_change": { "type": "long" }, + "open_help_popover": { + "type": "long", + "_meta": { + "description": "Number of times the user opened one of the in-product help popovers." + } + }, "indexpattern_field_info_click": { "type": "long" }, From ad1004519afb7b2f236b1eb393bacd9f68207d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Wed, 24 Mar 2021 13:45:56 +0100 Subject: [PATCH 20/88] [Metrics UI] Fix preview charts for inventory alerts when using a filter (#94561) * Pass filterQuery instead of filterQueryText to invertory alerts preview chart * Add test to verify that filterQuery is sent to the expression chart * Improve test description Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../inventory/components/expression.test.tsx | 40 ++++++++++++++++++- .../inventory/components/expression.tsx | 3 +- 2 files changed, 41 insertions(+), 2 deletions(-) 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 891e98606264e..88d72300c2d6d 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { mountWithIntl, shallowWithIntl, nextTick } from '@kbn/test/jest'; // We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` import { coreMock as mockCoreMock } from 'src/core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -89,6 +89,44 @@ describe('Expression', () => { }, ]); }); + + it('should pass the elasticsearch query to the expression chart', async () => { + const FILTER_QUERY = + '{"bool":{"should":[{"match_phrase":{"host.name":"testHostName"}}],"minimum_should_match":1}}'; + + const alertParams = { + criteria: [ + { + metric: 'cpu', + timeSize: 1, + timeUnit: 'm', + threshold: [10], + comparator: Comparator.GT, + }, + ], + nodeType: undefined, + filterQueryText: 'host.name: "testHostName"', + filterQuery: FILTER_QUERY, + }; + + const wrapper = shallowWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + metadata={{}} + /> + ); + + const chart = wrapper.find('[data-test-subj="preview-chart"]'); + + expect(chart.prop('filterQuery')).toBe(FILTER_QUERY); + }); + describe('using custom metrics', () => { it('should prefill the alert using the context metadata', async () => { const currentOptions = { 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 9ce7162933f2d..b28c76d1cb374 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -319,9 +319,10 @@ export const Expressions: React.FC = (props) => { > ); From 326abc2a9472df5712497affa62d157b46946b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Wed, 24 Mar 2021 13:51:20 +0100 Subject: [PATCH 21/88] Add filters to query total groupings (#94792) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/query_total_grouping.test.ts | 127 ++++++++++++++++++ .../lib/query_total_groupings.ts | 30 +++-- 2 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_grouping.test.ts diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_grouping.test.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_grouping.test.ts new file mode 100644 index 0000000000000..cc5631ffcec9c --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_grouping.test.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { queryTotalGroupings } from './query_total_groupings'; + +describe('queryTotalGroupings', () => { + const ESSearchClientMock = jest.fn().mockReturnValue({}); + const defaultOptions = { + timerange: { + from: 1615972672011, + interval: '>=10s', + to: 1615976272012, + field: '@timestamp', + }, + indexPattern: 'testIndexPattern', + metrics: [], + dropLastBucket: true, + groupBy: ['testField'], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return 0 when there is no groupBy', async () => { + const { groupBy, ...options } = defaultOptions; + + const response = await queryTotalGroupings(ESSearchClientMock, options); + expect(response).toBe(0); + }); + + it('should return 0 when there is groupBy is empty', async () => { + const options = { + ...defaultOptions, + groupBy: [], + }; + + const response = await queryTotalGroupings(ESSearchClientMock, options); + expect(response).toBe(0); + }); + + it('should query ES with a timerange', async () => { + await queryTotalGroupings(ESSearchClientMock, defaultOptions); + + expect(ESSearchClientMock.mock.calls[0][0].body.query.bool.filter).toContainEqual({ + range: { + '@timestamp': { + gte: 1615972672011, + lte: 1615976272012, + format: 'epoch_millis', + }, + }, + }); + }); + + it('should query ES with a exist fields', async () => { + const options = { + ...defaultOptions, + groupBy: ['testField1', 'testField2'], + }; + + await queryTotalGroupings(ESSearchClientMock, options); + + expect(ESSearchClientMock.mock.calls[0][0].body.query.bool.filter).toContainEqual({ + exists: { field: 'testField1' }, + }); + + expect(ESSearchClientMock.mock.calls[0][0].body.query.bool.filter).toContainEqual({ + exists: { field: 'testField2' }, + }); + }); + + it('should query ES with a query filter', async () => { + const options = { + ...defaultOptions, + filters: [ + { + bool: { + should: [{ match_phrase: { field1: 'value1' } }], + minimum_should_match: 1, + }, + }, + ], + }; + + await queryTotalGroupings(ESSearchClientMock, options); + + expect(ESSearchClientMock.mock.calls[0][0].body.query.bool.filter).toContainEqual({ + bool: { + should: [ + { + match_phrase: { + field1: 'value1', + }, + }, + ], + minimum_should_match: 1, + }, + }); + }); + + it('should return 0 when there are no aggregations in the response', async () => { + const clientMock = jest.fn().mockReturnValue({}); + + const response = await queryTotalGroupings(clientMock, defaultOptions); + + expect(response).toBe(0); + }); + + it('should return the value of the aggregation in the response', async () => { + const clientMock = jest.fn().mockReturnValue({ + aggregations: { + count: { + value: 10, + }, + }, + }); + + const response = await queryTotalGroupings(clientMock, defaultOptions); + + expect(response).toBe(10); + }); +}); diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts index 92aa39d3bf820..b871fa21c111d 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts @@ -23,6 +23,23 @@ export const queryTotalGroupings = async ( return Promise.resolve(0); } + let filters: Array> = [ + { + range: { + [options.timerange.field]: { + gte: options.timerange.from, + lte: options.timerange.to, + format: 'epoch_millis', + }, + }, + }, + ...options.groupBy.map((field) => ({ exists: { field } })), + ]; + + if (options.filters) { + filters = [...filters, ...options.filters]; + } + const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -31,18 +48,7 @@ export const queryTotalGroupings = async ( size: 0, query: { bool: { - filter: [ - { - range: { - [options.timerange.field]: { - gte: options.timerange.from, - lte: options.timerange.to, - format: 'epoch_millis', - }, - }, - }, - ...options.groupBy.map((field) => ({ exists: { field } })), - ], + filter: filters, }, }, aggs: { From 6295ae71b5d3a2810767968780be7c7eaccc60d7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Mar 2021 14:04:19 +0100 Subject: [PATCH 22/88] [Lens] Cache avilable operations meta data (#95034) --- .../dimension_panel/operation_support.ts | 4 ++-- .../plugins/lens/public/indexpattern_datasource/loader.ts | 6 ++++++ .../indexpattern_datasource/operations/__mocks__/index.ts | 1 + .../public/indexpattern_datasource/operations/operations.ts | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index d462d73740aaa..801b1b17a1831 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { DatasourceDimensionDropProps } from '../../types'; import { OperationType } from '../indexpattern'; -import { getAvailableOperationsByMetadata } from '../operations'; +import { memoizedGetAvailableOperationsByMetadata } from '../operations'; import { IndexPatternPrivateState } from '../types'; export interface OperationSupportMatrix { @@ -30,7 +30,7 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + const filteredOperationsByMetadata = memoizedGetAvailableOperationsByMetadata( currentIndexPattern ).filter((operation) => props.filterOperations(operation.operationMetaData)); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 7a50b1e60d13f..7052a69ee6fb7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -29,6 +29,7 @@ import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/pub import { documentField } from './document_field'; import { readFromStorage, writeToStorage } from '../settings_storage'; import { getFieldByNameFactory } from './pure_helpers'; +import { memoizedGetAvailableOperationsByMetadata } from './operations'; type SetState = StateSetter; type IndexPatternsService = Pick; @@ -49,6 +50,11 @@ export async function loadIndexPatterns({ return cache; } + if (memoizedGetAvailableOperationsByMetadata.cache.clear) { + // clear operations meta data cache because index pattern reference may change + memoizedGetAvailableOperationsByMetadata.cache.clear(); + } + const allIndexPatterns = await Promise.allSettled( missingIds.map((id) => indexPatternsService.get(id)) ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index dfb86ec16da30..6ac208913af2e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -19,6 +19,7 @@ jest.spyOn(actualHelpers, 'getErrorMessages'); export const { getAvailableOperationsByMetadata, + memoizedGetAvailableOperationsByMetadata, getOperations, getOperationDisplay, getOperationTypesForField, 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 63671fe35e99e..140ebc813f6c1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { memoize } from 'lodash'; import { OperationMetadata } from '../../types'; import { operationDefinitionMap, @@ -187,3 +187,5 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { return Object.values(operationByMetadata); } + +export const memoizedGetAvailableOperationsByMetadata = memoize(getAvailableOperationsByMetadata); From edfdb7895773120d6ffb2b8670f85e10f6955a8c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 24 Mar 2021 14:05:08 +0100 Subject: [PATCH 23/88] [Console] Clean up use of `any` (#95065) * cleaned up autocomplete.ts and get_endpoint_from_postition.ts of anys * general clean up of lowering hanging fruit * cleaned up remaining anys, still need to test * fix remaining TS compilation issues * also tidy up use of "as any" and some more ": any" * addressed type issues and introduced the default editor settings object * clean up @ts-ignore * added comments to interface Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/components/console_menu.tsx | 5 +- .../application/components/settings_modal.tsx | 4 +- .../console_history/console_history.tsx | 2 +- .../console_history/history_viewer.tsx | 2 +- .../console_editor/apply_editor_settings.ts | 2 +- .../legacy/console_editor/editor.test.tsx | 4 +- .../editor/legacy/console_editor/editor.tsx | 2 +- .../editor/legacy/console_menu_actions.ts | 2 +- .../application/containers/settings.tsx | 22 ++- .../editor_context/editor_context.tsx | 8 +- .../application/contexts/request_context.tsx | 4 +- .../contexts/services_context.mock.ts | 5 +- .../application/contexts/services_context.tsx | 8 +- .../restore_request_from_history.ts | 8 +- .../use_restore_request_from_history.ts | 3 +- .../send_request_to_es.ts | 17 +- .../use_send_current_request_to_es/track.ts | 6 +- .../use_send_current_request_to_es.test.tsx | 4 +- .../application/hooks/use_set_input_editor.ts | 3 +- .../console/public/application/index.tsx | 4 +- .../legacy_core_editor/create_readonly.ts | 8 +- .../legacy_core_editor.test.mocks.ts | 2 +- .../legacy_core_editor/legacy_core_editor.ts | 47 +++--- .../models/legacy_core_editor/smart_resize.ts | 3 +- .../application/models/sense_editor/curl.ts | 4 +- .../models/sense_editor/sense_editor.ts | 27 +-- .../public/application/stores/editor.ts | 9 +- .../lib/ace_token_provider/token_provider.ts | 4 +- .../public/lib/autocomplete/autocomplete.ts | 154 +++++++++++------- .../components/full_request_component.ts | 2 +- .../get_endpoint_from_position.ts | 3 +- .../console/public/lib/autocomplete/types.ts | 61 +++++++ src/plugins/console/public/lib/es/es.ts | 8 +- src/plugins/console/public/lib/row_parser.ts | 2 +- .../lib/token_iterator/token_iterator.test.ts | 2 +- src/plugins/console/public/lib/utils/index.ts | 4 +- .../console/public/services/history.ts | 6 +- src/plugins/console/public/services/index.ts | 2 +- .../console/public/services/settings.ts | 32 ++-- .../console/public/services/storage.ts | 6 +- src/plugins/console/public/types/common.ts | 6 + .../console/public/types/core_editor.ts | 10 +- .../server/lib/elasticsearch_proxy_config.ts | 4 +- .../console/server/lib/proxy_config.ts | 19 ++- .../server/lib/proxy_config_collection.ts | 10 +- .../console/server/lib/proxy_request.test.ts | 2 +- .../console/server/lib/proxy_request.ts | 6 +- .../routes/api/console/proxy/body.test.ts | 10 +- .../api/console/proxy/create_handler.ts | 8 +- .../api/console/proxy/query_string.test.ts | 3 +- .../server/routes/api/console/proxy/stubs.ts | 8 +- .../services/spec_definitions_service.ts | 34 +++- 52 files changed, 402 insertions(+), 219 deletions(-) create mode 100644 src/plugins/console/public/lib/autocomplete/types.ts diff --git a/src/plugins/console/public/application/components/console_menu.tsx b/src/plugins/console/public/application/components/console_menu.tsx index 40e3ce9c44e26..6c5eb8646c58d 100644 --- a/src/plugins/console/public/application/components/console_menu.tsx +++ b/src/plugins/console/public/application/components/console_menu.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; interface Props { getCurl: () => Promise; getDocumentation: () => Promise; - autoIndent: (ev?: React.MouseEvent) => void; + autoIndent: (ev: React.MouseEvent) => void; addNotification?: (opts: { title: string }) => void; } @@ -84,8 +84,7 @@ export class ConsoleMenu extends Component { window.open(documentation, '_blank'); }; - // Using `any` here per this issue: https://github.com/elastic/eui/issues/2265 - autoIndent: any = (event: React.MouseEvent) => { + autoIndent = (event: React.MouseEvent) => { this.closePopover(); this.props.autoIndent(event); }; diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index 161b67500b47c..033bce42177be 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -32,7 +32,7 @@ export type AutocompleteOptions = 'fields' | 'indices' | 'templates'; interface Props { onSaveSettings: (newSettings: DevToolsSettings) => void; onClose: () => void; - refreshAutocompleteSettings: (selectedSettings: any) => void; + refreshAutocompleteSettings: (selectedSettings: DevToolsSettings['autocomplete']) => void; settings: DevToolsSettings; } @@ -233,7 +233,7 @@ export function DevToolsSettingsModal(props: Props) { return rest; })} idToSelectedMap={checkboxIdToSelectedMap} - onChange={(e: any) => { + onChange={(e: unknown) => { onAutocompleteChange(e as AutocompleteOptions); }} /> diff --git a/src/plugins/console/public/application/containers/console_history/console_history.tsx b/src/plugins/console/public/application/containers/console_history/console_history.tsx index 4d5c08705e0d5..7fbef6de80eef 100644 --- a/src/plugins/console/public/application/containers/console_history/console_history.tsx +++ b/src/plugins/console/public/application/containers/console_history/console_history.tsx @@ -53,7 +53,7 @@ export function ConsoleHistory({ close }: Props) { const selectedReq = useRef(null); const describeReq = useMemo(() => { - const _describeReq = (req: any) => { + const _describeReq = (req: { endpoint: string; time: string }) => { const endpoint = req.endpoint; const date = moment(req.time); diff --git a/src/plugins/console/public/application/containers/console_history/history_viewer.tsx b/src/plugins/console/public/application/containers/console_history/history_viewer.tsx index 00a7cf8afa2c0..605f9a254f228 100644 --- a/src/plugins/console/public/application/containers/console_history/history_viewer.tsx +++ b/src/plugins/console/public/application/containers/console_history/history_viewer.tsx @@ -20,7 +20,7 @@ import { applyCurrentSettings } from '../editor/legacy/console_editor/apply_edit interface Props { settings: DevToolsSettings; - req: any | null; + req: { method: string; endpoint: string; data: string; time: string } | null; } export function HistoryViewer({ settings, req }: Props) { diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts b/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts index 678ea1c387096..f84999b294742 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts @@ -14,7 +14,7 @@ export function applyCurrentSettings( editor: CoreEditor | CustomAceEditor, settings: DevToolsSettings ) { - if ((editor as any).setStyles) { + if ((editor as { setStyles?: Function }).setStyles) { (editor as CoreEditor).setStyles({ wrapLines: settings.wrapMode, fontSize: settings.fontSize + 'px', diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx index c4da04dfbf5a6..1732dd9572b90 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -27,7 +27,7 @@ import { // Mocked functions import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; - +import type { DevToolsSettings } from '../../../../../services'; import * as consoleMenuActions from '../console_menu_actions'; import { Editor } from './editor'; @@ -40,7 +40,7 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { - + diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 8b965480d077b..541ad8b0563a4 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -229,7 +229,7 @@ function EditorUI({ initialTextValue }: EditorProps) { getDocumentation={() => { return getDocumentation(editorInstanceRef.current!, docLinkVersion); }} - autoIndent={(event: any) => { + autoIndent={(event) => { autoIndent(editorInstanceRef.current!, event); }} addNotification={({ title }) => notifications.toasts.add({ title })} diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts b/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts index 7aa3e96464800..f1bacd62289fb 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts +++ b/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts @@ -9,7 +9,7 @@ import { getEndpointFromPosition } from '../../../../lib/autocomplete/get_endpoint_from_position'; import { SenseEditor } from '../../../models/sense_editor'; -export async function autoIndent(editor: SenseEditor, event: Event) { +export async function autoIndent(editor: SenseEditor, event: React.MouseEvent) { event.preventDefault(); await editor.autoIndent(); editor.getCoreEditor().getContainer().focus(); diff --git a/src/plugins/console/public/application/containers/settings.tsx b/src/plugins/console/public/application/containers/settings.tsx index 7028a479635f4..1282a0b389902 100644 --- a/src/plugins/console/public/application/containers/settings.tsx +++ b/src/plugins/console/public/application/containers/settings.tsx @@ -15,14 +15,20 @@ import { retrieveAutoCompleteInfo } from '../../lib/mappings/mappings'; import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings, Settings as SettingsService } from '../../services'; -const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToolsSettings) => { +const getAutocompleteDiff = ( + newSettings: DevToolsSettings, + prevSettings: DevToolsSettings +): AutocompleteOptions[] => { return Object.keys(newSettings.autocomplete).filter((key) => { // @ts-ignore return prevSettings.autocomplete[key] !== newSettings.autocomplete[key]; - }); + }) as AutocompleteOptions[]; }; -const refreshAutocompleteSettings = (settings: SettingsService, selectedSettings: any) => { +const refreshAutocompleteSettings = ( + settings: SettingsService, + selectedSettings: DevToolsSettings['autocomplete'] +) => { retrieveAutoCompleteInfo(settings, selectedSettings); }; @@ -44,12 +50,12 @@ const fetchAutocompleteSettingsIfNeeded = ( if (isSettingsChanged) { // If the user has changed one of the autocomplete settings, then we'll fetch just the // ones which have changed. - const changedSettings: any = autocompleteDiff.reduce( - (changedSettingsAccum: any, setting: string): any => { - changedSettingsAccum[setting] = newSettings.autocomplete[setting as AutocompleteOptions]; + const changedSettings: DevToolsSettings['autocomplete'] = autocompleteDiff.reduce( + (changedSettingsAccum, setting) => { + changedSettingsAccum[setting] = newSettings.autocomplete[setting]; return changedSettingsAccum; }, - {} + {} as DevToolsSettings['autocomplete'] ); retrieveAutoCompleteInfo(settings, changedSettings); } else if (isPollingChanged && newSettings.polling) { @@ -89,7 +95,7 @@ export function Settings({ onClose }: Props) { + refreshAutocompleteSettings={(selectedSettings) => refreshAutocompleteSettings(settings, selectedSettings) } settings={settings.toJSON()} diff --git a/src/plugins/console/public/application/contexts/editor_context/editor_context.tsx b/src/plugins/console/public/application/contexts/editor_context/editor_context.tsx index 32e5216a85d7e..d4f809b5fbfb3 100644 --- a/src/plugins/console/public/application/contexts/editor_context/editor_context.tsx +++ b/src/plugins/console/public/application/contexts/editor_context/editor_context.tsx @@ -11,11 +11,11 @@ import * as editor from '../../stores/editor'; import { DevToolsSettings } from '../../../services'; import { createUseContext } from '../create_use_context'; -const EditorReadContext = createContext(null as any); -const EditorActionContext = createContext>(null as any); +const EditorReadContext = createContext(editor.initialValue); +const EditorActionContext = createContext>(() => {}); export interface EditorContextArgs { - children: any; + children: JSX.Element; settings: DevToolsSettings; } @@ -25,7 +25,7 @@ export function EditorContextProvider({ children, settings }: EditorContextArgs) settings, })); return ( - + {children} ); diff --git a/src/plugins/console/public/application/contexts/request_context.tsx b/src/plugins/console/public/application/contexts/request_context.tsx index 38ac5c7163add..96ba1f69212b4 100644 --- a/src/plugins/console/public/application/contexts/request_context.tsx +++ b/src/plugins/console/public/application/contexts/request_context.tsx @@ -10,8 +10,8 @@ import React, { createContext, useReducer, Dispatch } from 'react'; import { createUseContext } from './create_use_context'; import * as store from '../stores/request'; -const RequestReadContext = createContext(null as any); -const RequestActionContext = createContext>(null as any); +const RequestReadContext = createContext(store.initialValue); +const RequestActionContext = createContext>(() => {}); export function RequestContextProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(store.reducer, store.initialValue); diff --git a/src/plugins/console/public/application/contexts/services_context.mock.ts b/src/plugins/console/public/application/contexts/services_context.mock.ts index 6c67aa37727b2..c4ac8ca25378b 100644 --- a/src/plugins/console/public/application/contexts/services_context.mock.ts +++ b/src/plugins/console/public/application/contexts/services_context.mock.ts @@ -9,6 +9,7 @@ import { notificationServiceMock } from '../../../../../core/public/mocks'; import { httpServiceMock } from '../../../../../core/public/mocks'; +import type { ObjectStorageClient } from '../../../common/types'; import { HistoryMock } from '../../services/history.mock'; import { SettingsMock } from '../../services/settings.mock'; import { StorageMock } from '../../services/storage.mock'; @@ -18,7 +19,7 @@ import { ContextValue } from './services_context'; export const serviceContextMock = { create: (): ContextValue => { - const storage = new StorageMock({} as any, 'test'); + const storage = new StorageMock(({} as unknown) as Storage, 'test'); const http = httpServiceMock.createSetupContract(); const api = createApi({ http }); const esHostService = createEsHostService({ api }); @@ -31,7 +32,7 @@ export const serviceContextMock = { settings: new SettingsMock(storage), history: new HistoryMock(storage), notifications: notificationServiceMock.createSetupContract(), - objectStorageClient: {} as any, + objectStorageClient: ({} as unknown) as ObjectStorageClient, }, docLinkVersion: 'NA', }; diff --git a/src/plugins/console/public/application/contexts/services_context.tsx b/src/plugins/console/public/application/contexts/services_context.tsx index 58587bf3030e2..53c021d4d0982 100644 --- a/src/plugins/console/public/application/contexts/services_context.tsx +++ b/src/plugins/console/public/application/contexts/services_context.tsx @@ -30,10 +30,10 @@ export interface ContextValue { interface ContextProps { value: ContextValue; - children: any; + children: JSX.Element; } -const ServicesContext = createContext(null as any); +const ServicesContext = createContext(null); export function ServicesContextProvider({ children, value }: ContextProps) { useEffect(() => { @@ -46,8 +46,8 @@ export function ServicesContextProvider({ children, value }: ContextProps) { export const useServicesContext = () => { const context = useContext(ServicesContext); - if (context === undefined) { + if (context == null) { throw new Error('useServicesContext must be used inside the ServicesContextProvider.'); } - return context; + return context!; }; diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts index f8537f9d0b3c4..85c9cf6b9f014 100644 --- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts +++ b/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts @@ -7,12 +7,10 @@ */ import RowParser from '../../../lib/row_parser'; +import { ESRequest } from '../../../types'; import { SenseEditor } from '../../models/sense_editor'; -/** - * This function is considered legacy and should not be changed or updated before we have editor - * interfaces in place (it's using a customized version of Ace directly). - */ -export function restoreRequestFromHistory(editor: SenseEditor, req: any) { + +export function restoreRequestFromHistory(editor: SenseEditor, req: ESRequest) { const coreEditor = editor.getCoreEditor(); let pos = coreEditor.getCurrentPosition(); let prefix = ''; diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts index 310d6c67b90bc..7c140e2b18975 100644 --- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts +++ b/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts @@ -8,10 +8,11 @@ import { useCallback } from 'react'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { ESRequest } from '../../../types'; import { restoreRequestFromHistory } from './restore_request_from_history'; export const useRestoreRequestFromHistory = () => { - return useCallback((req: any) => { + return useCallback((req: ESRequest) => { const editor = registry.getInputEditor(); restoreRequestFromHistory(editor, req); }, []); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index aeaa2f76816e4..a86cfd8890a5b 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -8,19 +8,14 @@ import { extractWarningMessages } from '../../../lib/utils'; import { XJson } from '../../../../../es_ui_shared/public'; -const { collapseLiteralStrings } = XJson; // @ts-ignore import * as es from '../../../lib/es/es'; import { BaseResponseType } from '../../../types'; -export interface EsRequestArgs { - requests: any; -} +const { collapseLiteralStrings } = XJson; -export interface ESRequestObject { - path: string; - data: any; - method: string; +export interface EsRequestArgs { + requests: Array<{ url: string; method: string; data: string[] }>; } export interface ESResponseObject { @@ -32,7 +27,7 @@ export interface ESResponseObject { } export interface ESRequestResult { - request: ESRequestObject; + request: { data: string; method: string; path: string }; response: ESResponseObject; } @@ -61,7 +56,7 @@ export function sendRequestToES(args: EsRequestArgs): Promise resolve(results); return; } - const req = requests.shift(); + const req = requests.shift()!; const esPath = req.url; const esMethod = req.method; let esData = collapseLiteralStrings(req.data.join('\n')); @@ -71,7 +66,7 @@ export function sendRequestToES(args: EsRequestArgs): Promise const startTime = Date.now(); es.send(esMethod, esPath, esData).always( - (dataOrjqXHR: any, textStatus: string, jqXhrORerrorThrown: any) => { + (dataOrjqXHR, textStatus: string, jqXhrORerrorThrown) => { if (reqId !== CURRENT_REQ_ID) { return; } diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts index c9b8cdec66a96..f5deefd627187 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts @@ -10,7 +10,11 @@ import { SenseEditor } from '../../models/sense_editor'; import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; import { MetricsTracker } from '../../../types'; -export const track = (requests: any[], editor: SenseEditor, trackUiMetric: MetricsTracker) => { +export const track = ( + requests: Array<{ method: string }>, + editor: SenseEditor, + trackUiMetric: MetricsTracker +) => { const coreEditor = editor.getCoreEditor(); // `getEndpointFromPosition` gets values from the server-side generated JSON files which // are a combination of JS, automatically generated JSON and manual overrides. That means diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx index e1fc323cd2d9d..63b5788316956 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx @@ -26,8 +26,8 @@ import { useSendCurrentRequestToES } from './use_send_current_request_to_es'; describe('useSendCurrentRequestToES', () => { let mockContextValue: ContextValue; - let dispatch: (...args: any[]) => void; - const contexts = ({ children }: { children?: any }) => ( + let dispatch: (...args: unknown[]) => void; + const contexts = ({ children }: { children: JSX.Element }) => ( {children} ); diff --git a/src/plugins/console/public/application/hooks/use_set_input_editor.ts b/src/plugins/console/public/application/hooks/use_set_input_editor.ts index 2c6dc101bee0e..c01dbbb0d2798 100644 --- a/src/plugins/console/public/application/hooks/use_set_input_editor.ts +++ b/src/plugins/console/public/application/hooks/use_set_input_editor.ts @@ -9,12 +9,13 @@ import { useCallback } from 'react'; import { useEditorActionContext } from '../contexts/editor_context'; import { instance as registry } from '../contexts/editor_context/editor_registry'; +import { SenseEditor } from '../models'; export const useSetInputEditor = () => { const dispatch = useEditorActionContext(); return useCallback( - (editor: any) => { + (editor: SenseEditor) => { dispatch({ type: 'setInputEditor', payload: editor }); registry.setInputEditor(editor); }, diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index 1237348af215c..0b41095f8cc19 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HttpSetup, NotificationsSetup } from 'src/core/public'; +import { HttpSetup, NotificationsSetup, I18nStart } from 'src/core/public'; import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings } from '../services'; @@ -20,7 +20,7 @@ import { createApi, createEsHostService } from './lib'; export interface BootDependencies { http: HttpSetup; docLinkVersion: string; - I18nContext: any; + I18nContext: I18nStart['Context']; notifications: NotificationsSetup; usageCollection?: UsageCollectionSetup; element: HTMLElement; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts index 43da86773d294..dc63f0dcd480c 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts @@ -13,7 +13,7 @@ import * as OutputMode from './mode/output'; import smartResize from './smart_resize'; export interface CustomAceEditor extends ace.Editor { - update: (text: string, mode?: any, cb?: () => void) => void; + update: (text: string, mode?: unknown, cb?: () => void) => void; append: (text: string, foldPrevious?: boolean, cb?: () => void) => void; } @@ -28,9 +28,9 @@ export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor { output.$blockScrolling = Infinity; output.resize = smartResize(output); - output.update = (val: string, mode?: any, cb?: () => void) => { + output.update = (val: string, mode?: unknown, cb?: () => void) => { if (typeof mode === 'function') { - cb = mode; + cb = mode as () => void; mode = void 0; } @@ -65,7 +65,7 @@ export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor { (function setupSession(session) { session.setMode('ace/mode/text'); - (session as any).setFoldStyle('markbeginend'); + ((session as unknown) as { setFoldStyle: (v: string) => void }).setFoldStyle('markbeginend'); session.setTabSize(2); session.setUseWrapMode(true); })(output.getSession()); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts index c39d4549de0b6..0ee15f7a559ae 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts @@ -13,7 +13,7 @@ jest.mock('./mode/worker', () => { // @ts-ignore window.Worker = function () { this.postMessage = () => {}; - (this as any).terminate = () => {}; + ((this as unknown) as { terminate: () => void }).terminate = () => {}; }; // @ts-ignore diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index eab5ac16d17db..fa118532aa52d 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -31,8 +31,8 @@ const rangeToAceRange = ({ start, end }: Range) => new _AceRange(start.lineNumber - 1, start.column - 1, end.lineNumber - 1, end.column - 1); export class LegacyCoreEditor implements CoreEditor { - private _aceOnPaste: any; - $actions: any; + private _aceOnPaste: Function; + $actions: JQuery; resize: () => void; constructor(private readonly editor: IAceEditor, actions: HTMLElement) { @@ -41,7 +41,9 @@ export class LegacyCoreEditor implements CoreEditor { const session = this.editor.getSession(); session.setMode(new InputMode.Mode()); - (session as any).setFoldStyle('markbeginend'); + ((session as unknown) as { setFoldStyle: (style: string) => void }).setFoldStyle( + 'markbeginend' + ); session.setTabSize(2); session.setUseWrapMode(true); @@ -72,7 +74,7 @@ export class LegacyCoreEditor implements CoreEditor { // torn down, e.g. by closing the History tab, and we don't need to do anything further. if (session.bgTokenizer) { // Wait until the bgTokenizer is done running before executing the callback. - if ((session.bgTokenizer as any).running) { + if (((session.bgTokenizer as unknown) as { running: boolean }).running) { setTimeout(check, checkInterval); } else { resolve(); @@ -197,7 +199,7 @@ export class LegacyCoreEditor implements CoreEditor { .addMarker(rangeToAceRange(range), 'ace_snippet-marker', 'fullLine', false); } - removeMarker(ref: any) { + removeMarker(ref: number) { this.editor.getSession().removeMarker(ref); } @@ -222,8 +224,10 @@ export class LegacyCoreEditor implements CoreEditor { } isCompleterActive() { - // Secrets of the arcane here. - return Boolean((this.editor as any).completer && (this.editor as any).completer.activated); + return Boolean( + ((this.editor as unknown) as { completer: { activated: unknown } }).completer && + ((this.editor as unknown) as { completer: { activated: unknown } }).completer.activated + ); } private forceRetokenize() { @@ -250,7 +254,7 @@ export class LegacyCoreEditor implements CoreEditor { this._aceOnPaste.call(this.editor, text); } - private setActionsBar = (value?: any, topOrBottom: 'top' | 'bottom' = 'top') => { + private setActionsBar = (value: number | null, topOrBottom: 'top' | 'bottom' = 'top') => { if (value === null) { this.$actions.css('visibility', 'hidden'); } else { @@ -271,7 +275,7 @@ export class LegacyCoreEditor implements CoreEditor { }; private hideActionsBar = () => { - this.setActionsBar(); + this.setActionsBar(null); }; execCommand(cmd: string) { @@ -295,7 +299,7 @@ export class LegacyCoreEditor implements CoreEditor { }); } - legacyUpdateUI(range: any) { + legacyUpdateUI(range: Range) { if (!this.$actions) { return; } @@ -360,14 +364,19 @@ export class LegacyCoreEditor implements CoreEditor { ace.define( 'ace/autocomplete/text_completer', ['require', 'exports', 'module'], - function (require: any, exports: any) { - exports.getCompletions = function ( - innerEditor: any, - session: any, - pos: any, - prefix: any, - callback: any - ) { + function ( + require: unknown, + exports: { + getCompletions: ( + innerEditor: unknown, + session: unknown, + pos: unknown, + prefix: unknown, + callback: (e: null | Error, values: string[]) => void + ) => void; + } + ) { + exports.getCompletions = function (innerEditor, session, pos, prefix, callback) { callback(null, []); }; } @@ -387,7 +396,7 @@ export class LegacyCoreEditor implements CoreEditor { DO_NOT_USE_2: IAceEditSession, pos: { row: number; column: number }, prefix: string, - callback: (...args: any[]) => void + callback: (...args: unknown[]) => void ) => { const position: Position = { lineNumber: pos.row + 1, diff --git a/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts b/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts index fdbaedce09187..83d7cd15e60eb 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts @@ -7,9 +7,10 @@ */ import { get, throttle } from 'lodash'; +import type { Editor } from 'brace'; // eslint-disable-next-line import/no-default-export -export default function (editor: any) { +export default function (editor: Editor) { const resize = editor.resize; const throttledResize = throttle(() => { diff --git a/src/plugins/console/public/application/models/sense_editor/curl.ts b/src/plugins/console/public/application/models/sense_editor/curl.ts index 299ccd0a1f6a6..74cbebf051d03 100644 --- a/src/plugins/console/public/application/models/sense_editor/curl.ts +++ b/src/plugins/console/public/application/models/sense_editor/curl.ts @@ -25,7 +25,7 @@ export function detectCURL(text: string) { export function parseCURL(text: string) { let state = 'NONE'; const out = []; - let body: any[] = []; + let body: string[] = []; let line = ''; const lines = text.trim().split('\n'); let matches; @@ -62,7 +62,7 @@ export function parseCURL(text: string) { } function unescapeLastBodyEl() { - const str = body.pop().replace(/\\([\\"'])/g, '$1'); + const str = body.pop()!.replace(/\\([\\"'])/g, '$1'); body.push(str); } diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index d6dd74f0fefe3..0f65d3f1e33e2 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -7,8 +7,10 @@ */ import _ from 'lodash'; -import RowParser from '../../../lib/row_parser'; + import { XJson } from '../../../../../es_ui_shared/public'; + +import RowParser from '../../../lib/row_parser'; import * as utils from '../../../lib/utils'; // @ts-ignore @@ -16,22 +18,20 @@ import * as es from '../../../lib/es/es'; import { CoreEditor, Position, Range } from '../../../types'; import { createTokenIterator } from '../../factories'; - -import Autocomplete from '../../../lib/autocomplete/autocomplete'; +import createAutocompleter from '../../../lib/autocomplete/autocomplete'; const { collapseLiteralStrings } = XJson; export class SenseEditor { - currentReqRange: (Range & { markerRef: any }) | null; - parser: any; + currentReqRange: (Range & { markerRef: unknown }) | null; + parser: RowParser; - // @ts-ignore - private readonly autocomplete: any; + private readonly autocomplete: ReturnType; constructor(private readonly coreEditor: CoreEditor) { this.currentReqRange = null; this.parser = new RowParser(this.coreEditor); - this.autocomplete = new (Autocomplete as any)({ + this.autocomplete = createAutocompleter({ coreEditor, parser: this.parser, }); @@ -114,7 +114,10 @@ export class SenseEditor { return this.coreEditor.setValue(data, reTokenizeAll); }; - replaceRequestRange = (newRequest: any, requestRange: Range) => { + replaceRequestRange = ( + newRequest: { method: string; url: string; data: string | string[] }, + requestRange: Range + ) => { const text = utils.textFromRequest(newRequest); if (requestRange) { this.coreEditor.replaceRange(requestRange, text); @@ -207,12 +210,12 @@ export class SenseEditor { const request: { method: string; data: string[]; - url: string | null; + url: string; range: Range; } = { method: '', data: [], - url: null, + url: '', range, }; @@ -284,7 +287,7 @@ export class SenseEditor { return []; } - const requests: any = []; + const requests: unknown[] = []; let rangeStartCursor = expandedRange.start.lineNumber; const endLineNumber = expandedRange.end.lineNumber; diff --git a/src/plugins/console/public/application/stores/editor.ts b/src/plugins/console/public/application/stores/editor.ts index 1de4712d9640f..3fdb0c3fd3422 100644 --- a/src/plugins/console/public/application/stores/editor.ts +++ b/src/plugins/console/public/application/stores/editor.ts @@ -9,8 +9,9 @@ import { Reducer } from 'react'; import { produce } from 'immer'; import { identity } from 'fp-ts/lib/function'; -import { DevToolsSettings } from '../../services'; +import { DevToolsSettings, DEFAULT_SETTINGS } from '../../services'; import { TextObject } from '../../../common/text_object'; +import { SenseEditor } from '../models'; export interface Store { ready: boolean; @@ -21,15 +22,15 @@ export interface Store { export const initialValue: Store = produce( { ready: false, - settings: null as any, + settings: DEFAULT_SETTINGS, currentTextObject: null, }, identity ); export type Action = - | { type: 'setInputEditor'; payload: any } - | { type: 'setCurrentTextObject'; payload: any } + | { type: 'setInputEditor'; payload: SenseEditor } + | { type: 'setCurrentTextObject'; payload: TextObject } | { type: 'updateSettings'; payload: DevToolsSettings }; export const reducer: Reducer = (state, action) => diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts index ac518d43ddf25..692528fb8bced 100644 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts +++ b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts @@ -63,7 +63,7 @@ export class AceTokensProvider implements TokensProvider { return null; } - const tokens: TokenInfo[] = this.session.getTokens(lineNumber - 1) as any; + const tokens = (this.session.getTokens(lineNumber - 1) as unknown) as TokenInfo[]; if (!tokens || !tokens.length) { // We are inside of the document but have no tokens for this line. Return an empty // array to represent this empty line. @@ -74,7 +74,7 @@ export class AceTokensProvider implements TokensProvider { } getTokenAt(pos: Position): Token | null { - const tokens: TokenInfo[] = this.session.getTokens(pos.lineNumber - 1) as any; + const tokens = (this.session.getTokens(pos.lineNumber - 1) as unknown) as TokenInfo[]; if (tokens) { return extractTokenFromAceTokenRow(pos.lineNumber, pos.column, tokens); } diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index e46b5cda3c3a9..d89a9f3d2e5e2 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -18,19 +18,21 @@ import { // @ts-ignore } from '../kb/kb'; +import { createTokenIterator } from '../../application/factories'; +import { Position, Token, Range, CoreEditor } from '../../types'; +import type RowParser from '../row_parser'; + import * as utils from '../utils'; // @ts-ignore import { populateContext } from './engine'; +import { AutoCompleteContext, ResultTerm } from './types'; // @ts-ignore import { URL_PATH_END_MARKER } from './components/index'; -import { createTokenIterator } from '../../application/factories'; -import { Position, Token, Range, CoreEditor } from '../../types'; +let lastEvaluatedToken: Token | null = null; -let lastEvaluatedToken: any = null; - -function isUrlParamsToken(token: any) { +function isUrlParamsToken(token: { type: string } | null) { switch ((token || {}).type) { case 'url.param': case 'url.equal': @@ -54,7 +56,7 @@ function isUrlParamsToken(token: any) { export function getCurrentMethodAndTokenPaths( editor: CoreEditor, pos: Position, - parser: any, + parser: RowParser, forceEndOfUrl?: boolean /* Flag for indicating whether we want to avoid early escape optimization. */ ) { const tokenIter = createTokenIterator({ @@ -62,8 +64,8 @@ export function getCurrentMethodAndTokenPaths( position: pos, }); const startPos = pos; - let bodyTokenPath: any = []; - const ret: any = {}; + let bodyTokenPath: string[] | null = []; + const ret: AutoCompleteContext = {}; const STATES = { looking_for_key: 0, // looking for a key but without jumping over anything but white space and colon. @@ -210,7 +212,12 @@ export function getCurrentMethodAndTokenPaths( ret.urlParamsTokenPath = null; ret.requestStartRow = tokenIter.getCurrentPosition().lineNumber; - let curUrlPart: any; + let curUrlPart: + | null + | string + | Array> + | undefined + | Record; while (t && isUrlParamsToken(t)) { switch (t.type) { @@ -240,7 +247,7 @@ export function getCurrentMethodAndTokenPaths( if (!ret.urlParamsTokenPath) { ret.urlParamsTokenPath = []; } - ret.urlParamsTokenPath.unshift(curUrlPart || {}); + ret.urlParamsTokenPath.unshift((curUrlPart as Record) || {}); curUrlPart = null; break; } @@ -268,7 +275,7 @@ export function getCurrentMethodAndTokenPaths( break; case 'url.slash': if (curUrlPart) { - ret.urlTokenPath.unshift(curUrlPart); + ret.urlTokenPath.unshift(curUrlPart as string); curUrlPart = null; } break; @@ -277,7 +284,7 @@ export function getCurrentMethodAndTokenPaths( } if (curUrlPart) { - ret.urlTokenPath.unshift(curUrlPart); + ret.urlTokenPath.unshift(curUrlPart as string); } if (!ret.bodyTokenPath && !ret.urlParamsTokenPath) { @@ -297,9 +304,15 @@ export function getCurrentMethodAndTokenPaths( } // eslint-disable-next-line -export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEditor; parser: any }) { +export default function ({ + coreEditor: editor, + parser, +}: { + coreEditor: CoreEditor; + parser: RowParser; +}) { function isUrlPathToken(token: Token | null) { - switch ((token || ({} as any)).type) { + switch ((token || ({} as Token)).type) { case 'url.slash': case 'url.comma': case 'url.part': @@ -309,8 +322,12 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } } - function addMetaToTermsList(list: any, meta: any, template?: string) { - return _.map(list, function (t: any) { + function addMetaToTermsList( + list: unknown[], + meta: unknown, + template?: string + ): Array<{ meta: unknown; template: unknown; name?: string }> { + return _.map(list, function (t) { if (typeof t !== 'object') { t = { name: t }; } @@ -318,8 +335,13 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito }); } - function applyTerm(term: any) { - const context = term.context; + function applyTerm(term: { + value?: string; + context?: AutoCompleteContext; + template?: { __raw: boolean; value: string }; + insertValue?: string; + }) { + const context = term.context!; // make sure we get up to date replacement info. addReplacementInfoToContext(context, editor.getCurrentPosition(), term.insertValue); @@ -346,7 +368,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } else { indentedTemplateLines = utils.jsonToString(term.template, true).split('\n'); } - let currentIndentation = editor.getLineValue(context.rangeToReplace.start.lineNumber); + let currentIndentation = editor.getLineValue(context.rangeToReplace!.start.lineNumber); currentIndentation = currentIndentation.match(/^\s*/)![0]; for ( let i = 1; @@ -374,8 +396,8 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito // disable listening to the changes we are making. editor.off('changeSelection', editorChangeListener); - if (context.rangeToReplace.start.column !== context.rangeToReplace.end.column) { - editor.replace(context.rangeToReplace, valueToInsert); + if (context.rangeToReplace!.start.column !== context.rangeToReplace!.end.column) { + editor.replace(context.rangeToReplace!, valueToInsert); } else { editor.insert(valueToInsert); } @@ -384,12 +406,12 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito // go back to see whether we have one of ( : { & [ do not require a comma. All the rest do. let newPos = { - lineNumber: context.rangeToReplace.start.lineNumber, + lineNumber: context.rangeToReplace!.start.lineNumber, column: - context.rangeToReplace.start.column + + context.rangeToReplace!.start.column + termAsString.length + - context.prefixToAdd.length + - (templateInserted ? 0 : context.suffixToAdd.length), + context.prefixToAdd!.length + + (templateInserted ? 0 : context.suffixToAdd!.length), }; const tokenIter = createTokenIterator({ @@ -406,7 +428,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito break; case 'punctuation.colon': nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - if ((nonEmptyToken || ({} as any)).type === 'paren.lparen') { + if ((nonEmptyToken || ({} as Token)).type === 'paren.lparen') { nonEmptyToken = parser.nextNonEmptyToken(tokenIter); newPos = tokenIter.getCurrentPosition(); if (nonEmptyToken && nonEmptyToken.value.indexOf('"') === 0) { @@ -429,7 +451,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito function getAutoCompleteContext(ctxEditor: CoreEditor, pos: Position) { // deduces all the parameters need to position and insert the auto complete - const context: any = { + const context: AutoCompleteContext = { autoCompleteSet: null, // instructions for what can be here endpoint: null, urlPath: null, @@ -501,7 +523,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito case 'whitespace': t = parser.prevNonEmptyToken(tokenIter); - switch ((t || ({} as any)).type) { + switch ((t || ({} as Token)).type) { case 'method': // we moved one back return 'path'; @@ -552,7 +574,11 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito return null; } - function addReplacementInfoToContext(context: any, pos: Position, replacingTerm?: any) { + function addReplacementInfoToContext( + context: AutoCompleteContext, + pos: Position, + replacingTerm?: unknown + ) { // extract the initial value, rangeToReplace & textBoxPosition // Scenarios for current token: @@ -605,7 +631,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito default: if (replacingTerm && context.updatedForToken.value === replacingTerm) { context.rangeToReplace = { - start: { lineNumber: pos.lineNumber, column: anchorToken.column }, + start: { lineNumber: pos.lineNumber, column: anchorToken.position.column }, end: { lineNumber: pos.lineNumber, column: @@ -645,7 +671,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } } - function addBodyPrefixSuffixToContext(context: any) { + function addBodyPrefixSuffixToContext(context: AutoCompleteContext) { // Figure out what happens next to the token to see whether it needs trailing commas etc. // Templates will be used if not destroying existing structure. @@ -680,9 +706,9 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } context.addTemplate = true; // extend range to replace to include all up to token - context.rangeToReplace.end.lineNumber = tokenIter.getCurrentTokenLineNumber(); - context.rangeToReplace.end.column = - tokenIter.getCurrentTokenColumn() + nonEmptyToken.value.length; + context.rangeToReplace!.end.lineNumber = tokenIter.getCurrentTokenLineNumber() as number; + context.rangeToReplace!.end.column = + (tokenIter.getCurrentTokenColumn() as number) + nonEmptyToken.value.length; // move one more time to check if we need a trailing comma nonEmptyToken = parser.nextNonEmptyToken(tokenIter); @@ -711,11 +737,11 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito insertingRelativeToToken = 0; } else { const pos = editor.getCurrentPosition(); - if (pos.column === context.updatedForToken.position.column) { + if (pos.column === context.updatedForToken!.position.column) { insertingRelativeToToken = -1; } else if ( pos.column < - context.updatedForToken.position.column + context.updatedForToken.value.length + context.updatedForToken!.position.column + context.updatedForToken!.value.length ) { insertingRelativeToToken = 0; } else { @@ -743,12 +769,12 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito return context; } - function addUrlParamsPrefixSuffixToContext(context: any) { + function addUrlParamsPrefixSuffixToContext(context: AutoCompleteContext) { context.prefixToAdd = ''; context.suffixToAdd = ''; } - function addMethodPrefixSuffixToContext(context: any) { + function addMethodPrefixSuffixToContext(context: AutoCompleteContext) { context.prefixToAdd = ''; context.suffixToAdd = ''; const tokenIter = createTokenIterator({ editor, position: editor.getCurrentPosition() }); @@ -761,12 +787,12 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } } - function addPathPrefixSuffixToContext(context: any) { + function addPathPrefixSuffixToContext(context: AutoCompleteContext) { context.prefixToAdd = ''; context.suffixToAdd = ''; } - function addMethodAutoCompleteSetToContext(context: any) { + function addMethodAutoCompleteSetToContext(context: AutoCompleteContext) { context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'].map((m, i) => ({ name: m, score: -i, @@ -774,7 +800,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito })); } - function addPathAutoCompleteSetToContext(context: any, pos: Position) { + function addPathAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) { const ret = getCurrentMethodAndTokenPaths(editor, pos, parser); context.method = ret.method; context.token = ret.token; @@ -783,10 +809,10 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito const components = getTopLevelUrlCompleteComponents(context.method); populateContext(ret.urlTokenPath, context, editor, true, components); - context.autoCompleteSet = addMetaToTermsList(context.autoCompleteSet, 'endpoint'); + context.autoCompleteSet = addMetaToTermsList(context.autoCompleteSet!, 'endpoint'); } - function addUrlParamsAutoCompleteSetToContext(context: any, pos: Position) { + function addUrlParamsAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) { const ret = getCurrentMethodAndTokenPaths(editor, pos, parser); context.method = ret.method; context.otherTokenValues = ret.otherTokenValues; @@ -813,7 +839,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito // zero length tokenPath is true return context; } - let tokenPath: any[] = []; + let tokenPath: string[] = []; const currentParam = ret.urlParamsTokenPath.pop(); if (currentParam) { tokenPath = Object.keys(currentParam); // single key object @@ -830,7 +856,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito return context; } - function addBodyAutoCompleteSetToContext(context: any, pos: Position) { + function addBodyAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) { const ret = getCurrentMethodAndTokenPaths(editor, pos, parser); context.method = ret.method; context.otherTokenValues = ret.otherTokenValues; @@ -859,7 +885,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito // needed for scope linking + global term resolving context.endpointComponentResolver = getEndpointBodyCompleteComponents; context.globalComponentResolver = getGlobalAutocompleteComponents; - let components; + let components: unknown; if (context.endpoint) { components = context.endpoint.bodyAutocompleteRootComponents; } else { @@ -935,15 +961,19 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } } - function getCompletions(position: Position, prefix: string, callback: (...args: any[]) => void) { + function getCompletions( + position: Position, + prefix: string, + callback: (e: Error | null, result: ResultTerm[] | null) => void + ) { try { const context = getAutoCompleteContext(editor, position); if (!context) { callback(null, []); } else { const terms = _.map( - context.autoCompleteSet.filter((term: any) => Boolean(term) && term.name != null), - function (term: any) { + context.autoCompleteSet!.filter((term) => Boolean(term) && term.name != null), + function (term) { if (typeof term !== 'object') { term = { name: term, @@ -951,7 +981,13 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } else { term = _.clone(term); } - const defaults: any = { + const defaults: { + value?: string; + meta: string; + score: number; + context: AutoCompleteContext; + completer?: { insertMatch: (v: unknown) => void }; + } = { value: term.name, meta: 'API', score: 0, @@ -969,7 +1005,10 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito } ); - terms.sort(function (t1: any, t2: any) { + terms.sort(function ( + t1: { score: number; name?: string }, + t2: { score: number; name?: string } + ) { /* score sorts from high to low */ if (t1.score > t2.score) { return -1; @@ -978,7 +1017,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito return 1; } /* names sort from low to high */ - if (t1.name < t2.name) { + if (t1.name! < t2.name!) { return -1; } if (t1.name === t2.name) { @@ -989,7 +1028,7 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito callback( null, - _.map(terms, function (t: any, i: any) { + _.map(terms, function (t, i) { t.insertValue = t.insertValue || t.value; t.value = '' + t.value; // normalize to strings t.score = -i; @@ -1010,8 +1049,13 @@ export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEdito getCompletions, // TODO: This needs to be cleaned up _test: { - getCompletions: (_editor: any, _editSession: any, pos: any, prefix: any, callback: any) => - getCompletions(pos, prefix, callback), + getCompletions: ( + _editor: unknown, + _editSession: unknown, + pos: Position, + prefix: string, + callback: (e: Error | null, result: ResultTerm[] | null) => void + ) => getCompletions(pos, prefix, callback), addReplacementInfoToContext, addChangeListener: () => editor.on('changeSelection', editorChangeListener), removeChangeListener: () => editor.off('changeSelection', editorChangeListener), diff --git a/src/plugins/console/public/lib/autocomplete/components/full_request_component.ts b/src/plugins/console/public/lib/autocomplete/components/full_request_component.ts index 935e3622bde04..64aefa7b4a121 100644 --- a/src/plugins/console/public/lib/autocomplete/components/full_request_component.ts +++ b/src/plugins/console/public/lib/autocomplete/components/full_request_component.ts @@ -11,7 +11,7 @@ import { ConstantComponent } from './constant_component'; export class FullRequestComponent extends ConstantComponent { private readonly name: string; - constructor(name: string, parent: any, private readonly template: string) { + constructor(name: string, parent: unknown, private readonly template: string) { super(name, parent); this.name = name; } diff --git a/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts b/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts index 849123bc68a71..fe24492df0e7b 100644 --- a/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts +++ b/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts @@ -8,13 +8,14 @@ import { CoreEditor, Position } from '../../types'; import { getCurrentMethodAndTokenPaths } from './autocomplete'; +import type RowParser from '../row_parser'; // @ts-ignore import { getTopLevelUrlCompleteComponents } from '../kb/kb'; // @ts-ignore import { populateContext } from './engine'; -export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: any) { +export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: RowParser) { const lineValue = editor.getLineValue(pos.lineNumber); const context = { ...getCurrentMethodAndTokenPaths( diff --git a/src/plugins/console/public/lib/autocomplete/types.ts b/src/plugins/console/public/lib/autocomplete/types.ts new file mode 100644 index 0000000000000..33c543f43be9e --- /dev/null +++ b/src/plugins/console/public/lib/autocomplete/types.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 { CoreEditor, Range, Token } from '../../types'; + +export interface ResultTerm { + context?: AutoCompleteContext; + insertValue?: string; + name?: string; + value?: string; +} + +export interface AutoCompleteContext { + autoCompleteSet?: null | ResultTerm[]; + endpoint?: null | { + paramsAutocomplete: { + getTopLevelComponents: (method?: string | null) => unknown; + }; + bodyAutocompleteRootComponents: unknown; + id?: string; + documentation?: string; + }; + urlPath?: null | unknown; + urlParamsTokenPath?: Array> | null; + method?: string | null; + token?: Token; + activeScheme?: unknown; + replacingToken?: boolean; + rangeToReplace?: Range; + autoCompleteType?: null | string; + editor?: CoreEditor; + + /** + * The tokenized user input that prompted the current autocomplete at the cursor. This can be out of sync with + * the input that is currently being displayed in the editor. + */ + createdWithToken?: Token | null; + + /** + * The tokenized user input that is currently being displayed at the cursor in the editor when the user accepted + * the autocomplete suggestion. + */ + updatedForToken?: Token | null; + + addTemplate?: unknown; + prefixToAdd?: string; + suffixToAdd?: string; + textBoxPosition?: { lineNumber: number; column: number }; + urlTokenPath?: string[]; + otherTokenValues?: string; + requestStartRow?: number | null; + bodyTokenPath?: string[] | null; + endpointComponentResolver?: unknown; + globalComponentResolver?: unknown; + documentation?: string; +} diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 03ee218fa2e1d..dffc2c9682cf2 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -19,7 +19,7 @@ export function getVersion() { return esVersion; } -export function getContentType(body: any) { +export function getContentType(body: unknown) { if (!body) return; return 'application/json'; } @@ -27,7 +27,7 @@ export function getContentType(body: any) { export function send( method: string, path: string, - data: any, + data: string | object, { asSystemRequest }: SendOptions = {} ) { const wrappedDfd = $.Deferred(); @@ -47,10 +47,10 @@ export function send( }; $.ajax(options).then( - (responseData: any, textStatus: string, jqXHR: any) => { + (responseData, textStatus: string, jqXHR: unknown) => { wrappedDfd.resolveWith({}, [responseData, textStatus, jqXHR]); }, - ((jqXHR: any, textStatus: string, errorThrown: Error) => { + ((jqXHR: { status: number; responseText: string }, textStatus: string, errorThrown: Error) => { if (jqXHR.status === 0) { jqXHR.responseText = "\n\nFailed to connect to Console's backend.\nPlease check the Kibana server is up and running"; diff --git a/src/plugins/console/public/lib/row_parser.ts b/src/plugins/console/public/lib/row_parser.ts index 5f8fb08ca1d6f..e18bf6ac6446b 100644 --- a/src/plugins/console/public/lib/row_parser.ts +++ b/src/plugins/console/public/lib/row_parser.ts @@ -75,7 +75,7 @@ export default class RowParser { return MODE.REQUEST_START; } - rowPredicate(lineNumber: number | undefined, editor: CoreEditor, value: any) { + rowPredicate(lineNumber: number | undefined, editor: CoreEditor, value: number) { const mode = this.getRowParseMode(lineNumber); // eslint-disable-next-line no-bitwise return (mode & value) > 0; diff --git a/src/plugins/console/public/lib/token_iterator/token_iterator.test.ts b/src/plugins/console/public/lib/token_iterator/token_iterator.test.ts index bc28e1d9e1a03..6b2f556f82a0e 100644 --- a/src/plugins/console/public/lib/token_iterator/token_iterator.test.ts +++ b/src/plugins/console/public/lib/token_iterator/token_iterator.test.ts @@ -15,7 +15,7 @@ const mockTokensProviderFactory = (tokenMtx: Token[][]): TokensProvider => { return tokenMtx[lineNumber - 1] || null; }, getTokenAt(pos: Position): Token | null { - return null as any; + return null; }, }; }; diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index 71b305807e61d..8b8974f4e2f0d 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -11,7 +11,7 @@ import { XJson } from '../../../../es_ui_shared/public'; const { collapseLiteralStrings, expandLiteralStrings } = XJson; -export function textFromRequest(request: any) { +export function textFromRequest(request: { method: string; url: string; data: string | string[] }) { let data = request.data; if (typeof data !== 'string') { data = data.join('\n'); @@ -19,7 +19,7 @@ export function textFromRequest(request: any) { return request.method + ' ' + request.url + '\n' + data; } -export function jsonToString(data: any, indent: boolean) { +export function jsonToString(data: object, indent: boolean) { return JSON.stringify(data, null, indent ? 2 : 0); } diff --git a/src/plugins/console/public/services/history.ts b/src/plugins/console/public/services/history.ts index 1dd18f8672a4b..ee1e97ceb386e 100644 --- a/src/plugins/console/public/services/history.ts +++ b/src/plugins/console/public/services/history.ts @@ -35,12 +35,12 @@ export class History { // be triggered from different places in the app. The alternative would be to store // this in state so that we hook into the React model, but it would require loading history // every time the application starts even if a user is not going to view history. - change(listener: (reqs: any[]) => void) { + change(listener: (reqs: unknown[]) => void) { const subscription = this.changeEmitter.subscribe(listener); return () => subscription.unsubscribe(); } - addToHistory(endpoint: string, method: string, data: any) { + addToHistory(endpoint: string, method: string, data?: string) { const keys = this.getHistoryKeys(); keys.splice(0, MAX_NUMBER_OF_HISTORY_ITEMS); // only maintain most recent X; keys.forEach((key) => { @@ -59,7 +59,7 @@ export class History { this.changeEmitter.next(this.getHistory()); } - updateCurrentState(content: any) { + updateCurrentState(content: string) { const timestamp = new Date().getTime(); this.storage.set('editor_state', { time: timestamp, diff --git a/src/plugins/console/public/services/index.ts b/src/plugins/console/public/services/index.ts index b6bcafc974b93..2b1e64525d0f9 100644 --- a/src/plugins/console/public/services/index.ts +++ b/src/plugins/console/public/services/index.ts @@ -8,4 +8,4 @@ export { createHistory, History } from './history'; export { createStorage, Storage, StorageKeys } from './storage'; -export { createSettings, Settings, DevToolsSettings } from './settings'; +export { createSettings, Settings, DevToolsSettings, DEFAULT_SETTINGS } from './settings'; diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts index 8f142e876293e..647ac1e0ad09f 100644 --- a/src/plugins/console/public/services/settings.ts +++ b/src/plugins/console/public/services/settings.ts @@ -8,6 +8,14 @@ import { Storage } from './index'; +export const DEFAULT_SETTINGS = Object.freeze({ + fontSize: 14, + polling: true, + tripleQuotes: true, + wrapMode: true, + autocomplete: Object.freeze({ fields: true, indices: true, templates: true }), +}); + export interface DevToolsSettings { fontSize: number; wrapMode: boolean; @@ -24,50 +32,46 @@ export class Settings { constructor(private readonly storage: Storage) {} getFontSize() { - return this.storage.get('font_size', 14); + return this.storage.get('font_size', DEFAULT_SETTINGS.fontSize); } - setFontSize(size: any) { + setFontSize(size: number) { this.storage.set('font_size', size); return true; } getWrapMode() { - return this.storage.get('wrap_mode', true); + return this.storage.get('wrap_mode', DEFAULT_SETTINGS.wrapMode); } - setWrapMode(mode: any) { + setWrapMode(mode: boolean) { this.storage.set('wrap_mode', mode); return true; } - setTripleQuotes(tripleQuotes: any) { + setTripleQuotes(tripleQuotes: boolean) { this.storage.set('triple_quotes', tripleQuotes); return true; } getTripleQuotes() { - return this.storage.get('triple_quotes', true); + return this.storage.get('triple_quotes', DEFAULT_SETTINGS.tripleQuotes); } getAutocomplete() { - return this.storage.get('autocomplete_settings', { - fields: true, - indices: true, - templates: true, - }); + return this.storage.get('autocomplete_settings', DEFAULT_SETTINGS.autocomplete); } - setAutocomplete(settings: any) { + setAutocomplete(settings: object) { this.storage.set('autocomplete_settings', settings); return true; } getPolling() { - return this.storage.get('console_polling', true); + return this.storage.get('console_polling', DEFAULT_SETTINGS.polling); } - setPolling(polling: any) { + setPolling(polling: boolean) { this.storage.set('console_polling', polling); return true; } diff --git a/src/plugins/console/public/services/storage.ts b/src/plugins/console/public/services/storage.ts index d933024625e77..221020e496fec 100644 --- a/src/plugins/console/public/services/storage.ts +++ b/src/plugins/console/public/services/storage.ts @@ -17,11 +17,11 @@ export enum StorageKeys { export class Storage { constructor(private readonly engine: IStorageEngine, private readonly prefix: string) {} - encode(val: any) { + encode(val: unknown) { return JSON.stringify(val); } - decode(val: any) { + decode(val: string | null) { if (typeof val === 'string') { return JSON.parse(val); } @@ -37,7 +37,7 @@ export class Storage { } } - set(key: string, val: any) { + set(key: string, val: unknown) { this.engine.setItem(this.encodeKey(key), this.encode(val)); return val; } diff --git a/src/plugins/console/public/types/common.ts b/src/plugins/console/public/types/common.ts index 77b3d7c4477fc..53d896ad01d2f 100644 --- a/src/plugins/console/public/types/common.ts +++ b/src/plugins/console/public/types/common.ts @@ -11,6 +11,12 @@ export interface MetricsTracker { load: (eventName: string) => void; } +export interface ESRequest { + method: string; + endpoint: string; + data?: string; +} + export type BaseResponseType = | 'application/json' | 'text/csv' diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index f5e81f413d5c5..cc344d6bcc881 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -21,7 +21,7 @@ export type EditorEvent = export type AutoCompleterFunction = ( pos: Position, prefix: string, - callback: (...args: any[]) => void + callback: (...args: unknown[]) => void ) => void; export interface Position { @@ -235,7 +235,7 @@ export interface CoreEditor { * have this backdoor to update UI in response to request range changes, for example, as the user * moves the cursor around */ - legacyUpdateUI(opts: any): void; + legacyUpdateUI(opts: unknown): void; /** * A method to for the editor to resize, useful when, for instance, window size changes. @@ -250,7 +250,11 @@ export interface CoreEditor { /** * Register a keyboard shortcut and provide a function to be called. */ - registerKeyboardShortcut(opts: { keys: any; fn: () => void; name: string }): void; + registerKeyboardShortcut(opts: { + keys: string | { win?: string; mac?: string }; + fn: () => void; + name: string; + }): void; /** * Register a completions function that will be called when the editor diff --git a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts index 757142c87a119..bad6942d0c9af 100644 --- a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts +++ b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts @@ -14,7 +14,7 @@ import url from 'url'; import { ESConfigForProxy } from '../types'; const createAgent = (legacyConfig: ESConfigForProxy) => { - const target = url.parse(_.head(legacyConfig.hosts) as any); + const target = url.parse(_.head(legacyConfig.hosts)!); if (!/^https/.test(target.protocol || '')) return new http.Agent(); const agentOptions: https.AgentOptions = {}; @@ -28,7 +28,7 @@ const createAgent = (legacyConfig: ESConfigForProxy) => { agentOptions.rejectUnauthorized = true; // by default, NodeJS is checking the server identify - agentOptions.checkServerIdentity = _.noop as any; + agentOptions.checkServerIdentity = (_.noop as unknown) as https.AgentOptions['checkServerIdentity']; break; case 'full': agentOptions.rejectUnauthorized = true; diff --git a/src/plugins/console/server/lib/proxy_config.ts b/src/plugins/console/server/lib/proxy_config.ts index 556f6affca91d..8e2633545799b 100644 --- a/src/plugins/console/server/lib/proxy_config.ts +++ b/src/plugins/console/server/lib/proxy_config.ts @@ -12,6 +12,17 @@ import { Agent as HttpsAgent, AgentOptions } from 'https'; import { WildcardMatcher } from './wildcard_matcher'; +interface Config { + match: { + protocol: string; + host: string; + port: string; + path: string; + }; + ssl?: { verify?: boolean; ca?: string; cert?: string; key?: string }; + timeout: number; +} + export class ProxyConfig { // @ts-ignore private id: string; @@ -26,9 +37,9 @@ export class ProxyConfig { private readonly sslAgent?: HttpsAgent; - private verifySsl: any; + private verifySsl: undefined | boolean; - constructor(config: { match: any; timeout: number }) { + constructor(config: Config) { config = { ...config, }; @@ -61,8 +72,8 @@ export class ProxyConfig { this.sslAgent = this._makeSslAgent(config); } - _makeSslAgent(config: any) { - const ssl = config.ssl || {}; + _makeSslAgent(config: Config) { + const ssl: Config['ssl'] = config.ssl || {}; this.verifySsl = ssl.verify; const sslAgentOpts: AgentOptions = { diff --git a/src/plugins/console/server/lib/proxy_config_collection.ts b/src/plugins/console/server/lib/proxy_config_collection.ts index 83900838332b5..f6c0b7c659de0 100644 --- a/src/plugins/console/server/lib/proxy_config_collection.ts +++ b/src/plugins/console/server/lib/proxy_config_collection.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Agent } from 'http'; import { defaultsDeep } from 'lodash'; import { parse as parseUrl } from 'url'; @@ -14,7 +15,12 @@ import { ProxyConfig } from './proxy_config'; export class ProxyConfigCollection { private configs: ProxyConfig[]; - constructor(configs: Array<{ match: any; timeout: number }> = []) { + constructor( + configs: Array<{ + match: { protocol: string; host: string; port: string; path: string }; + timeout: number; + }> = [] + ) { this.configs = configs.map((settings) => new ProxyConfig(settings)); } @@ -22,7 +28,7 @@ export class ProxyConfigCollection { return Boolean(this.configs.length); } - configForUri(uri: string): object { + configForUri(uri: string): { agent: Agent; timeout: number } { const parsedUri = parseUrl(uri); const settings = this.configs.map((config) => config.getForParsedUri(parsedUri as any)); return defaultsDeep({}, ...settings); diff --git a/src/plugins/console/server/lib/proxy_request.test.ts b/src/plugins/console/server/lib/proxy_request.test.ts index 3fb915f0540b4..25257aa4c5579 100644 --- a/src/plugins/console/server/lib/proxy_request.test.ts +++ b/src/plugins/console/server/lib/proxy_request.test.ts @@ -55,7 +55,7 @@ describe(`Console's send request`, () => { fakeRequest = { abort: sinon.stub(), on() {}, - once(event: string, fn: any) { + once(event: string, fn: (v: string) => void) { if (event === 'response') { return fn('done'); } diff --git a/src/plugins/console/server/lib/proxy_request.ts b/src/plugins/console/server/lib/proxy_request.ts index d5914ab32bab7..46b4aa642a70e 100644 --- a/src/plugins/console/server/lib/proxy_request.ts +++ b/src/plugins/console/server/lib/proxy_request.ts @@ -46,9 +46,9 @@ export const proxyRequest = ({ const client = uri.protocol === 'https:' ? https : http; let resolved = false; - let resolve: any; - let reject: any; - const reqPromise = new Promise((res, rej) => { + let resolve: (res: http.IncomingMessage) => void; + let reject: (res: unknown) => void; + const reqPromise = new Promise((res, rej) => { resolve = res; reject = rej; }); diff --git a/src/plugins/console/server/routes/api/console/proxy/body.test.ts b/src/plugins/console/server/routes/api/console/proxy/body.test.ts index 64e2918764d00..80a2e075957de 100644 --- a/src/plugins/console/server/routes/api/console/proxy/body.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/body.test.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { IKibanaResponse } from 'src/core/server'; import { getProxyRouteHandlerDeps } from './mocks'; import { Readable } from 'stream'; @@ -16,10 +16,14 @@ import * as requestModule from '../../../../lib/proxy_request'; import { createResponseStub } from './stubs'; describe('Console Proxy Route', () => { - let request: any; + let request: ( + method: string, + path: string, + response?: string + ) => Promise | IKibanaResponse; beforeEach(() => { - request = (method: string, path: string, response: string) => { + request = (method, path, response) => { (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub(response)); const handler = createHandler(getProxyRouteHandlerDeps({})); diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 290a2cdec7b76..6a514483d14f2 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -41,7 +41,7 @@ function toURL(base: string, path: string) { } function filterHeaders(originalHeaders: object, headersToKeep: string[]): object { - const normalizeHeader = function (header: any) { + const normalizeHeader = function (header: string) { if (!header) { return ''; } @@ -68,7 +68,7 @@ function getRequestConfig( return { ...proxyConfigCollection.configForUri(uri), headers: newHeaders, - } as any; + }; } return { @@ -81,7 +81,7 @@ function getProxyHeaders(req: KibanaRequest) { const headers = Object.create(null); // Scope this proto-unsafe functionality to where it is being used. - function extendCommaList(obj: Record, property: string, value: any) { + function extendCommaList(obj: Record, property: string, value: string) { obj[property] = (obj[property] ? obj[property] + ',' : '') + value; } @@ -142,7 +142,7 @@ export const createHandler = ({ }; esIncomingMessage = await proxyRequest({ - method: method.toLowerCase() as any, + method: method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head', headers: requestHeaders, uri, timeout, diff --git a/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts b/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts index 19b5070236872..be4f1dbab942f 100644 --- a/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/query_string.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { IKibanaResponse } from 'src/core/server'; import { kibanaResponseFactory } from '../../../../../../../core/server'; import { getProxyRouteHandlerDeps } from './mocks'; import { createResponseStub } from './stubs'; @@ -14,7 +15,7 @@ import * as requestModule from '../../../../lib/proxy_request'; import { createHandler } from './create_handler'; describe('Console Proxy Route', () => { - let request: any; + let request: (method: string, path: string) => Promise | IKibanaResponse; beforeEach(() => { (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo')); diff --git a/src/plugins/console/server/routes/api/console/proxy/stubs.ts b/src/plugins/console/server/routes/api/console/proxy/stubs.ts index 4aa23f99ea30e..3474d0c9b33b8 100644 --- a/src/plugins/console/server/routes/api/console/proxy/stubs.ts +++ b/src/plugins/console/server/routes/api/console/proxy/stubs.ts @@ -9,8 +9,12 @@ import { IncomingMessage } from 'http'; import { Readable } from 'stream'; -export function createResponseStub(response: any) { - const resp: any = new Readable({ +export function createResponseStub(response?: string) { + const resp: Readable & { + statusCode?: number; + statusMessage?: string; + headers?: Record; + } = new Readable({ read() { if (response) { this.push(response); diff --git a/src/plugins/console/server/services/spec_definitions_service.ts b/src/plugins/console/server/services/spec_definitions_service.ts index 8b0f4c04ae0bd..e0af9422666af 100644 --- a/src/plugins/console/server/services/spec_definitions_service.ts +++ b/src/plugins/console/server/services/spec_definitions_service.ts @@ -15,6 +15,15 @@ import { jsSpecLoaders } from '../lib'; const PATH_TO_OSS_JSON_SPEC = resolve(__dirname, '../lib/spec_definitions/json'); +interface EndpointDescription { + methods?: string[]; + patterns?: string | string[]; + url_params?: Record; + data_autocomplete_rules?: Record; + url_components?: Record; + priority?: number; +} + export class SpecDefinitionsService { private readonly name = 'es'; @@ -24,16 +33,23 @@ export class SpecDefinitionsService { private hasLoadedSpec = false; - public addGlobalAutocompleteRules(parentNode: string, rules: any) { + public addGlobalAutocompleteRules(parentNode: string, rules: unknown) { this.globalRules[parentNode] = rules; } - public addEndpointDescription(endpoint: string, description: any = {}) { - let copiedDescription: any = {}; + public addEndpointDescription(endpoint: string, description: EndpointDescription = {}) { + let copiedDescription: { patterns?: string; url_params?: Record } = {}; if (this.endpoints[endpoint]) { copiedDescription = { ...this.endpoints[endpoint] }; } - let urlParamsDef: any; + let urlParamsDef: + | { + ignore_unavailable?: string; + allow_no_indices?: string; + expand_wildcards?: string[]; + } + | undefined; + _.each(description.patterns || [], function (p) { if (p.indexOf('{indices}') >= 0) { urlParamsDef = urlParamsDef || {}; @@ -70,7 +86,7 @@ export class SpecDefinitionsService { this.extensionSpecFilePaths.push(path); } - public addProcessorDefinition(processor: any) { + public addProcessorDefinition(processor: unknown) { if (!this.hasLoadedSpec) { throw new Error( 'Cannot add a processor definition because spec definitions have not loaded!' @@ -104,11 +120,13 @@ export class SpecDefinitionsService { return generatedFiles.reduce((acc, file) => { const overrideFile = overrideFiles.find((f) => basename(f) === basename(file)); - const loadedSpec = JSON.parse(readFileSync(file, 'utf8')); + const loadedSpec: Record = JSON.parse( + readFileSync(file, 'utf8') + ); if (overrideFile) { merge(loadedSpec, JSON.parse(readFileSync(overrideFile, 'utf8'))); } - const spec: any = {}; + const spec: Record = {}; Object.entries(loadedSpec).forEach(([key, value]) => { if (acc[key]) { // add time to remove key collision @@ -119,7 +137,7 @@ export class SpecDefinitionsService { }); return { ...acc, ...spec }; - }, {} as any); + }, {} as Record); } private loadJsonSpec() { From 2dea06f5a43c63d6ed63b1f537e70b07c19168b8 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 24 Mar 2021 13:43:00 +0000 Subject: [PATCH 24/88] skip flaky suite (#94545) --- test/functional/apps/discover/_data_grid_context.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 896cd4ad595c9..326fba9e6c087 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const browser = getService('browser'); - describe('discover data grid context tests', () => { + // FLAKY: https://github.com/elastic/kibana/issues/94545 + describe.skip('discover data grid context tests', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); From 017a2b8413e01c210f1f0ece1957f3e301091d38 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 24 Mar 2021 13:46:38 +0000 Subject: [PATCH 25/88] skip flaky suite (#94535) --- .../apis/security_solution/uncommon_processes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts index ecff7da6147be..fff8b98c8fd1f 100644 --- a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts @@ -27,7 +27,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('uncommon_processes', () => { + // FLAKY: https://github.com/elastic/kibana/issues/94535 + describe.skip('uncommon_processes', () => { before(() => esArchiver.load('auditbeat/uncommon_processes')); after(() => esArchiver.unload('auditbeat/uncommon_processes')); From d5883beb25fd868fffcde73ea1a74c9e9b21c3ab Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 24 Mar 2021 10:19:35 -0400 Subject: [PATCH 26/88] Adding ES query rule type to stack alerts feature privilege (#95225) --- x-pack/plugins/stack_alerts/server/feature.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index d421832a4cd97..9f91398cc7d24 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; +import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -21,7 +22,7 @@ export const BUILT_IN_ALERTS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [IndexThreshold, GeoContainment], + alerting: [IndexThreshold, GeoContainment, ElasticsearchQuery], privileges: { all: { app: [], @@ -30,7 +31,7 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [IndexThreshold, GeoContainment], + all: [IndexThreshold, GeoContainment, ElasticsearchQuery], read: [], }, savedObject: { @@ -48,7 +49,7 @@ export const BUILT_IN_ALERTS_FEATURE = { }, alerting: { all: [], - read: [IndexThreshold, GeoContainment], + read: [IndexThreshold, GeoContainment, ElasticsearchQuery], }, savedObject: { all: [], From edaa64f150d804123645778ea3de01e0c4b143d7 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 24 Mar 2021 15:38:58 +0100 Subject: [PATCH 27/88] [ML] Functional tests for Anomaly swim lane (#94723) * [ML] update @elastic/charts * [ML] swim lane service, axes tests * [ML] check single cell selection and current URL * [ML] clear selection * [ML] assert anomaly explorer charts * [ML] fix unit test * [ML] assert anomalies table and top influencers list * [ML] update apiDoc version * [ML] exclude host from the URL assertion * [ML] clicks view by swim lane * [ML] fix method for cell selection * [ML] brush action tests * [ML] use debug state flag * [ML] declare window interface * [ML] pagination tests * [ML] enable test * [ML] scroll into view for swim lane actions * [ML] rename URL assertion method * [ML] fix assertion for charts count * [ML] extend assertion * [ML] refactor test subjects selection * [ML] fix assertSelection * [ML] reduce timeout for charts assertion --- .../services/visualizations/elastic_chart.ts | 6 +- .../application/explorer/anomaly_timeline.tsx | 6 +- .../explorer_charts_container.js | 2 +- .../explorer_charts_container.test.js | 2 +- .../explorer/swimlane_container.tsx | 25 ++- .../explorer/swimlane_pagination.tsx | 17 +- x-pack/plugins/ml/server/routes/apidoc.json | 7 +- .../routes/apidoc_scripts/version_filter.ts | 2 +- .../ml/anomaly_detection/anomaly_explorer.ts | 188 ++++++++++++++++ .../functional/services/ml/anomalies_table.ts | 8 + .../services/ml/anomaly_explorer.ts | 25 +++ x-pack/test/functional/services/ml/index.ts | 3 + .../test/functional/services/ml/navigation.ts | 23 +- .../test/functional/services/ml/swim_lane.ts | 212 ++++++++++++++++++ 14 files changed, 504 insertions(+), 22 deletions(-) create mode 100644 x-pack/test/functional/services/ml/swim_lane.ts diff --git a/test/functional/services/visualizations/elastic_chart.ts b/test/functional/services/visualizations/elastic_chart.ts index 010394b275228..80483100a06dd 100644 --- a/test/functional/services/visualizations/elastic_chart.ts +++ b/test/functional/services/visualizations/elastic_chart.ts @@ -29,7 +29,11 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); class ElasticChart { - public async getCanvas() { + public async getCanvas(dataTestSubj?: string) { + if (dataTestSubj) { + const chart = await this.getChart(dataTestSubj); + return await chart.findByClassName('echCanvasRenderer'); + } return await find.byCssSelector('.echChart canvas:last-of-type'); } diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 4289986bb6a59..7c63d4087ce1e 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -160,7 +160,11 @@ export const AnomalyTimeline: FC = React.memo( {selectedCells ? ( - + - + {seriesToUse.length > 0 && seriesToUse.map((series) => ( { ); expect(wrapper.html()).toBe( - '
' + '
' ); }); diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 8deffa15cd6bd..c108257094b6a 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -26,6 +26,8 @@ import { HeatmapSpec, TooltipSettings, HeatmapBrushEvent, + Position, + ScaleType, } from '@elastic/charts'; import moment from 'moment'; @@ -44,6 +46,15 @@ import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; import { useUiSettings } from '../contexts/kibana'; +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + /** * Ignore insignificant resize, e.g. browser scrollbar appearance. */ @@ -352,7 +363,7 @@ export const SwimlaneContainer: FC = ({ direction={'column'} style={{ width: '100%', height: '100%', overflow: 'hidden' }} ref={resizeRef} - data-test-subj="mlSwimLaneContainer" + data-test-subj={dataTestSubj} > = ({ }} grow={false} > -
+
{showSwimlane && !isLoading && ( = ({ valueAccessor="value" highlightedData={highlightedData} valueFormatter={getFormattedSeverityScore} - xScaleType="time" + xScaleType={ScaleType.Time} ySortPredicate="dataIndex" config={swimLaneConfig} /> diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx index 8b205d2b8d5a1..18297d06dd6fe 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx @@ -57,6 +57,7 @@ export const SwimLanePagination: FC = ({ closePopover(); setPerPage(v); }} + data-test-subj={`${v} rows`} > = ({ iconType="arrowDown" iconSide="right" onClick={onButtonClick} + data-test-subj="mlSwimLanePageSizeControl" > - + + + } isOpen={isPopoverOpen} closePopover={closePopover} panelPaddingSize="none" > - + @@ -102,6 +106,7 @@ export const SwimLanePagination: FC = ({ pageCount={pageCount} activePage={componentFromPage} onPageClick={goToPage} + data-test-subj="mlSwimLanePagination" /> diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 1a10046380658..ba61a987d69ef 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -1,6 +1,6 @@ { "name": "ml_kibana_api", - "version": "7.11.0", + "version": "7.13.0", "description": "This is the documentation of the REST API provided by the Machine Learning Kibana plugin. Each API is experimental and can include breaking changes in any version.", "title": "ML Kibana API", "order": [ @@ -159,6 +159,9 @@ "GetTrainedModel", "GetTrainedModelStats", "GetTrainedModelPipelines", - "DeleteTrainedModel" + "DeleteTrainedModel", + + "Alerting", + "PreviewAlert" ] } diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts index ad00915f28d6d..430f105fb27d4 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/version_filter.ts @@ -7,7 +7,7 @@ import { Block } from './types'; -const API_VERSION = '7.8.0'; +const API_VERSION = '7.13.0'; /** * Post Filter parsed results. diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index 086b6c7e7f9d7..ff38544fa8c03 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -51,9 +52,15 @@ const testDataList = [ }, ]; +const cellSize = 15; + +const overallSwimLaneTestSubj = 'mlAnomalyExplorerSwimlaneOverall'; +const viewBySwimLaneTestSubj = 'mlAnomalyExplorerSwimlaneViewBy'; + export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); + const elasticChart = getService('elasticChart'); describe('anomaly explorer', function () { this.tags(['mlqa']); @@ -76,12 +83,16 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + await elasticChart.setNewChartUiDebugFlag(false); await ml.api.cleanMlIndices(); }); it('opens a job from job list link', async () => { await ml.testExecution.logTestStep('navigate to job list'); await ml.navigation.navigateToMl(); + // Set debug state has to happen at this point + // because page refresh happens after navigation to the ML app. + await elasticChart.setNewChartUiDebugFlag(true); await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in anomaly explorer'); @@ -126,6 +137,183 @@ export default function ({ getService }: FtrProviderContext) { await ml.anomaliesTable.assertTableNotEmpty(); }); + it('renders Overall swim lane', async () => { + await ml.testExecution.logTestStep('has correct axes labels'); + await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'x', [ + '2016-02-07 00:00', + '2016-02-08 00:00', + '2016-02-09 00:00', + '2016-02-10 00:00', + '2016-02-11 00:00', + '2016-02-12 00:00', + ]); + await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'y', ['Overall']); + }); + + it('renders View By swim lane', async () => { + await ml.testExecution.logTestStep('has correct axes labels'); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [ + '2016-02-07 00:00', + '2016-02-08 00:00', + '2016-02-09 00:00', + '2016-02-10 00:00', + '2016-02-11 00:00', + '2016-02-12 00:00', + ]); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [ + 'AAL', + 'VRD', + 'EGF', + 'SWR', + 'AMX', + 'JZA', + 'TRS', + 'ACA', + 'BAW', + 'ASA', + ]); + }); + + it('supports cell selection by click on Overall swim lane', async () => { + await ml.testExecution.logTestStep('checking page state before the cell selection'); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + + await ml.testExecution.logTestStep('clicks on the Overall swim lane cell'); + const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0]; + await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, { + x: sampleCell.x + cellSize, + y: sampleCell.y + cellSize, + }); + // TODO extend cell data with X and Y values, and cell width + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454846400000, 1454860800000], + y: ['Overall'], + }); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(true); + + await ml.testExecution.logTestStep('updates the View By swim lane'); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']); + + await ml.testExecution.logTestStep('renders anomaly explorer charts'); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(4); + + await ml.testExecution.logTestStep('updates top influencers list'); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); + + await ml.testExecution.logTestStep('updates anomalies table'); + await ml.anomaliesTable.assertTableRowsCount(4); + + await ml.testExecution.logTestStep('updates the URL state'); + await ml.navigation.assertCurrentURLContains( + 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t%2CviewByFieldName%3Aairline%2CviewByFromPage%3A1%2CviewByPerPage%3A10' + ); + + await ml.testExecution.logTestStep('clears the selection'); + await ml.anomalyExplorer.clearSwimLaneSelection(); + await ml.navigation.assertCurrentURLNotContain( + 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t%2CviewByFieldName%3Aairline%2CviewByFromPage%3A1%2CviewByPerPage%3A10' + ); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + }); + + it('allows to change the swim lane pagination', async () => { + await ml.testExecution.logTestStep('checks default pagination'); + await ml.swimLane.assertPageSize(viewBySwimLaneTestSubj, 10); + await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1); + + await ml.testExecution.logTestStep('updates pagination'); + await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 5); + + const axisLabels = await ml.swimLane.getAxisLabels(viewBySwimLaneTestSubj, 'y'); + expect(axisLabels.length).to.eql(5); + + await ml.swimLane.selectPage(viewBySwimLaneTestSubj, 3); + + await ml.testExecution.logTestStep('resets pagination'); + await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 10); + await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1); + }); + + it('supports cell selection by click on View By swim lane', async () => { + await ml.testExecution.logTestStep('checking page state before the cell selection'); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + + await ml.testExecution.logTestStep('clicks on the View By swim lane cell'); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + const sampleCell = (await ml.swimLane.getCells(viewBySwimLaneTestSubj))[0]; + await ml.swimLane.selectSingleCell(viewBySwimLaneTestSubj, { + x: sampleCell.x + cellSize, + y: sampleCell.y + cellSize, + }); + + await ml.testExecution.logTestStep('check page content'); + await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['AAL'], + }); + + await ml.anomaliesTable.assertTableRowsCount(1); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1); + + await ml.testExecution.logTestStep('highlights the Overall swim lane'); + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['Overall'], + }); + + await ml.testExecution.logTestStep('clears the selection'); + await ml.anomalyExplorer.clearSwimLaneSelection(); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + }); + + it('supports cell selection by brush action', async () => { + await ml.testExecution.logTestStep('checking page state before the cell selection'); + await ml.anomalyExplorer.assertClearSelectionButtonVisible(false); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + const cells = await ml.swimLane.getCells(viewBySwimLaneTestSubj); + + const sampleCell1 = cells[0]; + // Get cell from another row + const sampleCell2 = cells.find((c) => c.y !== sampleCell1.y); + + await ml.swimLane.selectCells(viewBySwimLaneTestSubj, { + x1: sampleCell1.x + cellSize, + y1: sampleCell1.y + cellSize, + x2: sampleCell2!.x + cellSize, + y2: sampleCell2!.y + cellSize, + }); + + await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { + x: [1454817600000, 1454846400000], + y: ['AAL', 'VRD'], + }); + + await ml.anomaliesTable.assertTableRowsCount(2); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(2); + + await ml.testExecution.logTestStep('clears the selection'); + await ml.anomalyExplorer.clearSwimLaneSelection(); + await ml.anomaliesTable.assertTableRowsCount(25); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0); + }); + it('adds swim lane embeddable to a dashboard', async () => { // should be the last step because it navigates away from the Anomaly Explorer page await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts index 54109e40a7526..30bb3e67bc862 100644 --- a/x-pack/test/functional/services/ml/anomalies_table.ts +++ b/x-pack/test/functional/services/ml/anomalies_table.ts @@ -22,6 +22,14 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide return await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); }, + async assertTableRowsCount(expectedCount: number) { + const actualCount = (await this.getTableRows()).length; + expect(actualCount).to.eql( + expectedCount, + `Expect anomaly table rows count to be ${expectedCount}, got ${actualCount}` + ); + }, + async getRowSubjByRowIndex(rowIndex: number) { const tableRows = await this.getTableRows(); expect(tableRows.length).to.be.greaterThan( diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts index 7fe53b1e3773a..4b1992777b8a7 100644 --- a/x-pack/test/functional/services/ml/anomaly_explorer.ts +++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts @@ -111,5 +111,30 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); }, + + async assertClearSelectionButtonVisible(expectVisible: boolean) { + if (expectVisible) { + await testSubjects.existOrFail('mlAnomalyTimelineClearSelection'); + } else { + await testSubjects.missingOrFail('mlAnomalyTimelineClearSelection'); + } + }, + + async clearSwimLaneSelection() { + await this.assertClearSelectionButtonVisible(true); + await testSubjects.click('mlAnomalyTimelineClearSelection'); + await this.assertClearSelectionButtonVisible(false); + }, + + async assertAnomalyExplorerChartsCount(expectedChartsCount: number) { + const chartsContainer = await testSubjects.find('mlExplorerChartsContainer'); + const actualChartsCount = ( + await chartsContainer.findAllByClassName('ml-explorer-chart-container', 3000) + ).length; + expect(actualChartsCount).to.eql( + expectedChartsCount, + `Expect ${expectedChartsCount} charts to appear, got ${actualChartsCount}` + ); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 894ba3d6ef07d..83c0c5e416434 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -45,6 +45,7 @@ import { MachineLearningTestExecutionProvider } from './test_execution'; import { MachineLearningTestResourcesProvider } from './test_resources'; import { MachineLearningDataVisualizerTableProvider } from './data_visualizer_table'; import { MachineLearningAlertingProvider } from './alerting'; +import { SwimLaneProvider } from './swim_lane'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -96,6 +97,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context); const alerting = MachineLearningAlertingProvider(context, commonUI); + const swimLane = SwimLaneProvider(context); return { anomaliesTable, @@ -134,6 +136,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { settingsCalendar, settingsFilterList, singleMetricViewer, + swimLane, testExecution, testResources, }; diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 93b8a5efecc07..075c788a86336 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -14,6 +14,7 @@ export function MachineLearningNavigationProvider({ getPageObjects, }: FtrProviderContext) { const retry = getService('retry'); + const browser = getService('browser'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common']); @@ -156,7 +157,7 @@ export function MachineLearningNavigationProvider({ }, async navigateToSingleMetricViewerViaAnomalyExplorer() { - // clicks the `Single Metric Viewere` icon on the button group to switch result views + // clicks the `Single Metric Viewer` icon on the button group to switch result views await testSubjects.click('mlAnomalyResultsViewSelectorSingleMetricViewer'); await retry.tryForTime(60 * 1000, async () => { // verify that the single metric viewer page is visible @@ -193,5 +194,25 @@ export function MachineLearningNavigationProvider({ await testSubjects.existOrFail('homeApp', { timeout: 2000 }); }); }, + + /** + * Assert the active URL. + * @param expectedUrlPart - URL component excluding host + */ + async assertCurrentURLContains(expectedUrlPart: string) { + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).to.include.string( + expectedUrlPart, + `Expected the current URL "${currentUrl}" to include ${expectedUrlPart}` + ); + }, + + async assertCurrentURLNotContain(expectedUrlPart: string) { + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).to.not.include.string( + expectedUrlPart, + `Expected the current URL "${currentUrl}" to not include ${expectedUrlPart}` + ); + }, }; } diff --git a/x-pack/test/functional/services/ml/swim_lane.ts b/x-pack/test/functional/services/ml/swim_lane.ts new file mode 100644 index 0000000000000..d659b24559a43 --- /dev/null +++ b/x-pack/test/functional/services/ml/swim_lane.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { DebugState } from '@elastic/charts'; +import { DebugStateAxis } from '@elastic/charts/dist/state/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +type HeatmapDebugState = Required>; + +export function SwimLaneProvider({ getService }: FtrProviderContext) { + const elasticChart = getService('elasticChart'); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + /** + * Y axis labels width + padding + */ + const xOffset = 185; + + /** + * Get coordinates relative to the left top corner of the canvas + * and transpose them from the center point. + */ + async function getCoordinatesFromCenter( + el: WebElementWrapper, + coordinates: { x: number; y: number } + ) { + const { width, height } = await el.getSize(); + + const elCenter = { + x: Math.round(width / 2), + y: Math.round(height / 2), + }; + + /** + * Origin of the element uses the center point, hence we need ot adjust + * the click coordinated accordingly. + */ + const resultX = xOffset + Math.round(coordinates.x) - elCenter.x; + const resultY = Math.round(coordinates.y) - elCenter.y; + + return { + x: resultX, + y: resultY, + }; + } + + const getRenderTracker = async (testSubj: string) => { + const renderCount = await elasticChart.getVisualizationRenderingCount(testSubj); + + return { + async verify() { + if (testSubj === 'mlAnomalyExplorerSwimlaneViewBy') { + // We have a glitchy behaviour when clicking on the View By swim lane. + // The entire charts is re-rendered, hence it requires a different check + await testSubjects.existOrFail(testSubj); + await elasticChart.waitForRenderComplete(testSubj); + } else { + await elasticChart.waitForRenderingCount(renderCount + 1, testSubj); + } + }, + }; + }; + + return { + async getDebugState(testSubj: string): Promise { + const state = await elasticChart.getChartDebugData(testSubj); + if (!state) { + throw new Error('Swim lane debug state is not available'); + } + return state as HeatmapDebugState; + }, + + async getAxisLabels(testSubj: string, axis: 'x' | 'y'): Promise { + const state = await this.getDebugState(testSubj); + return state.axes[axis][0].labels; + }, + + async assertAxisLabels(testSubj: string, axis: 'x' | 'y', expectedValues: string[]) { + const actualValues = await this.getAxisLabels(testSubj, axis); + expect(actualValues).to.eql( + expectedValues, + `Expected swim lane ${axis} labels to be ${expectedValues}, got ${actualValues}` + ); + }, + + async getCells(testSubj: string): Promise { + const state = await this.getDebugState(testSubj); + return state.heatmap.cells; + }, + + async getHighlighted(testSubj: string): Promise { + const state = await this.getDebugState(testSubj); + return state.heatmap.selection; + }, + + async assertSelection( + testSubj: string, + expectedData: HeatmapDebugState['heatmap']['selection']['data'], + expectedArea?: HeatmapDebugState['heatmap']['selection']['area'] + ) { + const actualSelection = await this.getHighlighted(testSubj); + expect(actualSelection.data).to.eql( + expectedData, + `Expected swim lane to have ${ + expectedData + ? `selected X-axis values ${expectedData.x.join( + ',' + )} and Y-axis values ${expectedData.y.join(',')}` + : 'no data selected' + }, got ${ + actualSelection.data + ? `${actualSelection.data.x.join(',')} and ${actualSelection.data.y.join(',')}` + : 'null' + }` + ); + if (expectedArea) { + expect(actualSelection.area).to.eql(expectedArea); + } + }, + + /** + * Selects a single cell + * @param testSubj + * @param x - number of pixels from the Y-axis + * @param y - number of pixels from the top of the canvas element + */ + async selectSingleCell(testSubj: string, { x, y }: { x: number; y: number }) { + await testSubjects.existOrFail(testSubj); + await testSubjects.scrollIntoView(testSubj); + const renderTracker = await getRenderTracker(testSubj); + const el = await elasticChart.getCanvas(testSubj); + + const { x: resultX, y: resultY } = await getCoordinatesFromCenter(el, { x, y }); + + await browser + .getActions() + .move({ x: resultX, y: resultY, origin: el._webElement }) + .click() + .perform(); + + await renderTracker.verify(); + }, + + async selectCells( + testSubj: string, + coordinates: { x1: number; x2: number; y1: number; y2: number } + ) { + await testSubjects.existOrFail(testSubj); + await testSubjects.scrollIntoView(testSubj); + const renderTracker = await getRenderTracker(testSubj); + + const el = await elasticChart.getCanvas(testSubj); + + const { x: resultX1, y: resultY1 } = await getCoordinatesFromCenter(el, { + x: coordinates.x1, + y: coordinates.y1, + }); + const { x: resultX2, y: resultY2 } = await getCoordinatesFromCenter(el, { + x: coordinates.x2, + y: coordinates.y2, + }); + + await browser.dragAndDrop( + { + location: el, + offset: { x: resultX1, y: resultY1 }, + }, + { + location: el, + offset: { x: resultX2, y: resultY2 }, + } + ); + + await renderTracker.verify(); + }, + + async assertActivePage(testSubj: string, expectedPage: number) { + const pagination = await testSubjects.find(`${testSubj} > mlSwimLanePagination`); + const activePage = await pagination.findByCssSelector( + '.euiPaginationButton-isActive .euiButtonEmpty__text' + ); + const text = await activePage.getVisibleText(); + expect(text).to.eql(expectedPage); + }, + + async assertPageSize(testSubj: string, expectedPageSize: number) { + const actualPageSize = await testSubjects.find( + `${testSubj} > ${expectedPageSize.toString()}` + ); + expect(await actualPageSize.isDisplayed()).to.be(true); + }, + + async selectPage(testSubj: string, page: number) { + await testSubjects.click(`${testSubj} > pagination-button-${page - 1}`); + await this.assertActivePage(testSubj, page); + }, + + async setPageSize(testSubj: string, rowsCount: 5 | 10 | 20 | 50 | 100) { + await testSubjects.click(`${testSubj} > mlSwimLanePageSizeControl`); + await testSubjects.existOrFail('mlSwimLanePageSizePanel'); + await testSubjects.click(`mlSwimLanePageSizePanel > ${rowsCount} rows`); + await this.assertPageSize(testSubj, rowsCount); + }, + }; +} From cf33d72442d2bf330e937d17dca9750d8a4bb756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 24 Mar 2021 15:52:41 +0100 Subject: [PATCH 28/88] [Logs UI] Tolerate log entries for which fields retrieval fails (#94972) --- .../log_entries/log_entries_search_strategy.ts | 10 +++++----- .../services/log_entries/log_entry_search_strategy.ts | 2 +- .../server/services/log_entries/queries/log_entries.ts | 2 +- .../server/services/log_entries/queries/log_entry.ts | 4 +++- 4 files changed, 10 insertions(+), 8 deletions(-) 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 190464ab6d5c1..161685aac29ad 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 @@ -203,13 +203,13 @@ const getLogEntryFromHit = ( } else if ('messageColumn' in column) { return { columnId: column.messageColumn.id, - message: messageFormattingRules.format(hit.fields, hit.highlight || {}), + message: messageFormattingRules.format(hit.fields ?? {}, hit.highlight || {}), }; } else { return { columnId: column.fieldColumn.id, field: column.fieldColumn.field, - value: hit.fields[column.fieldColumn.field] ?? [], + value: hit.fields?.[column.fieldColumn.field] ?? [], highlights: hit.highlight?.[column.fieldColumn.field] ?? [], }; } @@ -233,9 +233,9 @@ const pickRequestCursor = ( const getContextFromHit = (hit: LogEntryHit): LogEntryContext => { // Get all context fields, then test for the presence and type of the ones that go together - const containerId = hit.fields['container.id']?.[0]; - const hostName = hit.fields['host.name']?.[0]; - const logFilePath = hit.fields['log.file.path']?.[0]; + const containerId = hit.fields?.['container.id']?.[0]; + const hostName = hit.fields?.['host.name']?.[0]; + const logFilePath = hit.fields?.['log.file.path']?.[0]; if (typeof containerId === 'string') { return { 'container.id': containerId }; 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 2088761800cfe..85eacba823b2b 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 @@ -121,5 +121,5 @@ const createLogEntryFromHit = (hit: LogEntryHit) => ({ id: hit._id, index: hit._index, cursor: getLogEntryCursorFromHit(hit), - fields: Object.entries(hit.fields).map(([field, value]) => ({ field, value })), + fields: Object.entries(hit.fields ?? {}).map(([field, value]) => ({ field, value })), }); 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 460703b22766f..aa640f106d1ee 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 @@ -120,10 +120,10 @@ const createHighlightQuery = ( export const logEntryHitRT = rt.intersection([ commonHitFieldsRT, rt.type({ - fields: rt.record(rt.string, jsonArrayRT), sort: rt.tuple([rt.number, rt.number]), }), rt.partial({ + fields: rt.record(rt.string, jsonArrayRT), highlight: rt.record(rt.string, rt.array(rt.string)), }), ]); 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 74a12f14adcaa..51714be775e97 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 @@ -39,9 +39,11 @@ export const createGetLogEntryQuery = ( export const logEntryHitRT = rt.intersection([ commonHitFieldsRT, rt.type({ - fields: rt.record(rt.string, jsonArrayRT), sort: rt.tuple([rt.number, rt.number]), }), + rt.partial({ + fields: rt.record(rt.string, jsonArrayRT), + }), ]); export type LogEntryHit = rt.TypeOf; From 1527ab510b9d580b9f6f2b5dbbfa6f789118554c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 24 Mar 2021 16:01:52 +0100 Subject: [PATCH 29/88] [Search Sessions] Improve search session name edit test (#95152) --- x-pack/test/functional/services/search_sessions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/services/search_sessions.ts b/x-pack/test/functional/services/search_sessions.ts index 3ecb056c06074..34bea998925e5 100644 --- a/x-pack/test/functional/services/search_sessions.ts +++ b/x-pack/test/functional/services/search_sessions.ts @@ -72,7 +72,9 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { if (searchSessionName) { await testSubjects.click('searchSessionNameEdit'); - await testSubjects.setValue('searchSessionNameInput', searchSessionName); + await testSubjects.setValue('searchSessionNameInput', searchSessionName, { + clearWithKeyboard: true, + }); await testSubjects.click('searchSessionNameSave'); } From 759a52c74db62ac9d86416896f356b21eb8ec064 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 24 Mar 2021 08:36:23 -0700 Subject: [PATCH 30/88] [App Search] Add describe('listeners') blocks to older logic tests (#95215) * Add describe('listener') blocks to older logic tests * [Misc] LogRetentionLogic - move 2 it() blocks not within a describe() to its parent listener --- .../credentials/credentials_logic.test.ts | 2 + .../document_creation_logic.test.ts | 2 + .../documents/document_detail_logic.test.ts | 2 + .../components/engine/engine_logic.test.ts | 2 + .../engine_overview_logic.test.ts | 2 + .../log_retention/log_retention_logic.test.ts | 64 ++++++++++--------- 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index a178228f4996b..bf84b03e7603e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -1025,7 +1025,9 @@ describe('CredentialsLogic', () => { }); }); }); + }); + describe('listeners', () => { describe('fetchCredentials', () => { const meta = { page: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index 37d3d1577767f..2c6cadf9a8ece 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -294,7 +294,9 @@ describe('DocumentCreationLogic', () => { }); }); }); + }); + describe('listeners', () => { describe('onSubmitFile', () => { describe('with a valid file', () => { beforeAll(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index d2683fac649a0..add5e9414be13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -54,7 +54,9 @@ describe('DocumentDetailLogic', () => { }); }); }); + }); + describe('listeners', () => { describe('getDocumentDetails', () => { it('will call an API endpoint and then store the result', async () => { const fields = [{ name: 'name', value: 'python', type: 'string' }]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index bf2fba6344e7a..b9ec83db99f70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -172,7 +172,9 @@ describe('EngineLogic', () => { }); }); }); + }); + describe('listeners', () => { describe('initializeEngine', () => { it('fetches and sets engine data', async () => { mount({ engineName: 'some-engine' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index df8ed920e88df..decadba1092d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -78,7 +78,9 @@ describe('EngineOverviewLogic', () => { }); }); }); + }); + describe('listeners', () => { describe('pollForOverviewMetrics', () => { it('fetches data and calls onPollingSuccess', async () => { mount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index 19bd2af50aad9..7b63397ac6380 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -177,7 +177,9 @@ describe('LogRetentionLogic', () => { }); }); }); + }); + describe('listeners', () => { describe('saveLogRetention', () => { beforeEach(() => { mount(); @@ -264,6 +266,37 @@ describe('LogRetentionLogic', () => { LogRetentionOptions.Analytics ); }); + + it('will call saveLogRetention if NOT already enabled', () => { + mount({ + logRetention: { + [LogRetentionOptions.Analytics]: { + enabled: false, + }, + }, + }); + jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); + + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); + + expect(LogRetentionLogic.actions.saveLogRetention).toHaveBeenCalledWith( + LogRetentionOptions.Analytics, + true + ); + }); + + it('will do nothing if logRetention option is not yet set', () => { + mount({ + logRetention: {}, + }); + jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); + jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); + + LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.API); + + expect(LogRetentionLogic.actions.saveLogRetention).not.toHaveBeenCalled(); + expect(LogRetentionLogic.actions.setOpenedModal).not.toHaveBeenCalled(); + }); }); describe('fetchLogRetention', () => { @@ -306,36 +339,5 @@ describe('LogRetentionLogic', () => { expect(http.get).not.toHaveBeenCalled(); }); }); - - it('will call saveLogRetention if NOT already enabled', () => { - mount({ - logRetention: { - [LogRetentionOptions.Analytics]: { - enabled: false, - }, - }, - }); - jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); - - LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.Analytics); - - expect(LogRetentionLogic.actions.saveLogRetention).toHaveBeenCalledWith( - LogRetentionOptions.Analytics, - true - ); - }); - - it('will do nothing if logRetention option is not yet set', () => { - mount({ - logRetention: {}, - }); - jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); - jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); - - LogRetentionLogic.actions.toggleLogRetention(LogRetentionOptions.API); - - expect(LogRetentionLogic.actions.saveLogRetention).not.toHaveBeenCalled(); - expect(LogRetentionLogic.actions.setOpenedModal).not.toHaveBeenCalled(); - }); }); }); From 3639aa442283bdbdd9132860a77f267bc841e061 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Wed, 24 Mar 2021 11:44:22 -0400 Subject: [PATCH 31/88] [Fleet] Bulk reassign response should include all given ids (#95024) ## Summary `/agents/bulk_reassign` should return a response with a result for each agent given; including invalid or missing ids. It currently filters out missing or invalid before updating. This PR leaves them in and includes their error results in the response. [Added/updated tests](https://github.com/elastic/kibana/pull/95024/files#diff-7ec94bee3e2bae79e5d98b8c17c17b26fad14736143ffa144f3e035773d4cad1R113-R128) to confirm ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/common/types/rest_spec/agent.ts | 11 ++- .../fleet/server/routes/agent/handlers.ts | 20 ++-- .../fleet/server/services/agents/crud.ts | 44 +++++---- .../fleet/server/services/agents/reassign.ts | 95 ++++++++++++++----- x-pack/plugins/fleet/server/types/index.tsx | 6 ++ .../apis/agents/reassign.ts | 64 ++++++++++++- 6 files changed, 180 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 93cbb8369a3b1..b654c513e0afb 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -164,12 +164,13 @@ export interface PostBulkAgentReassignRequest { }; } -export interface PostBulkAgentReassignResponse { - [key: string]: { +export type PostBulkAgentReassignResponse = Record< + Agent['id'], + { success: boolean; - error?: Error; - }; -} + error?: string; + } +>; export interface GetOneAgentEventsRequest { params: { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index e6188a83c49e9..5ac264e29f079 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -308,26 +308,26 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; + const agentOptions = Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }; try { const results = await AgentService.reassignAgents( soClient, esClient, - Array.isArray(request.body.agents) - ? { agentIds: request.body.agents } - : { kuery: request.body.agents }, + agentOptions, request.body.policy_id ); - const body: PostBulkAgentReassignResponse = results.items.reduce((acc, so) => { - return { - ...acc, - [so.id]: { - success: !so.error, - error: so.error || undefined, - }, + const body = results.items.reduce((acc, so) => { + acc[so.id] = { + success: !so.error, + error: so.error?.message, }; + return acc; }, {}); + return response.ok({ body }); } catch (error) { return defaultIngestErrorHandler({ error, response }); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 22e9f559c56b8..9aa7bbc9f2b18 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -9,8 +9,8 @@ import Boom from '@hapi/boom'; import type { SearchResponse, MGetResponse, GetResponse } from 'elasticsearch'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import type { AgentSOAttributes, Agent, BulkActionResult, ListWithKuery } from '../../types'; import type { ESSearchResponse } from '../../../../../../typings/elasticsearch'; -import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; @@ -69,22 +69,23 @@ export type GetAgentsOptions = }; export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) { - let initialResults = []; - + let agents: Agent[] = []; if ('agentIds' in options) { - initialResults = await getAgentsById(esClient, options.agentIds); + agents = await getAgentsById(esClient, options.agentIds); } else if ('kuery' in options) { - initialResults = ( + agents = ( await getAllAgentsByKuery(esClient, { kuery: options.kuery, showInactive: options.showInactive ?? false, }) ).agents; } else { - throw new IngestManagerError('Cannot get agents'); + throw new IngestManagerError( + 'Either options.agentIds or options.kuery are required to get agents' + ); } - return initialResults; + return agents; } export async function getAgentsByKuery( @@ -188,7 +189,7 @@ export async function countInactiveAgents( export async function getAgentById(esClient: ElasticsearchClient, agentId: string) { const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`); try { - const agentHit = await esClient.get>({ + const agentHit = await esClient.get({ index: AGENTS_INDEX, id: agentId, }); @@ -207,10 +208,17 @@ export async function getAgentById(esClient: ElasticsearchClient, agentId: strin } } -async function getAgentDocuments( +export function isAgentDocument( + maybeDocument: any +): maybeDocument is GetResponse { + return '_id' in maybeDocument && '_source' in maybeDocument; +} + +export type ESAgentDocumentResult = GetResponse; +export async function getAgentDocuments( esClient: ElasticsearchClient, agentIds: string[] -): Promise>> { +): Promise { const res = await esClient.mget>({ index: AGENTS_INDEX, body: { docs: agentIds.map((_id) => ({ _id })) }, @@ -221,14 +229,16 @@ async function getAgentDocuments( export async function getAgentsById( esClient: ElasticsearchClient, - agentIds: string[], - options: { includeMissing?: boolean } = { includeMissing: false } + agentIds: string[] ): Promise { const allDocs = await getAgentDocuments(esClient, agentIds); - const agentDocs = options.includeMissing - ? allDocs - : allDocs.filter((res) => res._id && res._source); - const agents = agentDocs.map((doc) => searchHitToAgent(doc)); + const agents = allDocs.reduce((results, doc) => { + if (isAgentDocument(doc)) { + results.push(searchHitToAgent(doc)); + } + + return results; + }, []); return agents; } @@ -276,7 +286,7 @@ export async function bulkUpdateAgents( agentId: string; data: Partial; }> -) { +): Promise<{ items: BulkActionResult[] }> { if (updateData.length === 0) { return { items: [] }; } diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 74e60c42b9973..5574c42ced053 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -8,13 +8,20 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; -import type { Agent } from '../../types'; +import type { Agent, BulkActionResult } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { AgentReassignmentError } from '../../errors'; -import { getAgents, getAgentPolicyForAgent, updateAgent, bulkUpdateAgents } from './crud'; +import { + getAgentDocuments, + getAgents, + getAgentPolicyForAgent, + updateAgent, + bulkUpdateAgents, +} from './crud'; import type { GetAgentsOptions } from './index'; import { createAgentAction, bulkCreateAgentActions } from './actions'; +import { searchHitToAgent } from './helpers'; export async function reassignAgent( soClient: SavedObjectsClientContract, @@ -67,39 +74,67 @@ export async function reassignAgentIsAllowed( export async function reassignAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: { agents: Agent[] } | GetAgentsOptions, + options: ({ agents: Agent[] } | GetAgentsOptions) & { force?: boolean }, newAgentPolicyId: string -): Promise<{ items: Array<{ id: string; success: boolean; error?: Error }> }> { +): Promise<{ items: BulkActionResult[] }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!agentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); } - const allResults = 'agents' in options ? options.agents : await getAgents(esClient, options); + const outgoingErrors: Record = {}; + let givenAgents: Agent[] = []; + if ('agents' in options) { + givenAgents = options.agents; + } else if ('agentIds' in options) { + const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); + for (const agentResult of givenAgentsResults) { + if (agentResult.found === false) { + outgoingErrors[agentResult._id] = new AgentReassignmentError( + `Cannot find agent ${agentResult._id}` + ); + } else { + givenAgents.push(searchHitToAgent(agentResult)); + } + } + } else if ('kuery' in options) { + givenAgents = await getAgents(esClient, options); + } + const givenOrder = + 'agentIds' in options ? options.agentIds : givenAgents.map((agent) => agent.id); + // which are allowed to unenroll - const settled = await Promise.allSettled( - allResults.map((agent) => - reassignAgentIsAllowed(soClient, esClient, agent.id, newAgentPolicyId).then((_) => agent) - ) + const agentResults = await Promise.allSettled( + givenAgents.map(async (agent, index) => { + if (agent.policy_id === newAgentPolicyId) { + throw new AgentReassignmentError(`${agent.id} is already assigned to ${newAgentPolicyId}`); + } + + const isAllowed = await reassignAgentIsAllowed( + soClient, + esClient, + agent.id, + newAgentPolicyId + ); + if (isAllowed) { + return agent; + } + throw new AgentReassignmentError(`${agent.id} may not be reassigned to ${newAgentPolicyId}`); + }) ); // Filter to agents that do not already use the new agent policy ID - const agentsToUpdate = allResults.filter((agent, index) => { - if (settled[index].status === 'fulfilled') { - if (agent.policy_id === newAgentPolicyId) { - settled[index] = { - status: 'rejected', - reason: new AgentReassignmentError( - `${agent.id} is already assigned to ${newAgentPolicyId}` - ), - }; - } else { - return true; - } + const agentsToUpdate = agentResults.reduce((agents, result, index) => { + if (result.status === 'fulfilled') { + agents.push(result.value); + } else { + const id = givenAgents[index].id; + outgoingErrors[id] = result.reason; } - }); + return agents; + }, []); - const res = await bulkUpdateAgents( + await bulkUpdateAgents( esClient, agentsToUpdate.map((agent) => ({ agentId: agent.id, @@ -110,6 +145,18 @@ export async function reassignAgents( })) ); + const orderedOut = givenOrder.map((agentId) => { + const hasError = agentId in outgoingErrors; + const result: BulkActionResult = { + id: agentId, + success: !hasError, + }; + if (hasError) { + result.error = outgoingErrors[agentId]; + } + return result; + }); + const now = new Date().toISOString(); await bulkCreateAgentActions( soClient, @@ -121,5 +168,5 @@ export async function reassignAgents( })) ); - return res; + return { items: orderedOut }; } diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index c25b047c0e1ad..2b46f7e76a719 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -90,5 +90,11 @@ export type AgentPolicyUpdateHandler = ( agentPolicyId: string ) => Promise; +export interface BulkActionResult { + id: string; + success: boolean; + error?: Error; +} + export * from './models'; export * from './rest_spec'; diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 77da9ecce3294..627cb299f0909 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -101,15 +101,32 @@ export default function (providerContext: FtrProviderContext) { expect(agent3data.body.item.policy_id).to.eql('policy2'); }); - it('should allow to reassign multiple agents by id -- some invalid', async () => { - await supertest + it('should allow to reassign multiple agents by id -- mix valid & invalid', async () => { + const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ agents: ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc'], policy_id: 'policy2', - }) - .expect(200); + }); + + expect(body).to.eql({ + agent2: { success: true }, + INVALID_ID: { + success: false, + error: 'Cannot find agent INVALID_ID', + }, + agent3: { success: true }, + MISSING_ID: { + success: false, + error: 'Cannot find agent MISSING_ID', + }, + etc: { + success: false, + error: 'Cannot find agent etc', + }, + }); + const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), supertest.get(`/api/fleet/agents/agent3`), @@ -118,6 +135,45 @@ export default function (providerContext: FtrProviderContext) { expect(agent3data.body.item.policy_id).to.eql('policy2'); }); + it('should allow to reassign multiple agents by id -- mixed invalid, managed, etc', async () => { + // agent1 is enrolled in policy1. set policy1 to managed + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: true }) + .expect(200); + + const { body } = await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'INVALID_ID', 'agent3'], + policy_id: 'policy2', + }); + + expect(body).to.eql({ + agent2: { + success: false, + error: 'Cannot reassign an agent from managed agent policy policy1', + }, + INVALID_ID: { + success: false, + error: 'Cannot find agent INVALID_ID', + }, + agent3: { + success: false, + error: 'Cannot reassign an agent from managed agent policy policy1', + }, + }); + + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent2`), + supertest.get(`/api/fleet/agents/agent3`), + ]); + expect(agent2data.body.item.policy_id).to.eql('policy1'); + expect(agent3data.body.item.policy_id).to.eql('policy1'); + }); + it('should allow to reassign multiple agents by kuery', async () => { await supertest .post(`/api/fleet/agents/bulk_reassign`) From de3a7d6f0d1091cd26ab29a49b138bb468a38ae1 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 24 Mar 2021 10:45:51 -0500 Subject: [PATCH 32/88] Use `es` instead of `legacyEs` in APM API integration test (#95303) References #83910. --- x-pack/test/apm_api_integration/tests/feature_controls.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index e82b14d6cb7e6..edeffe1e5c296 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -14,7 +14,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const spaces = getService('spaces'); - const es = getService('legacyEs'); + const es = getService('es'); const log = getService('log'); const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); From 0551472cd90c2b29919c44ccf1c492b85e0fdb62 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 24 Mar 2021 08:47:10 -0700 Subject: [PATCH 33/88] [jest] switch to jest-environment-jsdom (#95125) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 1 + packages/kbn-test/jest-preset.js | 2 +- x-pack/plugins/global_search_bar/jest.config.js | 3 +++ x-pack/plugins/lens/jest.config.js | 3 +++ x-pack/plugins/security_solution/jest.config.js | 3 +++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 32cf8dc1aee0f..7cb6a505eeafe 100644 --- a/package.json +++ b/package.json @@ -697,6 +697,7 @@ "jest-cli": "^26.6.3", "jest-diff": "^26.6.2", "jest-environment-jsdom-thirteen": "^1.0.1", + "jest-environment-jsdom": "^26.6.2", "jest-raw-loader": "^1.0.1", "jest-silent-reporter": "^0.2.1", "jest-snapshot": "^26.6.2", diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index a1475985af8df..4949d6d1f9fad 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -68,7 +68,7 @@ module.exports = { ], // The test environment that will be used for testing - testEnvironment: 'jest-environment-jsdom-thirteen', + testEnvironment: 'jest-environment-jsdom', // The glob patterns Jest uses to detect test files testMatch: ['**/*.test.{js,mjs,ts,tsx}'], diff --git a/x-pack/plugins/global_search_bar/jest.config.js b/x-pack/plugins/global_search_bar/jest.config.js index 73cf5402a83a9..26a6934226ec4 100644 --- a/x-pack/plugins/global_search_bar/jest.config.js +++ b/x-pack/plugins/global_search_bar/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/global_search_bar'], + + // TODO: migrate to "jest-environment-jsdom" https://github.com/elastic/kibana/issues/95200 + testEnvironment: 'jest-environment-jsdom-thirteen', }; diff --git a/x-pack/plugins/lens/jest.config.js b/x-pack/plugins/lens/jest.config.js index 615e540eaedce..9a3f12e1ead32 100644 --- a/x-pack/plugins/lens/jest.config.js +++ b/x-pack/plugins/lens/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/lens'], + + // TODO: migrate to "jest-environment-jsdom" https://github.com/elastic/kibana/issues/95202 + testEnvironment: 'jest-environment-jsdom-thirteen', }; diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/jest.config.js index 700eaebf6c202..b4dcedfcceeee 100644 --- a/x-pack/plugins/security_solution/jest.config.js +++ b/x-pack/plugins/security_solution/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/security_solution'], + + // TODO: migrate to "jest-environment-jsdom" https://github.com/elastic/kibana/issues/95201 + testEnvironment: 'jest-environment-jsdom-thirteen', }; From 2e5b5debb5e198a4f6dfd45dafdb5b4cdfecb561 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Wed, 24 Mar 2021 17:04:49 +0100 Subject: [PATCH 34/88] [Fleet] Add force option to DELETE package endpoint. (#95051) * Add force option to DELETE package endpoint. * Add integration test. * Adjust openapi spec. * Run EPM tests before fleet setup tests. * Run package delete tests first in EPM tests --- .../plugins/fleet/common/openapi/bundled.json | 16 +++++- .../plugins/fleet/common/openapi/bundled.yaml | 8 +++ .../openapi/paths/epm@packages@{pkgkey}.yaml | 8 +++ .../fleet/server/routes/epm/handlers.ts | 11 +++- .../server/services/epm/packages/remove.ts | 5 +- .../fleet/server/types/rest_spec/epm.ts | 5 ++ .../fleet_api_integration/apis/epm/delete.ts | 54 +++++++++++++++++++ .../fleet_api_integration/apis/epm/index.js | 1 + .../test/fleet_api_integration/apis/index.js | 6 +-- 9 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/epm/delete.ts diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 55c32802c3334..388aebed9a85b 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -1633,7 +1633,21 @@ { "$ref": "#/paths/~1setup/post/parameters/0" } - ] + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + } + } + } } }, "/agents/{agentId}": { diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 9461927bb09b8..227faffdac489 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -1038,6 +1038,14 @@ paths: operationId: post-epm-delete-pkgkey parameters: - $ref: '#/paths/~1setup/post/parameters/0' + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean '/agents/{agentId}': parameters: - schema: diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml index 43937aa153f50..85d8615a9eb4b 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}.yaml @@ -89,3 +89,11 @@ delete: operationId: post-epm-delete-pkgkey parameters: - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 3ac951f7987f8..f0d6e68427361 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -310,13 +310,20 @@ export const installPackageByUploadHandler: RequestHandler< }; export const deletePackageHandler: RequestHandler< - TypeOf + TypeOf, + undefined, + TypeOf > = async (context, request, response) => { try { const { pkgkey } = request.params; const savedObjectsClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const res = await removeInstallation({ savedObjectsClient, pkgkey, esClient }); + const res = await removeInstallation({ + savedObjectsClient, + pkgkey, + esClient, + force: request.body?.force, + }); const body: DeletePackageResponse = { response: res, }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 21e4e31be2bd0..de798e822b029 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -32,13 +32,14 @@ export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; esClient: ElasticsearchClient; + force?: boolean; }): Promise { - const { savedObjectsClient, pkgkey, esClient } = options; + const { savedObjectsClient, pkgkey, esClient, force } = options; // TODO: the epm api should change to /name/version so we don't need to do this const { pkgName, pkgVersion } = splitPkgKey(pkgkey); const installation = await getInstallation({ savedObjectsClient, pkgName }); if (!installation) throw Boom.badRequest(`${pkgName} is not installed`); - if (installation.removable === false) + if (installation.removable === false && !force) throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); const { total } = await packagePolicyService.list(savedObjectsClient, { diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 2243a7d3930cd..f7e3ed906e24b 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -65,4 +65,9 @@ export const DeletePackageRequestSchema = { params: schema.object({ pkgkey: schema.string(), }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), }; diff --git a/x-pack/test/fleet_api_integration/apis/epm/delete.ts b/x-pack/test/fleet_api_integration/apis/epm/delete.ts new file mode 100644 index 0000000000000..ecf9eb625bd7e --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/delete.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 { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const requiredPackage = 'system-0.11.0'; + + const installPackage = async (pkgkey: string) => { + await supertest + .post(`/api/fleet/epm/packages/${pkgkey}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + const deletePackage = async (pkgkey: string) => { + await supertest + .delete(`/api/fleet/epm/packages/${pkgkey}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + describe('delete and force delete scenarios', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await installPackage(requiredPackage); + }); + after(async () => { + await deletePackage(requiredPackage); + }); + + it('should return 400 if trying to uninstall a required package', async function () { + await supertest + .delete(`/api/fleet/epm/packages/${requiredPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 200 if trying to force uninstall a required package', async function () { + await supertest + .delete(`/api/fleet/epm/packages/${requiredPackage}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 0020e6bdf1bb0..009e1a2dad5f1 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -7,6 +7,7 @@ export default function loadTests({ loadTestFile }) { describe('EPM Endpoints', () => { + loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./list')); loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 3d7ee3686b575..27987e469cfe7 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -8,6 +8,9 @@ export default function ({ loadTestFile }) { describe('Fleet Endpoints', function () { this.tags('ciGroup10'); + // EPM + loadTestFile(require.resolve('./epm/index')); + // Fleet setup loadTestFile(require.resolve('./fleet_setup')); @@ -30,9 +33,6 @@ export default function ({ loadTestFile }) { // Enrollment API keys loadTestFile(require.resolve('./enrollment_api_keys/crud')); - // EPM - loadTestFile(require.resolve('./epm/index')); - // Package policies loadTestFile(require.resolve('./package_policy/create')); loadTestFile(require.resolve('./package_policy/update')); From d0c09463d00d5e4433ab016f8d04558d15b61e86 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 24 Mar 2021 12:39:39 -0400 Subject: [PATCH 35/88] [Upgrade Assistant] Reorganize folder structure (#94843) --- .../public/application/components/_index.scss | 4 ++-- .../__fixtures__/checkup_api_response.json | 0 .../__snapshots__/filter_bar.test.tsx.snap | 0 .../__snapshots__/group_by_bar.test.tsx.snap | 0 .../{tabs/checkup => es_deprecations}/_index.scss | 0 .../{tabs/checkup => es_deprecations}/constants.tsx | 10 ++++++---- .../{tabs/checkup => es_deprecations}/controls.tsx | 7 +++---- .../deprecation_tab.tsx} | 8 ++++---- .../deprecations/_cell.scss | 0 .../deprecations/_deprecations.scss | 0 .../deprecations/_index.scss | 0 .../checkup => es_deprecations}/deprecations/cell.tsx | 4 ++-- .../deprecations/count_summary.tsx | 2 +- .../deprecations/grouped.test.tsx | 4 ++-- .../deprecations/grouped.tsx | 4 ++-- .../deprecations/health.tsx | 2 +- .../checkup => es_deprecations}/deprecations/index.tsx | 0 .../deprecations/index_settings/button.tsx | 0 .../deprecations/index_settings/index.ts | 0 .../index_settings/remove_settings_provider.tsx | 2 +- .../deprecations/index_table.test.tsx | 0 .../deprecations/index_table.tsx | 4 ++-- .../deprecations/list.test.tsx | 4 ++-- .../checkup => es_deprecations}/deprecations/list.tsx | 4 ++-- .../deprecations/reindex/_button.scss | 0 .../deprecations/reindex/_index.scss | 0 .../deprecations/reindex/button.tsx | 6 +++--- .../flyout/__snapshots__/checklist_step.test.tsx.snap | 0 .../flyout/__snapshots__/warning_step.test.tsx.snap | 0 .../deprecations/reindex/flyout/_index.scss | 0 .../deprecations/reindex/flyout/_step_progress.scss | 0 .../reindex/flyout/checklist_step.test.tsx | 4 ++-- .../deprecations/reindex/flyout/checklist_step.tsx | 4 ++-- .../deprecations/reindex/flyout/container.tsx | 2 +- .../deprecations/reindex/flyout/index.tsx | 0 .../deprecations/reindex/flyout/progress.test.tsx | 2 +- .../deprecations/reindex/flyout/progress.tsx | 4 ++-- .../deprecations/reindex/flyout/step_progress.tsx | 0 .../deprecations/reindex/flyout/warning_step.test.tsx | 8 ++++---- .../reindex/flyout/warning_step_checkbox.tsx | 2 +- .../deprecations/reindex/flyout/warnings_step.tsx | 4 ++-- .../deprecations/reindex/index.tsx | 0 .../deprecations/reindex/polling_service.test.ts | 2 +- .../deprecations/reindex/polling_service.ts | 6 +++--- .../checkup => es_deprecations}/filter_bar.test.tsx | 4 ++-- .../{tabs/checkup => es_deprecations}/filter_bar.tsx | 4 ++-- .../checkup => es_deprecations}/group_by_bar.test.tsx | 2 +- .../{tabs/checkup => es_deprecations}/group_by_bar.tsx | 2 +- .../application/components/es_deprecations/index.ts | 8 ++++++++ .../components/{tabs => }/overview/_index.scss | 0 .../components/{tabs => }/overview/_steps.scss | 0 .../{tabs => }/overview/deprecation_logging_toggle.tsx | 4 ++-- .../{tabs/checkup/index.tsx => overview/index.ts} | 2 +- .../{tabs/overview/index.tsx => overview/overview.tsx} | 6 +++--- .../components/{tabs => }/overview/steps.tsx | 4 ++-- .../public/application/components/tabs.tsx | 8 ++++---- .../public/application/{ => lib}/utils.test.ts | 0 .../public/application/{ => lib}/utils.ts | 0 58 files changed, 78 insertions(+), 69 deletions(-) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs => es_deprecations}/__fixtures__/checkup_api_response.json (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/__snapshots__/filter_bar.test.tsx.snap (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/__snapshots__/group_by_bar.test.tsx.snap (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/_index.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/constants.tsx (70%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/controls.tsx (95%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup/checkup_tab.tsx => es_deprecations/deprecation_tab.tsx} (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/_cell.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/_deprecations.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/_index.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/cell.tsx (95%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/count_summary.tsx (95%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/grouped.test.tsx (98%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/grouped.tsx (98%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/health.tsx (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/index.tsx (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/index_settings/button.tsx (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/index_settings/index.ts (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/index_settings/remove_settings_provider.tsx (98%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/index_table.test.tsx (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/index_table.tsx (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/list.test.tsx (96%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/list.tsx (98%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/_button.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/_index.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/button.tsx (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/_index.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/_step_progress.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/checklist_step.test.tsx (94%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/checklist_step.tsx (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/container.tsx (99%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/index.tsx (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/progress.test.tsx (99%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/progress.tsx (99%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/step_progress.tsx (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/warning_step.test.tsx (88%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/warning_step_checkbox.tsx (99%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/flyout/warnings_step.tsx (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/index.tsx (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/polling_service.test.ts (97%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/deprecations/reindex/polling_service.ts (96%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/filter_bar.test.tsx (90%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/filter_bar.tsx (94%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/group_by_bar.test.tsx (95%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup => es_deprecations}/group_by_bar.tsx (97%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/index.ts rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs => }/overview/_index.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs => }/overview/_steps.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs => }/overview/deprecation_logging_toggle.tsx (96%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/checkup/index.tsx => overview/index.ts} (85%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs/overview/index.tsx => overview/overview.tsx} (91%) rename x-pack/plugins/upgrade_assistant/public/application/components/{tabs => }/overview/steps.tsx (99%) rename x-pack/plugins/upgrade_assistant/public/application/{ => lib}/utils.test.ts (100%) rename x-pack/plugins/upgrade_assistant/public/application/{ => lib}/utils.ts (100%) diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss index bb01107f334f6..8f900ca8dc055 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss +++ b/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss @@ -1,2 +1,2 @@ -@import 'tabs/checkup/index'; -@import 'tabs/overview/index'; +@import 'es_deprecations/index'; +@import 'overview/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/__fixtures__/checkup_api_response.json b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__fixtures__/checkup_api_response.json similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/__fixtures__/checkup_api_response.json rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__fixtures__/checkup_api_response.json diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/filter_bar.test.tsx.snap similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/filter_bar.test.tsx.snap diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/group_by_bar.test.tsx.snap similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/group_by_bar.test.tsx.snap diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/constants.tsx similarity index 70% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/constants.tsx index a3e9ae783e812..feff6010efe38 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/constants.tsx @@ -7,16 +7,18 @@ import { IconColor } from '@elastic/eui'; import { invert } from 'lodash'; -import { DeprecationInfo } from '../../../../../common/types'; +import { DeprecationInfo } from '../../../../common/types'; export const LEVEL_MAP: { [level: string]: number } = { warning: 0, critical: 1, }; -export const REVERSE_LEVEL_MAP: { [idx: number]: DeprecationInfo['level'] } = invert( - LEVEL_MAP -) as any; +interface ReverseLevelMap { + [idx: number]: DeprecationInfo['level']; +} + +export const REVERSE_LEVEL_MAP: ReverseLevelMap = invert(LEVEL_MAP) as ReverseLevelMap; export const COLOR_MAP: { [level: string]: IconColor } = { warning: 'default', diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx similarity index 95% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx index 578e9344659c2..7212c2db4c6b4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx @@ -9,13 +9,12 @@ import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DeprecationInfo } from '../../../../../common/types'; -import { GroupByOption, LevelFilterOption } from '../../types'; +import { DeprecationInfo } from '../../../../common/types'; +import { validateRegExpString } from '../../lib/utils'; +import { GroupByOption, LevelFilterOption } from '../types'; import { FilterBar } from './filter_bar'; import { GroupByBar } from './group_by_bar'; -import { validateRegExpString } from '../../../utils'; - interface CheckupControlsProps { allDeprecations?: DeprecationInfo[]; isLoading: boolean; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab.tsx index 3c4c747529915..a5ae341f1e424 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab.tsx @@ -19,9 +19,9 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { LoadingErrorBanner } from '../../error_banner'; -import { useAppContext } from '../../../app_context'; -import { GroupByOption, LevelFilterOption, UpgradeAssistantTabProps } from '../../types'; +import { LoadingErrorBanner } from '../error_banner'; +import { useAppContext } from '../../app_context'; +import { GroupByOption, LevelFilterOption, UpgradeAssistantTabProps } from '../types'; import { CheckupControls } from './controls'; import { GroupedDeprecations } from './deprecations/grouped'; @@ -34,7 +34,7 @@ export interface CheckupTabProps extends UpgradeAssistantTabProps { * Displays a list of deprecations that filterable and groupable. Can be used for cluster, * nodes, or indices checkups. */ -export const CheckupTab: FunctionComponent = ({ +export const DeprecationTab: FunctionComponent = ({ alertBanner, checkupLabel, deprecations, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_cell.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_cell.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_cell.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_cell.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_deprecations.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_deprecations.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_deprecations.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_deprecations.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_index.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx similarity index 95% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx index 890f7e63c9904..5f960bd09d286 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx @@ -17,9 +17,9 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EnrichedDeprecationInfo } from '../../../../../common/types'; +import { AppContext } from '../../../app_context'; import { ReindexButton } from './reindex'; -import { AppContext } from '../../../../app_context'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { FixIndexSettingsButton } from './index_settings'; interface DeprecationCellProps { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/count_summary.tsx similarity index 95% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/count_summary.tsx index 872e508494ed7..db176ba43d8ed 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/count_summary.tsx @@ -10,7 +10,7 @@ import React, { Fragment, FunctionComponent } from 'react'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { EnrichedDeprecationInfo } from '../../../../../common/types'; export const DeprecationCountSummary: FunctionComponent<{ deprecations: EnrichedDeprecationInfo[]; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx similarity index 98% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx index 0885286961b11..00059fe0456ce 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { EuiBadge, EuiPagination } from '@elastic/eui'; -import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../../common/types'; -import { GroupByOption, LevelFilterOption } from '../../../types'; +import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types'; +import { GroupByOption, LevelFilterOption } from '../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; describe('filterDeps', () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx similarity index 98% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx index 3c18d6fe8a609..9879b977f1cfd 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx @@ -19,8 +19,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../../common/types'; -import { GroupByOption, LevelFilterOption } from '../../../types'; +import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types'; +import { GroupByOption, LevelFilterOption } from '../../types'; import { DeprecationCountSummary } from './count_summary'; import { DeprecationHealth } from './health'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/health.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/health.tsx index d7e62684c4570..c489824b1059d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/health.tsx @@ -11,7 +11,7 @@ import React, { FunctionComponent } from 'react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from '../../../../../../common/types'; +import { DeprecationInfo } from '../../../../../common/types'; import { COLOR_MAP, LEVEL_MAP, REVERSE_LEVEL_MAP } from '../constants'; const LocalizedLevels: { [level: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index.tsx similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index.tsx diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/button.tsx similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/button.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/button.tsx diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/index.ts similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/index.ts diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/remove_settings_provider.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/remove_settings_provider.tsx similarity index 98% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/remove_settings_provider.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/remove_settings_provider.tsx index e344d300be06c..1fd0c79dbbef3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_settings/remove_settings_provider.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_settings/remove_settings_provider.tsx @@ -8,7 +8,7 @@ import React, { useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCode, EuiConfirmModal } from '@elastic/eui'; -import { useAppContext } from '../../../../../app_context'; +import { useAppContext } from '../../../../app_context'; interface Props { children: ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.test.tsx similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.test.tsx diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx index da2f5f04effc4..216884d547eeb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index_table.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EnrichedDeprecationInfo } from '../../../../../common/types'; +import { AppContext } from '../../../app_context'; import { ReindexButton } from './reindex'; -import { AppContext } from '../../../../app_context'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { FixIndexSettingsButton } from './index_settings'; const PAGE_SIZES = [10, 25, 50, 100, 250, 500, 1000]; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx similarity index 96% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx index 1d646442ebee0..c1b6357d504eb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx @@ -8,8 +8,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; -import { GroupByOption } from '../../../types'; +import { EnrichedDeprecationInfo } from '../../../../../common/types'; +import { GroupByOption } from '../../types'; import { DeprecationList } from './list'; describe('DeprecationList', () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx similarity index 98% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx index eb9012b3bddd1..65b878fe36a86 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx @@ -7,8 +7,8 @@ import React, { FunctionComponent } from 'react'; -import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../../common/types'; -import { GroupByOption } from '../../../types'; +import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types'; +import { GroupByOption } from '../../types'; import { COLOR_MAP, LEVEL_MAP } from '../constants'; import { DeprecationCell } from './cell'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_button.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/_button.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_button.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/_button.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/_index.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx index 98c22308d9373..34c1328459cdb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/button.tsx @@ -13,13 +13,13 @@ import { Subscription } from 'rxjs'; import { EuiButton, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocLinksStart, HttpSetup } from 'src/core/public'; -import { API_BASE_PATH } from '../../../../../../../common/constants'; +import { API_BASE_PATH } from '../../../../../../common/constants'; import { EnrichedDeprecationInfo, ReindexStatus, UIReindexOption, -} from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; +} from '../../../../../../common/types'; +import { LoadingState } from '../../../types'; import { ReindexFlyout } from './flyout'; import { ReindexPollingService, ReindexState } from './polling_service'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/_index.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/_step_progress.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/_step_progress.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/_step_progress.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/_step_progress.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/checklist_step.test.tsx similarity index 94% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/checklist_step.test.tsx index 5bea0d855e45e..f8d72addc2d18 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/checklist_step.test.tsx @@ -9,8 +9,8 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; -import { ReindexStatus } from '../../../../../../../../common/types'; -import { LoadingState } from '../../../../../types'; +import { ReindexStatus } from '../../../../../../../common/types'; +import { LoadingState } from '../../../../types'; import { ReindexState } from '../polling_service'; import { ChecklistFlyoutStep } from './checklist_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/checklist_step.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/checklist_step.tsx index fc4bdcc130e25..e852171a696b4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/checklist_step.tsx @@ -20,8 +20,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ReindexStatus } from '../../../../../../../../common/types'; -import { LoadingState } from '../../../../../types'; +import { ReindexStatus } from '../../../../../../../common/types'; +import { LoadingState } from '../../../../types'; import { ReindexState } from '../polling_service'; import { ReindexProgress } from './progress'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/container.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx similarity index 99% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/container.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx index 2f776f3937b50..3e7b931452566 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/container.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/container.tsx @@ -19,7 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../../common/types'; +import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types'; import { ReindexState } from '../polling_service'; import { ChecklistFlyoutStep } from './checklist_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/index.tsx similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/index.tsx diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/progress.test.tsx similarity index 99% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/progress.test.tsx index 690775efb9d12..24a00af7a9fee 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/progress.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; +import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexState } from '../polling_service'; import { ReindexProgress } from './progress'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/progress.tsx similarity index 99% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/progress.tsx index 5d10111f3ae5e..088266f3a4840 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/progress.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; -import { LoadingState } from '../../../../../types'; +import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { LoadingState } from '../../../../types'; import { ReindexState } from '../polling_service'; import { StepProgress, StepProgressStep } from './step_progress'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/step_progress.tsx similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/step_progress.tsx diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warning_step.test.tsx similarity index 88% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warning_step.test.tsx index ca9c53354bf75..ff11b9f1a8450 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warning_step.test.tsx @@ -9,14 +9,14 @@ import { I18nProvider } from '@kbn/i18n/react'; import { mount, shallow } from 'enzyme'; import React from 'react'; -import { ReindexWarning } from '../../../../../../../../common/types'; -import { mockKibanaSemverVersion } from '../../../../../../../../common/constants'; +import { ReindexWarning } from '../../../../../../../common/types'; +import { mockKibanaSemverVersion } from '../../../../../../../common/constants'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; -jest.mock('../../../../../../app_context', () => { +jest.mock('../../../../../app_context', () => { const { docLinksServiceMock } = jest.requireActual( - '../../../../../../../../../../../src/core/public/doc_links/doc_links_service.mock' + '../../../../../../../../../../src/core/public/doc_links/doc_links_service.mock' ); return { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step_checkbox.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warning_step_checkbox.tsx similarity index 99% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step_checkbox.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warning_step_checkbox.tsx index de41108886c00..a5e3260167218 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step_checkbox.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warning_step_checkbox.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocLinksStart } from 'kibana/public'; -import { ReindexWarning, ReindexWarningTypes } from '../../../../../../../../common/types'; +import { ReindexWarning, ReindexWarningTypes } from '../../../../../../../common/types'; export const hasReindexWarning = ( warnings: ReindexWarning[], diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warnings_step.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warnings_step.tsx index 241884e95c1eb..4415811f6bf38 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/flyout/warnings_step.tsx @@ -19,8 +19,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ReindexWarning, ReindexWarningTypes } from '../../../../../../../../common/types'; -import { useAppContext } from '../../../../../../app_context'; +import { ReindexWarning, ReindexWarningTypes } from '../../../../../../../common/types'; +import { useAppContext } from '../../../../../app_context'; import { CustomTypeNameWarningCheckbox, DeprecatedSettingWarningCheckbox, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/index.tsx similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/index.tsx diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/polling_service.test.ts similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/polling_service.test.ts index e7f56fc1bdf79..13818e864783e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/polling_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; import { httpServiceMock } from 'src/core/public/mocks'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/polling_service.ts similarity index 96% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/polling_service.ts index d6a4a8e7b5a56..239bd56bd2fa5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/reindex/polling_service.ts @@ -8,15 +8,15 @@ import { BehaviorSubject } from 'rxjs'; import { HttpSetup } from 'src/core/public'; -import { API_BASE_PATH } from '../../../../../../../common/constants'; +import { API_BASE_PATH } from '../../../../../../common/constants'; import { IndexGroup, ReindexOperation, ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; +} from '../../../../../../common/types'; +import { LoadingState } from '../../../types'; const POLL_INTERVAL = 1000; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.test.tsx similarity index 90% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.test.tsx index 81e04ac780422..feac88cf4a525 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.test.tsx @@ -7,9 +7,9 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { DeprecationInfo } from '../../../../../common/types'; +import { DeprecationInfo } from '../../../../common/types'; -import { LevelFilterOption } from '../../types'; +import { LevelFilterOption } from '../types'; import { FilterBar } from './filter_bar'; const defaultProps = { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx similarity index 94% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx index ffb81832b505d..7ef3ae2fc9332 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from '../../../../../common/types'; -import { LevelFilterOption } from '../../types'; +import { DeprecationInfo } from '../../../../common/types'; +import { LevelFilterOption } from '../types'; const LocalizedOptions: { [option: string]: string } = { all: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.filterBar.allButtonLabel', { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.test.tsx similarity index 95% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.test.tsx index 825dd9b337e49..53f76d6d0f981 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { GroupByOption } from '../../types'; +import { GroupByOption } from '../types'; import { GroupByBar } from './group_by_bar'; const defaultProps = { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.tsx similarity index 97% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.tsx index fc3971d4082b9..a80fe664ced2e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { GroupByOption } from '../../types'; +import { GroupByOption } from '../types'; const LocalizedOptions: { [option: string]: string } = { message: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel', { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/index.ts new file mode 100644 index 0000000000000..8b7435b94b2c1 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DeprecationTab } from './deprecation_tab'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_steps.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/_steps.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_steps.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/_steps.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/deprecation_logging_toggle.tsx similarity index 96% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/deprecation_logging_toggle.tsx index b026b17c65f18..5ed46c25ecf17 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/deprecation_logging_toggle.tsx @@ -10,8 +10,8 @@ import React, { useEffect, useState } from 'react'; import { EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useAppContext } from '../../../app_context'; -import { ResponseError } from '../../../lib/api'; +import { useAppContext } from '../../app_context'; +import { ResponseError } from '../../lib/api'; export const DeprecationLoggingToggle: React.FunctionComponent = () => { const { api } = useAppContext(); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/index.ts similarity index 85% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/index.ts index 8c04da8b7843a..c43c1415f6f7c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { CheckupTab } from './checkup_tab'; +export { OverviewTab } from './overview'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx similarity index 91% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 8cd3bbebd7861..01677e7394a87 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useAppContext } from '../../../app_context'; -import { LoadingErrorBanner } from '../../error_banner'; -import { UpgradeAssistantTabProps } from '../../types'; +import { useAppContext } from '../../app_context'; +import { LoadingErrorBanner } from '../error_banner'; +import { UpgradeAssistantTabProps } from '../types'; import { Steps } from './steps'; export const OverviewTab: FunctionComponent = (props) => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/steps.tsx similarity index 99% rename from x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/steps.tsx index afd04fb02e6dd..095960ae93562 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/steps.tsx @@ -20,9 +20,9 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { UpgradeAssistantTabProps } from '../../types'; +import { useAppContext } from '../../app_context'; +import { UpgradeAssistantTabProps } from '../types'; import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; -import { useAppContext } from '../../../app_context'; // Leaving these here even if unused so they are picked up for i18n static analysis // Keep this until last minor release (when next major is also released). diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 9875b4c5a1a33..231d9705bd0d9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { LatestMinorBanner } from './latest_minor_banner'; -import { CheckupTab } from './tabs/checkup'; -import { OverviewTab } from './tabs/overview'; +import { DeprecationTab } from './es_deprecations'; +import { OverviewTab } from './overview'; import { TelemetryState, UpgradeAssistantTabProps } from './types'; import { useAppContext } from '../app_context'; @@ -58,7 +58,7 @@ export const UpgradeAssistantTabs: React.FunctionComponent = () => { defaultMessage: 'Cluster', }), content: ( - { defaultMessage: 'Indices', }), content: ( - Date: Wed, 24 Mar 2021 10:23:07 -0700 Subject: [PATCH 36/88] [App Search] Various engines fixes (#95127) * [Misc] Update trash icon color - should be danger/red to match other tables in Kibana * Engine table - fix incorrect conditional around when users can delete engines - The check for that should be around Dev/Editor/Analyst roles, not around whether the account has a platinum license - Tests - DRY out reset mock - ideally would be in a beforeEach, but mount/beforeAll perf makes that difficult * Create engine button - wrap with canManageEngines check - prevents Dev/Editor/Analyst roles from hitting a 404 page - test cleanup - use describe blocks to convey conditional branching, combine 2 tests into 1 * Empty engines prompt - add canManageEngines check + switch from FormattedMessage to i18n (this view was created a long time ago before we settled on generally preferring i18n) + provide a more helpful body text when the user cannot create engines * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx Co-authored-by: Jason Stoltzfus Co-authored-by: Jason Stoltzfus --- .../engines/components/empty_state.test.tsx | 53 ++++++--- .../engines/components/empty_state.tsx | 112 +++++++++++------- .../engines/engines_overview.test.tsx | 88 ++++++++------ .../components/engines/engines_overview.tsx | 56 +++++---- .../components/engines/engines_table.test.tsx | 46 ++++--- .../components/engines/engines_table.tsx | 9 +- 6 files changed, 216 insertions(+), 148 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx index 14772375c9bd4..a737174477177 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx @@ -6,7 +6,7 @@ */ import '../../../../__mocks__/kea.mock'; -import { mockTelemetryActions } from '../../../../__mocks__'; +import { setMockValues, mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; @@ -17,30 +17,47 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { EmptyState } from './'; describe('EmptyState', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); - }); - - describe('CTA Button', () => { + describe('when the user can manage/create engines', () => { let wrapper: ShallowWrapper; - let prompt: ShallowWrapper; - let button: ShallowWrapper; - beforeEach(() => { + beforeAll(() => { + setMockValues({ myRole: { canManageEngines: true } }); wrapper = shallow(); - prompt = wrapper.find(EuiEmptyPrompt).dive(); - button = prompt.find('[data-test-subj="EmptyStateCreateFirstEngineCta"]'); }); - it('sends telemetry on create first engine click', () => { - button.simulate('click'); - expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalled(); + it('renders a prompt to create an engine', () => { + expect(wrapper.find('[data-test-subj="AdminEmptyEnginesPrompt"]')).toHaveLength(1); + }); + + describe('create engine button', () => { + let prompt: ShallowWrapper; + let button: ShallowWrapper; + + beforeAll(() => { + prompt = wrapper.find(EuiEmptyPrompt).dive(); + button = prompt.find('[data-test-subj="EmptyStateCreateFirstEngineCta"]'); + }); + + it('sends telemetry on create first engine click', () => { + button.simulate('click'); + expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalled(); + }); + + it('sends a user to engine creation', () => { + expect(button.prop('to')).toEqual('/engine_creation'); + }); + }); + }); + + describe('when the user cannot manage/create engines', () => { + beforeAll(() => { + setMockValues({ myRole: { canManageEngines: false } }); }); - it('sends a user to engine creation', () => { - expect(button.prop('to')).toEqual('/engine_creation'); + it('renders a prompt to contact the App Search admin', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="NonAdminEmptyEnginesPrompt"]')).toHaveLength(1); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index fc77e2f2511e0..df5a057e5d9c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -7,14 +7,15 @@ import React from 'react'; -import { useActions } from 'kea'; +import { useValues, useActions } from 'kea'; import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../../shared/telemetry'; +import { AppLogic } from '../../../app_logic'; import { ENGINE_CREATION_PATH } from '../../../routes'; import { EnginesOverviewHeader } from './header'; @@ -22,6 +23,9 @@ import { EnginesOverviewHeader } from './header'; import './empty_state.scss'; export const EmptyState: React.FC = () => { + const { + myRole: { canManageEngines }, + } = useValues(AppLogic); const { sendAppSearchTelemetry } = useActions(TelemetryLogic); return ( @@ -29,45 +33,71 @@ export const EmptyState: React.FC = () => { - - -

- } - titleSize="l" - body={ -

- -

- } - actions={ - - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'create_first_engine_button', - }) - } - > - - - } - /> + {canManageEngines ? ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', { + defaultMessage: 'Create your first engine', + })} + + } + titleSize="l" + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', { + defaultMessage: + 'An App Search engine stores the documents for your search experience.', + })} +

+ } + actions={ + + sendAppSearchTelemetry({ + action: 'clicked', + metric: 'create_first_engine_button', + }) + } + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta', + { defaultMessage: 'Create an engine' } + )} + + } + /> + ) : ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', { + defaultMessage: 'No engines available', + })} + + } + body={ +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description', + { + defaultMessage: + 'Contact your App Search administrator to either create or grant you access to an engine.', + } + )} +

+ } + /> + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index 5a3f730940760..3ca039907932e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -41,6 +41,7 @@ describe('EnginesOverview', () => { }, metaEnginesLoading: false, hasPlatinumLicense: false, + myRole: { canManageEngines: false }, }; const actions = { loadEngines: jest.fn(), @@ -90,61 +91,72 @@ describe('EnginesOverview', () => { setMockValues(valuesWithEngines); }); - it('renders and calls the engines API', async () => { + it('renders and calls the engines API', () => { const wrapper = shallow(); expect(wrapper.find(EnginesTable)).toHaveLength(1); expect(actions.loadEngines).toHaveBeenCalled(); }); - it('renders a create engine button which takes users to the create engine page', () => { - const wrapper = shallow(); + describe('when the user can manage/create engines', () => { + it('renders a create engine button which takes users to the create engine page', () => { + setMockValues({ + ...valuesWithEngines, + myRole: { canManageEngines: true }, + }); + const wrapper = shallow(); - expect( - wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to') - ).toEqual('/engine_creation'); + expect( + wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to') + ).toEqual('/engine_creation'); + }); }); - describe('when user has a platinum license', () => { - let wrapper: ShallowWrapper; - - beforeEach(() => { + describe('when the account has a platinum license', () => { + it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => { setMockValues({ ...valuesWithEngines, hasPlatinumLicense: true, }); - wrapper = shallow(); - }); + const wrapper = shallow(); - it('renders a 2nd meta engines table ', async () => { expect(wrapper.find(EnginesTable)).toHaveLength(2); - }); - - it('makes a 2nd meta engines call', () => { expect(actions.loadMetaEngines).toHaveBeenCalled(); }); - it('renders a create engine button which takes users to the create meta engine page', () => { - expect( - wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') - ).toEqual('/meta_engine_creation'); - }); - - it('contains an EuiEmptyPrompt that takes users to the create meta when metaEngines is empty', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - metaEngines: [], + describe('when the user can manage/create engines', () => { + it('renders a create engine button which takes users to the create meta engine page', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + myRole: { canManageEngines: true }, + }); + const wrapper = shallow(); + + expect( + wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') + ).toEqual('/meta_engine_creation'); }); - wrapper = shallow(); - const metaEnginesTable = wrapper.find(EnginesTable).last().dive(); - const emptyPrompt = metaEnginesTable.dive().find(EuiEmptyPrompt).dive(); - expect( - emptyPrompt - .find('[data-test-subj="appSearchMetaEnginesEmptyStateCreationButton"]') - .prop('to') - ).toEqual('/meta_engine_creation'); + describe('when metaEngines is empty', () => { + it('contains an EuiEmptyPrompt that takes users to the create meta engine page', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + myRole: { canManageEngines: true }, + metaEngines: [], + }); + const wrapper = shallow(); + const metaEnginesTable = wrapper.find(EnginesTable).last().dive(); + const emptyPrompt = metaEnginesTable.dive().find(EuiEmptyPrompt).dive(); + + expect( + emptyPrompt + .find('[data-test-subj="appSearchMetaEnginesEmptyStateCreationButton"]') + .prop('to') + ).toEqual('/meta_engine_creation'); + }); + }); }); }); @@ -152,7 +164,7 @@ describe('EnginesOverview', () => { const getTablePagination = (wrapper: ShallowWrapper) => wrapper.find(EnginesTable).prop('pagination'); - it('passes down page data from the API', async () => { + it('passes down page data from the API', () => { const wrapper = shallow(); const pagination = getTablePagination(wrapper); @@ -160,7 +172,7 @@ describe('EnginesOverview', () => { expect(pagination.pageIndex).toEqual(0); }); - it('re-polls the API on page change', async () => { + it('re-polls the API on page change', () => { const wrapper = shallow(); setMockValues({ @@ -178,7 +190,7 @@ describe('EnginesOverview', () => { expect(getTablePagination(wrapper).pageIndex).toEqual(50); }); - it('calls onPagination handlers', async () => { + it('calls onPagination handlers', () => { setMockValues({ ...valuesWithEngines, hasPlatinumLicense: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 8297fb490ee04..baf275fbe6c2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -25,6 +25,7 @@ import { LicensingLogic } from '../../../shared/licensing'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { AppLogic } from '../../app_logic'; import { EngineIcon, MetaEngineIcon } from '../../icons'; import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes'; @@ -44,6 +45,9 @@ import './engines_overview.scss'; export const EnginesOverview: React.FC = () => { const { hasPlatinumLicense } = useValues(LicensingLogic); + const { + myRole: { canManageEngines }, + } = useValues(AppLogic); const { dataLoading, @@ -91,14 +95,16 @@ export const EnginesOverview: React.FC = () => { - - {CREATE_AN_ENGINE_BUTTON_LABEL} - + {canManageEngines && ( + + {CREATE_AN_ENGINE_BUTTON_LABEL} + + )} @@ -126,14 +132,16 @@ export const EnginesOverview: React.FC = () => { - - {CREATE_A_META_ENGINE_BUTTON_LABEL} - + {canManageEngines && ( + + {CREATE_A_META_ENGINE_BUTTON_LABEL} + + )} @@ -149,13 +157,15 @@ export const EnginesOverview: React.FC = () => { title={

{META_ENGINE_EMPTY_PROMPT_TITLE}

} body={

{META_ENGINE_EMPTY_PROMPT_DESCRIPTION}

} actions={ - - {CREATE_A_META_ENGINE_BUTTON_LABEL} - + canManageEngines && ( + + {CREATE_A_META_ENGINE_BUTTON_LABEL} + + ) } /> } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx index 66f9c3c3c473d..fc37c3543af56 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx @@ -51,16 +51,21 @@ describe('EnginesTable', () => { onDeleteEngine, }; + const resetMocks = () => { + jest.clearAllMocks(); + setMockValues({ + myRole: { + canManageEngines: false, + }, + }); + }; + describe('basic table', () => { let wrapper: ReactWrapper; let table: ReactWrapper; beforeAll(() => { - jest.clearAllMocks(); - setMockValues({ - // LicensingLogic - hasPlatinumLicense: false, - }); + resetMocks(); wrapper = mountWithIntl(); table = wrapper.find(EuiBasicTable); }); @@ -101,11 +106,7 @@ describe('EnginesTable', () => { describe('loading', () => { it('passes the loading prop', () => { - jest.clearAllMocks(); - setMockValues({ - // LicensingLogic - hasPlatinumLicense: false, - }); + resetMocks(); const wrapper = mountWithIntl(); expect(wrapper.find(EuiBasicTable).prop('loading')).toEqual(true); @@ -114,6 +115,7 @@ describe('EnginesTable', () => { describe('noItemsMessage', () => { it('passes the noItemsMessage prop', () => { + resetMocks(); const wrapper = mountWithIntl(); expect(wrapper.find(EuiBasicTable).prop('noItemsMessage')).toEqual('No items.'); }); @@ -121,11 +123,7 @@ describe('EnginesTable', () => { describe('language field', () => { beforeAll(() => { - jest.clearAllMocks(); - setMockValues({ - // LicensingLogic - hasPlatinumLicense: false, - }); + resetMocks(); }); it('renders language when available', () => { @@ -181,29 +179,27 @@ describe('EnginesTable', () => { }); describe('actions', () => { - it('will hide the action buttons if the user does not have a platinum license', () => { - jest.clearAllMocks(); - setMockValues({ - // LicensingLogic - hasPlatinumLicense: false, - }); + it('will hide the action buttons if the user cannot manage/delete engines', () => { + resetMocks(); const wrapper = shallow(); const tableRow = wrapper.find(EuiTableRow).first(); expect(tableRow.find(EuiIcon)).toHaveLength(0); }); - describe('when user has a platinum license', () => { + describe('when the user can manage/delete engines', () => { let wrapper: ReactWrapper; let tableRow: ReactWrapper; let actions: ReactWrapper; beforeEach(() => { - jest.clearAllMocks(); + resetMocks(); setMockValues({ - // LicensingLogic - hasPlatinumLicense: true, + myRole: { + canManageEngines: true, + }, }); + wrapper = mountWithIntl(); tableRow = wrapper.find(EuiTableRow).first(); actions = tableRow.find(EuiIcon); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index 3b9b6e6c6a778..624e212c72702 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -19,9 +19,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedNumber } from '@kbn/i18n/react'; import { KibanaLogic } from '../../../shared/kibana'; -import { LicensingLogic } from '../../../shared/licensing'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry'; +import { AppLogic } from '../../app_logic'; import { UNIVERSAL_LANGUAGE } from '../../constants'; import { ENGINE_PATH } from '../../routes'; import { generateEncodedPath } from '../../utils/encode_path_params'; @@ -52,7 +52,9 @@ export const EnginesTable: React.FC = ({ }) => { const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const { navigateToUrl } = useValues(KibanaLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); + const { + myRole: { canManageEngines }, + } = useValues(AppLogic); const generateEncodedEnginePath = (engineName: string) => generateEncodedPath(ENGINE_PATH, { engineName }); @@ -177,6 +179,7 @@ export const EnginesTable: React.FC = ({ ), type: 'icon', icon: 'trash', + color: 'danger', onClick: (engine) => { if ( window.confirm( @@ -199,7 +202,7 @@ export const EnginesTable: React.FC = ({ ], }; - if (hasPlatinumLicense) { + if (canManageEngines) { columns.push(actionsColumn); } From f3908f77546a7baa6f0a2d6d7bf8eed4aca50144 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 24 Mar 2021 11:05:04 -0700 Subject: [PATCH 37/88] [DOCS] Forward port of information (#95340) --- .../production.asciidoc | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 8802719f95a95..726747d5d69d0 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -10,6 +10,7 @@ * <> * <> * <> +* <> * <> * <> @@ -56,6 +57,7 @@ protections. To do this, set `csp.strict` to `true` in your `kibana.yml`: +[source,js] -------- csp.strict: true -------- @@ -82,6 +84,7 @@ To use a local client node to load balance Kibana requests: . Install Elasticsearch on the same machine as Kibana. . Configure the node as a Coordinating only node. In `elasticsearch.yml`, set `node.data`, `node.master` and `node.ingest` to `false`: + +[source,js] -------- # 3. You want this node to be neither master nor data node nor ingest node, but # to act as a "search load balancer" (fetching data from nodes, @@ -94,11 +97,13 @@ node.ingest: false . Configure the client node to join your Elasticsearch cluster. In `elasticsearch.yml`, set the `cluster.name` to the name of your cluster. + +[source,js] -------- cluster.name: "my_cluster" -------- . Check your transport and HTTP host configs in `elasticsearch.yml` under `network.host` and `transport.host`. The `transport.host` needs to be on the network reachable to the cluster members, the `network.host` is the network for the HTTP connection for Kibana (localhost:9200 by default). + +[source,js] -------- network.host: localhost http.port: 9200 @@ -110,6 +115,7 @@ transport.tcp.port: 9300 - 9400 . Make sure Kibana is configured to point to your local client node. In `kibana.yml`, the `elasticsearch.hosts` setting should be set to `["localhost:9200"]`. + +[source,js] -------- # The Elasticsearch instance to use for all your queries. elasticsearch.hosts: ["http://localhost:9200"] @@ -121,12 +127,14 @@ elasticsearch.hosts: ["http://localhost:9200"] To serve multiple Kibana installations behind a load balancer, you must change the configuration. See {kibana-ref}/settings.html[Configuring Kibana] for details on each setting. Settings unique across each Kibana instance: +[source,js] -------- server.uuid server.name -------- Settings unique across each host (for example, running multiple installations on the same virtual machine): +[source,js] -------- logging.dest path.data @@ -135,6 +143,7 @@ server.port -------- Settings that must be the same: +[source,js] -------- xpack.security.encryptionKey //decrypting session information xpack.reporting.encryptionKey //decrypting reports @@ -143,11 +152,24 @@ xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys // saved objects encr -------- Separate configuration files can be used from the command line by using the `-c` flag: +[source,js] -------- bin/kibana -c config/instance1.yml bin/kibana -c config/instance2.yml -------- +[float] +[[accessing-load-balanced-kibana]] +=== Accessing multiple load-balanced {kib} clusters + +To access multiple load-balanced {kib} clusters from the same browser, +set `xpack.security.cookieName` in the configuration. +This avoids conflicts between cookies from the different {kib} instances. + +In each cluster, {kib} instances should have the same `cookieName` +value. This will achieve seamless high availability and keep the session +active in case of failure from the currently used instance. + [float] [[high-availability]] === High availability across multiple {es} nodes @@ -157,6 +179,7 @@ Kibana will transparently connect to an available node and continue operating. Currently the Console application is limited to connecting to the first node listed. In kibana.yml: +[source,js] -------- elasticsearch.hosts: - http://elasticsearch1:9200 @@ -175,6 +198,7 @@ it may make sense to tweak limits to meet more specific requirements. You can modify this limit by setting `--max-old-space-size` in the `node.options` config file that can be found inside `kibana/config` folder or any other configured with the environment variable `KBN_PATH_CONF` (for example in debian based system would be `/etc/kibana`). The option accepts a limit in MB: +[source,js] -------- --max-old-space-size=2048 -------- From 07a041ab3a42b9fbdfa62c8a717789c3d66eebf3 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 24 Mar 2021 13:28:31 -0500 Subject: [PATCH 38/88] [ML] Fix Transform runtime mapping editor so mappings can be removed (#95108) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../advanced_runtime_mappings_editor.tsx | 7 +++++-- .../advanced_runtime_mappings_settings.tsx | 3 ++- .../hooks/use_advanced_runtime_mappings_editor.ts | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx index bd37410518176..1e8397a4d9cc3 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx @@ -44,8 +44,11 @@ export const AdvancedRuntimeMappingsEditor: FC = (props) = } = props.pivotConfig; const applyChanges = () => { - const nextConfig = JSON.parse(advancedRuntimeMappingsConfig); + const nextConfig = + advancedRuntimeMappingsConfig === '' ? {} : JSON.parse(advancedRuntimeMappingsConfig); const previousConfig = runtimeMappings; applyRuntimeMappingsEditorChanges(); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts index 9bb5f91ae03c7..948c520def195 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts @@ -52,7 +52,8 @@ export const useAdvancedRuntimeMappingsEditor = (defaults: StepDefineExposedStat } = useXJsonMode(stringifiedRuntimeMappings ?? ''); const applyRuntimeMappingsEditorChanges = () => { - const parsedRuntimeMappings = JSON.parse(advancedRuntimeMappingsConfig); + const parsedRuntimeMappings = + advancedRuntimeMappingsConfig === '' ? {} : JSON.parse(advancedRuntimeMappingsConfig); const prettySourceConfig = JSON.stringify(parsedRuntimeMappings, null, 2); setRuntimeMappingsUpdated(true); setRuntimeMappings(parsedRuntimeMappings); From 2da269fe93012e6582f7a72d5f8074597f7e680f Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 24 Mar 2021 20:09:11 +0100 Subject: [PATCH 39/88] [Discover][EuiDataGrid] Fix generating reports (#93748) --- .../create_discover_grid_directive.tsx | 2 +- .../application/components/discover.test.tsx | 1 + .../application/components/discover.tsx | 2 ++ .../discover_grid/discover_grid.tsx | 15 +++++++++++++-- .../public/application/components/types.ts | 4 ++++ .../embeddable/search_embeddable.ts | 11 ++++++++++- .../embeddable/search_template_datagrid.html | 8 ++++---- .../apps/dashboard/reporting/screenshots.ts | 19 +++++++++++++++++++ 8 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx index 0d17fcbba9c23..d55e46574e1a7 100644 --- a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; @@ -38,6 +37,7 @@ export function createDiscoverGridDirective(reactDirective: any) { return reactDirective(DiscoverGridEmbeddable, [ ['columns', { watchDepth: 'collection' }], ['indexPattern', { watchDepth: 'reference' }], + ['isLoading', { watchDepth: 'reference' }], ['onAddColumn', { watchDepth: 'reference', wrapApply: false }], ['onFilter', { watchDepth: 'reference', wrapApply: false }], ['onRemoveColumn', { watchDepth: 'reference', wrapApply: false }], diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index 00554196e11fd..d804d60870421 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -62,6 +62,7 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { fetch: jest.fn(), fetchCounter: 0, fetchError: undefined, + fetchStatus: 'loading', fieldCounts: calcFieldCounts({}, esHits, indexPattern), hits: esHits.length, indexPattern, diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 1c4a5be2f2b24..056581e30b4d6 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -53,6 +53,7 @@ export function Discover({ fetchCounter, fetchError, fieldCounts, + fetchStatus, histogramData, hits, indexPattern, @@ -393,6 +394,7 @@ export function Discover({ columns={columns} expandedDoc={expandedDoc} indexPattern={indexPattern} + isLoading={fetchStatus === 'loading'} rows={rows} sort={(state.sort as SortPairArr[]) || []} sampleSize={opts.sampleSize} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 20d7d80b498a8..1888ae8562a37 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -60,6 +60,10 @@ export interface DiscoverGridProps { * The used index pattern */ indexPattern: IndexPattern; + /** + * Determines if data is currently loaded + */ + isLoading: boolean; /** * Function used to add a column in the document flyout */ @@ -135,6 +139,7 @@ export const DiscoverGrid = ({ ariaLabelledBy, columns, indexPattern, + isLoading, expandedDoc, onAddColumn, onFilter, @@ -258,7 +263,13 @@ export const DiscoverGrid = ({ isDarkMode: services.uiSettings.get('theme:darkMode'), }} > - <> + )} - + ); }; diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index e488f596cece8..db1cd89422454 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -42,6 +42,10 @@ export interface DiscoverProps { * Statistics by fields calculated using the fetched documents */ fieldCounts: Record; + /** + * Current state of data fetching + */ + fetchStatus: string; /** * Histogram aggregation data */ diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 1bf4cdc947be9..829d23fa071fb 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -168,7 +168,7 @@ export class SearchEmbeddable throw new Error('Search scope not defined'); } this.searchInstance = this.$compile( - this.services.uiSettings.get('doc_table:legacy', true) ? searchTemplate : searchTemplateGrid + this.services.uiSettings.get('doc_table:legacy') ? searchTemplate : searchTemplateGrid )(this.searchScope); const rootNode = angular.element(domNode); rootNode.append(this.searchInstance); @@ -226,6 +226,8 @@ export class SearchEmbeddable this.updateInput({ sort }); }; + searchScope.isLoading = true; + const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); searchScope.useNewFieldsApi = useNewFieldsApi; @@ -336,6 +338,9 @@ export class SearchEmbeddable searchSource.getSearchRequestBody().then((body: Record) => { inspectorRequest.json(body); }); + this.searchScope.$apply(() => { + this.searchScope!.isLoading = true; + }); this.updateOutput({ loading: true, error: undefined }); try { @@ -353,9 +358,13 @@ export class SearchEmbeddable this.searchScope.$apply(() => { this.searchScope!.hits = resp.hits.hits; this.searchScope!.totalHitCount = resp.hits.total; + this.searchScope!.isLoading = false; }); } catch (error) { this.updateOutput({ loading: false, error }); + this.searchScope.$apply(() => { + this.searchScope!.isLoading = false; + }); } }; diff --git a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html index 6524783897f8f..8ad7938350d9c 100644 --- a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html +++ b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html @@ -1,16 +1,16 @@ { before('initialize tests', async () => { @@ -150,6 +151,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(res.status).to.equal(200); expect(res.get('content-type')).to.equal('application/pdf'); }); + + it('downloads a PDF file with saved search given EuiDataGrid enabled', async function () { + await kibanaServer.uiSettings.replace({ 'doc_table:legacy': false }); + // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs + // function is taking about 15 seconds per comparison in jenkins. + this.timeout(300000); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('application/pdf'); + await kibanaServer.uiSettings.replace({}); + }); }); }); } From 983c3a0139cd9079f41088a4bd2c68c244dedb50 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 24 Mar 2021 12:37:02 -0700 Subject: [PATCH 40/88] [App Search] Log retention role fix (#95227) * LogRetentionCallout - add ability check to API call - on non-admin users, we're otherwise getting a forbidden error - remove now-unnecessary canManageLogSettings wrapping check around the link CTA, since the entire callout is now essentially gated behind the check * LogRententionTooltip - add ability check to API call - on non-admin users, we're otherwise getting a forbidden error - tests now require a ...values spread for myRole * [MISC] Refactor previous isLogRetentionUpdating check in favor of a Kea breakpoint - both dedupe calls for the commented use case, but breakpoint feels simpler & more Kea-y * PR feedback: Increase breakpoint speed --- .../components/log_retention_callout.test.tsx | 16 +++--- .../components/log_retention_callout.tsx | 36 +++++++------- .../components/log_retention_tooltip.test.tsx | 18 +++++-- .../components/log_retention_tooltip.tsx | 9 +++- .../log_retention/log_retention_logic.test.ts | 49 +++++++++---------- .../log_retention/log_retention_logic.ts | 10 ++-- 6 files changed, 71 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx index 124edb6871453..fe022391d76b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx @@ -63,15 +63,6 @@ describe('LogRetentionCallout', () => { expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual('API Logs have been disabled.'); }); - it('does not render a settings link if the user cannot manage settings', () => { - setMockValues({ myRole: { canManageLogSettings: false }, logRetention: { api: DISABLED } }); - const wrapper = mountWithIntl(); - - expect(wrapper.find(EuiCallOut)).toHaveLength(1); - expect(wrapper.find(EuiLink)).toHaveLength(0); - expect(wrapper.find('p')).toHaveLength(0); - }); - it('does not render if log retention is enabled', () => { setMockValues({ ...values, logRetention: { api: { enabled: true } } }); const wrapper = shallow(); @@ -100,5 +91,12 @@ describe('LogRetentionCallout', () => { expect(actions.fetchLogRetention).not.toHaveBeenCalled(); }); + + it('does not fetch log retention data if the user does not have access to log settings', () => { + setMockValues({ ...values, logRetention: null, myRole: { canManageLogSettings: false } }); + shallow(); + + expect(actions.fetchLogRetention).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx index 235d977793161..1fd1a9a79b225 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx @@ -40,7 +40,7 @@ export const LogRetentionCallout: React.FC = ({ type }) => { const hasLogRetention = logRetention !== null; useEffect(() => { - if (!hasLogRetention) fetchLogRetention(); + if (!hasLogRetention && canManageLogSettings) fetchLogRetention(); }, []); const logRetentionSettings = logRetention?.[type]; @@ -72,24 +72,22 @@ export const LogRetentionCallout: React.FC = ({ type }) => { ) } > - {canManageLogSettings && ( -

- - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsLinkText', - { defaultMessage: 'visit your settings' } - )} - - ), - }} - /> -

- )} +

+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsLinkText', + { defaultMessage: 'visit your settings' } + )} + + ), + }} + /> +

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx index 854a9f1d8d162..6b5693e4c301e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx @@ -19,7 +19,10 @@ import { LogRetentionOptions, LogRetentionMessage } from '../'; import { LogRetentionTooltip } from './'; describe('LogRetentionTooltip', () => { - const values = { logRetention: {} }; + const values = { + logRetention: {}, + myRole: { canManageLogSettings: true }, + }; const actions = { fetchLogRetention: jest.fn() }; beforeEach(() => { @@ -53,7 +56,7 @@ describe('LogRetentionTooltip', () => { }); it('does not render if log retention is not available', () => { - setMockValues({ logRetention: null }); + setMockValues({ ...values, logRetention: null }); const wrapper = mount(); expect(wrapper.isEmptyRender()).toBe(true); @@ -61,14 +64,21 @@ describe('LogRetentionTooltip', () => { describe('on mount', () => { it('fetches log retention data when not already loaded', () => { - setMockValues({ logRetention: null }); + setMockValues({ ...values, logRetention: null }); shallow(); expect(actions.fetchLogRetention).toHaveBeenCalled(); }); it('does not fetch log retention data if it has already been loaded', () => { - setMockValues({ logRetention: {} }); + setMockValues({ ...values, logRetention: {} }); + shallow(); + + expect(actions.fetchLogRetention).not.toHaveBeenCalled(); + }); + + it('does not fetch log retention data if the user does not have access to log settings', () => { + setMockValues({ ...values, logRetention: null, myRole: { canManageLogSettings: false } }); shallow(); expect(actions.fetchLogRetention).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx index bf074ba0272f2..ac701d4cc067b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx @@ -12,7 +12,9 @@ import { useValues, useActions } from 'kea'; import { EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LogRetentionLogic, LogRetentionMessage, LogRetentionOptions } from '../'; +import { AppLogic } from '../../../app_logic'; + +import { LogRetentionLogic, LogRetentionMessage, LogRetentionOptions } from '../index'; interface Props { type: LogRetentionOptions; @@ -21,11 +23,14 @@ interface Props { export const LogRetentionTooltip: React.FC = ({ type, position = 'bottom' }) => { const { fetchLogRetention } = useActions(LogRetentionLogic); const { logRetention } = useValues(LogRetentionLogic); + const { + myRole: { canManageLogSettings }, + } = useValues(AppLogic); const hasLogRetention = logRetention !== null; useEffect(() => { - if (!hasLogRetention) fetchLogRetention(); + if (!hasLogRetention && canManageLogSettings) fetchLogRetention(); }, []); return hasLogRetention ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index 7b63397ac6380..57f3f46fed92e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -107,21 +107,6 @@ describe('LogRetentionLogic', () => { }); }); - describe('setLogRetentionUpdating', () => { - describe('isLogRetentionUpdating', () => { - it('sets isLogRetentionUpdating to true', () => { - mount(); - - LogRetentionLogic.actions.setLogRetentionUpdating(); - - expect(LogRetentionLogic.values).toEqual({ - ...DEFAULT_VALUES, - isLogRetentionUpdating: true, - }); - }); - }); - }); - describe('clearLogRetentionUpdating', () => { describe('isLogRetentionUpdating', () => { it('resets isLogRetentionUpdating to false', () => { @@ -300,8 +285,27 @@ describe('LogRetentionLogic', () => { }); describe('fetchLogRetention', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + describe('isLogRetentionUpdating', () => { + it('sets isLogRetentionUpdating to true', () => { + mount({ + isLogRetentionUpdating: false, + }); + + LogRetentionLogic.actions.fetchLogRetention(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: true, + }); + }); + }); + it('will call an API endpoint and update log retention', async () => { mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); jest .spyOn(LogRetentionLogic.actions, 'updateLogRetention') .mockImplementationOnce(() => {}); @@ -309,14 +313,14 @@ describe('LogRetentionLogic', () => { http.get.mockReturnValue(Promise.resolve(TYPICAL_SERVER_LOG_RETENTION)); LogRetentionLogic.actions.fetchLogRetention(); - expect(LogRetentionLogic.values.isLogRetentionUpdating).toBe(true); + jest.runAllTimers(); + await nextTick(); expect(http.get).toHaveBeenCalledWith('/api/app_search/log_settings'); - await nextTick(); expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( TYPICAL_CLIENT_LOG_RETENTION ); - expect(LogRetentionLogic.values.isLogRetentionUpdating).toBe(false); + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); }); it('handles errors', async () => { @@ -325,19 +329,12 @@ describe('LogRetentionLogic', () => { http.get.mockReturnValue(Promise.reject('An error occured')); LogRetentionLogic.actions.fetchLogRetention(); + jest.runAllTimers(); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); }); - - it('does not run if isLogRetentionUpdating is true, preventing duplicate fetches', async () => { - mount({ isLogRetentionUpdating: true }); - - LogRetentionLogic.actions.fetchLogRetention(); - - expect(http.get).not.toHaveBeenCalled(); - }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts index ec078842dab55..aa1be3f8cdc64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts @@ -14,7 +14,6 @@ import { LogRetentionOptions, LogRetention, LogRetentionServer } from './types'; import { convertLogRetentionFromServerToClient } from './utils/convert_log_retention'; interface LogRetentionActions { - setLogRetentionUpdating(): void; clearLogRetentionUpdating(): void; closeModals(): void; fetchLogRetention(): void; @@ -36,7 +35,6 @@ interface LogRetentionValues { export const LogRetentionLogic = kea>({ path: ['enterprise_search', 'app_search', 'log_retention_logic'], actions: () => ({ - setLogRetentionUpdating: true, clearLogRetentionUpdating: true, closeModals: true, fetchLogRetention: true, @@ -57,7 +55,7 @@ export const LogRetentionLogic = kea false, closeModals: () => false, - setLogRetentionUpdating: () => true, + fetchLogRetention: () => true, toggleLogRetention: () => true, }, ], @@ -71,12 +69,10 @@ export const LogRetentionLogic = kea ({ - fetchLogRetention: async () => { - if (values.isLogRetentionUpdating) return; // Prevent duplicate calls to the API + fetchLogRetention: async (_, breakpoint) => { + await breakpoint(100); // Prevents duplicate calls to the API (e.g., when a tooltip & callout are on the same page) try { - actions.setLogRetentionUpdating(); - const { http } = HttpLogic.values; const response = await http.get('/api/app_search/log_settings'); From c7aba55f7b0e20ae2f5e21e364678d7e994a80dc Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 24 Mar 2021 14:02:31 -0600 Subject: [PATCH 41/88] [Maps] do not call MapEmbeddable updateInput after embeddable is destroyed (#95337) --- x-pack/plugins/maps/public/embeddable/map_embeddable.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index b7e50815fd1f7..d11e1d59b28f7 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -93,6 +93,7 @@ export class MapEmbeddable implements ReferenceOrValueEmbeddable { type = MAP_SAVED_OBJECT_TYPE; + private _isActive: boolean; private _savedMap: SavedMap; private _renderTooltipContent?: RenderToolTipContent; private _subscription: Subscription; @@ -118,6 +119,7 @@ export class MapEmbeddable parent ); + this._isActive = true; this._savedMap = new SavedMap({ mapEmbeddableInput: initialInput }); this._initializeSaveMap(); this._subscription = this.getUpdated$().subscribe(() => this.onUpdate()); @@ -404,6 +406,7 @@ export class MapEmbeddable destroy() { super.destroy(); + this._isActive = false; if (this._unsubscribeFromStore) { this._unsubscribeFromStore(); } @@ -424,6 +427,9 @@ export class MapEmbeddable } _handleStoreChanges() { + if (!this._isActive) { + return; + } const center = getMapCenter(this._savedMap.getStore().getState()); const zoom = getMapZoom(this._savedMap.getStore().getState()); From 7f740831cb9924a32460cc5008ac0fb1619be8c2 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 24 Mar 2021 22:16:38 +0200 Subject: [PATCH 42/88] Revert "use index patterns and search services for autocomplete (#92861)" (#95335) This reverts commit 2ef7f3bd0cee5f44d71e0528409bc3f8576be333. --- ...ata-server.indexpatternsserviceprovider.md | 2 +- ...rver.indexpatternsserviceprovider.setup.md | 4 +-- .../kibana-plugin-plugins-data-server.md | 1 + ...data-server.searchrequesthandlercontext.md | 11 ++++++ .../data/server/autocomplete/routes.ts | 3 +- .../autocomplete/value_suggestions_route.ts | 36 ++++++------------- src/plugins/data/server/index.ts | 4 +-- .../data/server/index_patterns/index.ts | 4 --- .../index_patterns/index_patterns_service.ts | 20 +---------- src/plugins/data/server/mocks.ts | 2 +- src/plugins/data/server/plugin.ts | 5 +-- .../data/server/search/routes/msearch.ts | 2 +- .../data/server/search/routes/search.ts | 2 +- .../data/server/search/search_service.ts | 2 +- src/plugins/data/server/search/types.ts | 11 ++++++ src/plugins/data/server/server.api.md | 15 ++++---- src/plugins/data/server/types.ts | 22 ------------ x-pack/plugins/infra/server/types.ts | 6 ++-- 18 files changed, 56 insertions(+), 96 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchrequesthandlercontext.md delete mode 100644 src/plugins/data/server/types.ts 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.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/src/plugins/data/server/autocomplete/routes.ts b/src/plugins/data/server/autocomplete/routes.ts index fc6bb0b69c102..c453094ff6874 100644 --- a/src/plugins/data/server/autocomplete/routes.ts +++ b/src/plugins/data/server/autocomplete/routes.ts @@ -9,10 +9,9 @@ import { Observable } from 'rxjs'; import { CoreSetup, SharedGlobalConfig } from 'kibana/server'; import { registerValueSuggestionsRoute } from './value_suggestions_route'; -import { DataRequestHandlerContext } from '../types'; export function registerRoutes({ http }: CoreSetup, config$: Observable): void { - const router = http.createRouter(); + const router = http.createRouter(); registerValueSuggestionsRoute(router, config$); } diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 8e6d3afa18ed5..489a23eb83897 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -12,12 +12,12 @@ import { IRouter, SharedGlobalConfig } from 'kibana/server'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { IFieldType, Filter, ES_SEARCH_STRATEGY, IEsSearchRequest } from '../index'; +import { IFieldType, Filter } from '../index'; +import { findIndexPatternById, getFieldByName } from '../index_patterns'; import { getRequestAbortedSignal } from '../lib'; -import { DataRequestHandlerContext } from '../types'; export function registerValueSuggestionsRoute( - router: IRouter, + router: IRouter, config$: Observable ) { router.post( @@ -44,40 +44,24 @@ export function registerValueSuggestionsRoute( const config = await config$.pipe(first()).toPromise(); const { field: fieldName, query, filters } = request.body; const { index } = request.params; + const { client } = context.core.elasticsearch.legacy; const signal = getRequestAbortedSignal(request.events.aborted$); - if (!context.indexPatterns) { - return response.badRequest(); - } - const autocompleteSearchOptions = { timeout: `${config.kibana.autocompleteTimeout.asMilliseconds()}ms`, terminate_after: config.kibana.autocompleteTerminateAfter.asMilliseconds(), }; - const indexPatterns = await context.indexPatterns.find(index, 1); - if (!indexPatterns || indexPatterns.length === 0) { - return response.notFound(); - } - const field = indexPatterns[0].getFieldByName(fieldName); + const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index); + + const field = indexPattern && getFieldByName(fieldName, indexPattern); const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); - const searchRequest: IEsSearchRequest = { - params: { - index, - body, - }, - }; - const { rawResponse } = await context.search - .search(searchRequest, { - strategy: ES_SEARCH_STRATEGY, - abortSignal: signal, - }) - .toPromise(); + const result = await client.callAsCurrentUser('search', { index, body }, { signal }); const buckets: any[] = - get(rawResponse, 'aggregations.suggestions.buckets') || - get(rawResponse, 'aggregations.nestedSuggestions.suggestions.buckets'); + get(result, 'aggregations.suggestions.buckets') || + get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); return response.ok({ body: map(buckets || [], 'key') }); } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index c153c0efa8892..cbf09ef57d96a 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -236,10 +236,10 @@ export { SearchUsage, SearchSessionService, ISearchSessionService, + SearchRequestHandlerContext, + DataRequestHandlerContext, } from './search'; -export { DataRequestHandlerContext } from './types'; - // Search namespace export const search = { aggs: { diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 85610cd85a3ce..7226d6f015cf8 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { IndexPatternsService } from '../../common/index_patterns'; - export * from './utils'; export { IndexPatternsFetcher, @@ -17,5 +15,3 @@ export { getCapabilitiesForRollupIndices, } from './fetcher'; export { IndexPatternsServiceProvider, IndexPatternsServiceStart } from './index_patterns_service'; - -export type IndexPatternsHandlerContext = IndexPatternsService; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index b489c29bc3b70..5d703021b94da 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -25,7 +25,6 @@ import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; import { IndexPatternsApiServer } from './index_patterns_api_client'; import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; -import { DataRequestHandlerContext } from '../types'; export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( @@ -36,7 +35,6 @@ export interface IndexPatternsServiceStart { export interface IndexPatternsServiceSetupDeps { expressions: ExpressionsServerSetup; - logger: Logger; } export interface IndexPatternsServiceStartDeps { @@ -47,27 +45,11 @@ export interface IndexPatternsServiceStartDeps { export class IndexPatternsServiceProvider implements Plugin { public setup( core: CoreSetup, - { logger, expressions }: IndexPatternsServiceSetupDeps + { expressions }: IndexPatternsServiceSetupDeps ) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); - core.http.registerRouteHandlerContext( - 'indexPatterns', - async (context, request) => { - const [coreStart, , dataStart] = await core.getStartServices(); - try { - return await dataStart.indexPatterns.indexPatternsServiceFactory( - coreStart.savedObjects.getScopedClient(request), - coreStart.elasticsearch.client.asScoped(request).asCurrentUser - ); - } catch (e) { - logger.error(e); - return undefined; - } - } - ); - registerRoutes(core.http, core.getStartServices); expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); diff --git a/src/plugins/data/server/mocks.ts b/src/plugins/data/server/mocks.ts index c82db7a141403..786dd30dbabd0 100644 --- a/src/plugins/data/server/mocks.ts +++ b/src/plugins/data/server/mocks.ts @@ -13,7 +13,7 @@ import { } from './search/mocks'; import { createFieldFormatsSetupMock, createFieldFormatsStartMock } from './field_formats/mocks'; import { createIndexPatternsStartMock } from './index_patterns/mocks'; -import { DataRequestHandlerContext } from './types'; +import { DataRequestHandlerContext } from './search'; function createSetupContract() { return { diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 3408c39cbb8e2..a7a7663d6981c 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -82,10 +82,7 @@ export class DataServerPlugin this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); - this.indexPatterns.setup(core, { - expressions, - logger: this.logger.get('indexPatterns'), - }); + this.indexPatterns.setup(core, { expressions }); core.uiSettings.register(getUiSettings()); diff --git a/src/plugins/data/server/search/routes/msearch.ts b/src/plugins/data/server/search/routes/msearch.ts index b5f06c4b343e7..b578805d8c2df 100644 --- a/src/plugins/data/server/search/routes/msearch.ts +++ b/src/plugins/data/server/search/routes/msearch.ts @@ -12,7 +12,7 @@ import { SearchRouteDependencies } from '../search_service'; import { getCallMsearch } from './call_msearch'; import { reportServerError } from '../../../../kibana_utils/server'; -import type { DataPluginRouter } from '../../types'; +import type { DataPluginRouter } from '../types'; /** * The msearch route takes in an array of searches, each consisting of header * and body json, and reformts them into a single request for the _msearch API. diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 6690e2b81f3e4..1680a9c4a7237 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -10,7 +10,7 @@ import { first } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import { getRequestAbortedSignal } from '../../lib'; import { reportServerError } from '../../../../kibana_utils/server'; -import type { DataPluginRouter } from '../../types'; +import type { DataPluginRouter } from '../types'; export function registerSearchRoute(router: DataPluginRouter): void { router.post( diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 69710e82b73b4..4dcab4eda34d1 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -29,6 +29,7 @@ import type { ISearchStrategy, SearchEnhancements, SearchStrategyDependencies, + DataRequestHandlerContext, } from './types'; import { AggsService } from './aggs'; @@ -74,7 +75,6 @@ import { ConfigSchema } from '../../config'; import { ISearchSessionService, SearchSessionService } from './session'; import { KbnServerError } from '../../../kibana_utils/server'; import { registerBsearchRoute } from './routes/bsearch'; -import { DataRequestHandlerContext } from '../types'; type StrategyMap = Record>; diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index d7aadcc348c87..e8548257c0167 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -8,10 +8,12 @@ import { Observable } from 'rxjs'; import type { + IRouter, IScopedClusterClient, IUiSettingsClient, SavedObjectsClientContract, KibanaRequest, + RequestHandlerContext, } from 'src/core/server'; import { ISearchOptions, @@ -114,3 +116,12 @@ export interface ISearchStart< } export type SearchRequestHandlerContext = IScopedSearchClient; + +/** + * @internal + */ +export interface DataRequestHandlerContext extends RequestHandlerContext { + search: SearchRequestHandlerContext; +} + +export type DataPluginRouter = IRouter; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2b7da7fe194ca..c1314ddbe6fa2 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -316,12 +316,6 @@ export const config: PluginConfigDescriptor; // @internal (undocumented) export interface DataRequestHandlerContext extends RequestHandlerContext { - // Warning: (ae-forgotten-export) The symbol "IndexPatternsHandlerContext" needs to be exported by the entry point index.d.ts - // - // (undocumented) - indexPatterns?: IndexPatternsHandlerContext; - // Warning: (ae-forgotten-export) The symbol "SearchRequestHandlerContext" needs to be exported by the entry point index.d.ts - // // (undocumented) search: SearchRequestHandlerContext; } @@ -964,7 +958,7 @@ export class IndexPatternsServiceProvider implements Plugin_3, { logger, expressions }: IndexPatternsServiceSetupDeps): void; + setup(core: CoreSetup_2, { expressions }: IndexPatternsServiceSetupDeps): void; // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1331,6 +1325,11 @@ export const search: { tabifyGetColumns: typeof tabifyGetColumns; }; +// Warning: (ae-missing-release-tag) "SearchRequestHandlerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SearchRequestHandlerContext = IScopedSearchClient; + // @internal export class SearchSessionService implements ISearchSessionService { constructor(); @@ -1522,7 +1521,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:112:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/types.ts b/src/plugins/data/server/types.ts deleted file mode 100644 index ea0fa49058d37..0000000000000 --- a/src/plugins/data/server/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { IRouter, RequestHandlerContext } from 'src/core/server'; - -import { SearchRequestHandlerContext } from './search'; -import { IndexPatternsHandlerContext } from './index_patterns'; - -/** - * @internal - */ -export interface DataRequestHandlerContext extends RequestHandlerContext { - search: SearchRequestHandlerContext; - indexPatterns?: IndexPatternsHandlerContext; -} - -export type DataPluginRouter = IRouter; 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; } From 7b35b28edc302a80cd69786824d2c982ff4bd06f Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 24 Mar 2021 13:32:52 -0700 Subject: [PATCH 43/88] [ci] try reducing Jest concurrency to mitigate random timeouts (#95348) Co-authored-by: spalger --- test/scripts/test/jest_unit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 9d472bceafd2208da3301eaf79c8cc1c8636b4fe Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 24 Mar 2021 15:45:24 -0700 Subject: [PATCH 44/88] [dev/cli/timings] report on time to dev server listening (#95120) Co-authored-by: spalger --- docs/developer/telemetry.asciidoc | 1 + src/dev/cli_dev_mode/cli_dev_mode.test.ts | 8 ++ src/dev/cli_dev_mode/cli_dev_mode.ts | 96 +++++++++++++----- src/dev/cli_dev_mode/dev_server.test.ts | 117 +++++++++++++++++++++- src/dev/cli_dev_mode/dev_server.ts | 27 +++++ 5 files changed, 224 insertions(+), 25 deletions(-) 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/src/dev/cli_dev_mode/cli_dev_mode.test.ts b/src/dev/cli_dev_mode/cli_dev_mode.test.ts index 54c49ce21505f..9ace543a8929b 100644 --- a/src/dev/cli_dev_mode/cli_dev_mode.test.ts +++ b/src/dev/cli_dev_mode/cli_dev_mode.test.ts @@ -31,6 +31,9 @@ const { Optimizer } = jest.requireMock('./optimizer'); jest.mock('./dev_server'); const { DevServer } = jest.requireMock('./dev_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: [''], @@ -208,6 +211,11 @@ describe('#start()/#stop()', () => { run$: devServerRun$, }; }); + CiStatsReporter.fromEnv.mockImplementation(() => { + return { + isEnabled: jest.fn().mockReturnValue(false), + }; + }); }); afterEach(() => { diff --git a/src/dev/cli_dev_mode/cli_dev_mode.ts b/src/dev/cli_dev_mode/cli_dev_mode.ts index 1eed8b14aed4a..f4f95f20daeef 100644 --- a/src/dev/cli_dev_mode/cli_dev_mode.ts +++ b/src/dev/cli_dev_mode/cli_dev_mode.ts @@ -10,7 +10,16 @@ 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 { + map, + mapTo, + filter, + take, + tap, + distinctUntilChanged, + switchMap, + concatMap, +} from 'rxjs/operators'; import { CliArgs } from '../../core/server/config'; import { LegacyConfig } from '../../core/server/legacy'; @@ -167,29 +176,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 +235,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/src/dev/cli_dev_mode/dev_server.test.ts b/src/dev/cli_dev_mode/dev_server.test.ts index c296c7caca63a..9962a9a285a42 100644 --- a/src/dev/cli_dev_mode/dev_server.test.ts +++ b/src/dev/cli_dev_mode/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/src/dev/cli_dev_mode/dev_server.ts index a4e32a40665e3..3daf298c82324 100644 --- a/src/dev/cli_dev_mode/dev_server.ts +++ b/src/dev/cli_dev_mode/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 * From 49078c82bc10379f0f8a9691b33f366751e4831a Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 24 Mar 2021 17:59:13 -0600 Subject: [PATCH 45/88] [core.savedObjects] Add helper for using find with pit and search_after. (#92981) --- ...er.isavedobjectspointintimefinder.close.md | 15 ++ ...ver.isavedobjectspointintimefinder.find.md | 13 ++ ...e-server.isavedobjectspointintimefinder.md | 20 ++ .../core/server/kibana-plugin-core-server.md | 3 + ...ver.savedobjectsclient.closepointintime.md | 2 + ...edobjectsclient.createpointintimefinder.md | 53 ++++++ ...a-plugin-core-server.savedobjectsclient.md | 5 +- ...vedobjectsclient.openpointintimefortype.md | 2 + ...atepointintimefinderdependencies.client.md | 11 ++ ...ectscreatepointintimefinderdependencies.md | 19 ++ ...edobjectscreatepointintimefinderoptions.md | 12 ++ ...savedobjectsrepository.closepointintime.md | 2 + ...jectsrepository.createpointintimefinder.md | 53 ++++++ ...ugin-core-server.savedobjectsrepository.md | 5 +- ...bjectsrepository.openpointintimefortype.md | 2 + ...plugin-plugins-data-server.plugin.start.md | 4 +- src/core/server/index.ts | 3 + .../export/saved_objects_exporter.ts | 11 +- .../saved_objects_service.test.ts | 8 +- .../saved_objects/saved_objects_service.ts | 1 + .../server/saved_objects/service/index.ts | 3 + .../server/saved_objects/service/lib/index.ts | 7 + .../service/lib/point_in_time_finder.mock.ts | 43 +++++ .../lib}/point_in_time_finder.test.ts | 173 ++++++++++++------ .../lib}/point_in_time_finder.ts | 128 +++++++------ .../service/lib/repository.mock.ts | 48 +++-- .../service/lib/repository.test.js | 35 ++++ .../service/lib/repository.test.mock.ts | 12 ++ .../saved_objects/service/lib/repository.ts | 75 ++++++++ .../lib/repository_create_repository.test.ts | 6 + .../service/saved_objects_client.mock.ts | 15 +- .../service/saved_objects_client.test.js | 39 ++++ .../service/saved_objects_client.ts | 69 ++++++- src/core/server/server.api.md | 19 +- src/plugins/data/server/server.api.md | 2 +- ...ypted_saved_objects_client_wrapper.test.ts | 27 +++ .../encrypted_saved_objects_client_wrapper.ts | 13 ++ ...ecure_saved_objects_client_wrapper.test.ts | 30 +++ .../secure_saved_objects_client_wrapper.ts | 16 ++ .../spaces_saved_objects_client.test.ts | 38 ++++ .../spaces_saved_objects_client.ts | 29 +++ 41 files changed, 904 insertions(+), 167 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.close.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md create mode 100644 src/core/server/saved_objects/service/lib/point_in_time_finder.mock.ts rename src/core/server/saved_objects/{export => service/lib}/point_in_time_finder.test.ts (57%) rename src/core/server/saved_objects/{export => service/lib}/point_in_time_finder.ts (58%) create mode 100644 src/core/server/saved_objects/service/lib/repository.test.mock.ts 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.md b/docs/development/core/server/kibana-plugin-core-server.md index 8dd4667002ead..4bf00d2da6e23 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -98,6 +98,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. | @@ -158,6 +159,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 +307,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.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.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.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/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/src/core/server/index.ts b/src/core/server/index.ts index 3e336dceb83d7..788c179501a80 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -282,6 +282,9 @@ export type { SavedObjectsClientFactoryProvider, SavedObjectsClosePointInTimeOptions, SavedObjectsClosePointInTimeResponse, + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreateOptions, SavedObjectsExportResultDetails, SavedObjectsFindResult, 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/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/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 58% 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..b8f459151e7b3 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 @@ -6,79 +6,76 @@ * Side Public License, v 1. */ -import { Logger } from '../../logging'; -import { SavedObjectsClientContract, SavedObjectsFindOptions } from '../types'; -import { SavedObjectsFindResponse } from '../service'; +import type { Logger } from '../../../logging'; +import type { SavedObjectsFindOptions, SavedObjectsClientContract } from '../../types'; +import type { SavedObjectsFindResponse } from '../'; + +type PointInTimeFinderClient = Pick< + SavedObjectsClientContract, + 'find' | 'openPointInTimeForType' | 'closePointInTime' +>; + +/** + * @public + */ +export type SavedObjectsCreatePointInTimeFinderOptions = Omit< + SavedObjectsFindOptions, + 'page' | 'pit' | 'searchAfter' +>; /** - * 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 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. @@ -110,7 +107,7 @@ export class PointInTimeFinder { 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}`); @@ -164,7 +162,7 @@ export class PointInTimeFinder { searchAfter?: unknown[]; }) { 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', 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..bff23895fe459 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'; @@ -39,6 +43,7 @@ describe('SavedObjectsRepository', () => { let client; let savedObjectsRepository; let migrator; + let logger; let serializer; const mockTimestamp = '2017-08-14T15:49:14.886Z'; @@ -238,11 +243,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 +276,7 @@ describe('SavedObjectsRepository', () => { typeRegistry: registry, serializer, allowedTypes, + logger, }); savedObjectsRepository._getCurrentTime = jest.fn(() => mockTimestamp); @@ -4632,4 +4640,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..a302cfe5a1e6f 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -13,7 +13,14 @@ import { GetResponse, SearchResponse, } from '../../../elasticsearch/'; +import { 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'; @@ -89,6 +96,7 @@ export interface SavedObjectsRepositoryOptions { serializer: SavedObjectsSerializer; migrator: IKibanaMigrator; allowedTypes: string[]; + logger: Logger; } /** @@ -148,6 +156,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 +171,7 @@ export class SavedObjectsRepository { typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, + logger: Logger, includedHiddenTypes: string[] = [], injectedConstructor: any = SavedObjectsRepository ): ISavedObjectsRepository { @@ -187,6 +197,7 @@ export class SavedObjectsRepository { serializer, allowedTypes, client, + logger, }); } @@ -199,6 +210,7 @@ export class SavedObjectsRepository { serializer, migrator, allowedTypes = [], + logger, } = options; // It's important that we migrate documents / mark them as up-to-date @@ -218,6 +230,7 @@ export class SavedObjectsRepository { } this._allowedTypes = allowedTypes; this._serializer = serializer; + this._logger = logger; } /** @@ -1788,6 +1801,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( @@ -1853,6 +1869,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 @@ -1896,6 +1915,62 @@ export class SavedObjectsRepository { 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 * 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/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..9fa2896b7bbfe 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, @@ -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/server.api.md b/src/core/server/server.api.md index 580315973ce8f..3d2023108c46a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1177,6 +1177,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; @@ -2219,6 +2225,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 +2328,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; @@ -2811,10 +2827,11 @@ 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; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c1314ddbe6fa2..e04fcdfa08f36 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1226,7 +1226,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 76f5cb49c7f07..d18e7e427eeca 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -1820,3 +1820,30 @@ describe('#closePointInTime', () => { expect(mockBaseClient.closePointInTime).toHaveBeenCalledTimes(1); }); }); + +describe('#createPointInTimeFinder', () => { + it('redirects request to underlying base client with default dependencies', () => { + const options = { type: ['a', 'b'], search: 'query' }; + wrapper.createPointInTimeFinder(options); + + expect(mockBaseClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(mockBaseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, { + client: wrapper, + }); + }); + + it('redirects request to underlying base client with custom dependencies', () => { + const options = { type: ['a', 'b'], search: 'query' }; + const dependencies = { + client: { + find: jest.fn(), + openPointInTimeForType: jest.fn(), + closePointInTime: jest.fn(), + }, + }; + wrapper.createPointInTimeFinder(options, dependencies); + + expect(mockBaseClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(mockBaseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); + }); +}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 6b06f7e4e68e9..88a89af6be3d0 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -19,6 +19,8 @@ import type { SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, SavedObjectsCreateOptions, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsFindResponse, @@ -263,6 +265,17 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon return await this.options.baseClient.closePointInTime(id, options); } + public createPointInTimeFinder( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ) { + return this.options.baseClient.createPointInTimeFinder(findOptions, { + client: this, + // Include dependencies last so that subsequent SO client wrappers have their settings applied. + ...dependencies, + }); + } + /** * Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't * registered, response is returned as is. diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 803b36e520a2f..554244dc98be9 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -1058,6 +1058,36 @@ describe('#closePointInTime', () => { }); }); +describe('#createPointInTimeFinder', () => { + it('redirects request to underlying base client with default dependencies', () => { + const options = { type: ['a', 'b'], search: 'query' }; + client.createPointInTimeFinder(options); + + expect(clientOpts.baseClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, { + client, + }); + }); + + it('redirects request to underlying base client with custom dependencies', () => { + const options = { type: ['a', 'b'], search: 'query' }; + const dependencies = { + client: { + find: jest.fn(), + openPointInTimeForType: jest.fn(), + closePointInTime: jest.fn(), + }, + }; + client.createPointInTimeFinder(options, dependencies); + + expect(clientOpts.baseClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.createPointInTimeFinder).toHaveBeenCalledWith( + options, + dependencies + ); + }); +}); + describe('#resolve', () => { const type = 'foo'; const id = `${type}-id`; diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 1858bc7108dc9..8378cc4d848cf 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -16,6 +16,8 @@ import type { SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, SavedObjectsCreateOptions, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsOpenPointInTimeOptions, @@ -616,6 +618,20 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.closePointInTime(id, options); } + public createPointInTimeFinder( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ) { + // We don't need to perform an authorization check here or add an audit log, because + // `createPointInTimeFinder` is simply a helper that calls `find`, `openPointInTimeForType`, + // and `closePointInTime` internally, so authz checks and audit logs will already be applied. + return this.baseClient.createPointInTimeFinder(findOptions, { + client: this, + // Include dependencies last so that subsequent SO client wrappers have their settings applied. + ...dependencies, + }); + } + private async checkPrivileges( actions: string | string[], namespaceOrNamespaces?: string | Array diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index fa53f110e30c3..cbb71d4bbcf81 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -643,5 +643,43 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); }); }); + + describe('#createPointInTimeFinder', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = createSpacesSavedObjectsClient(); + + const options = { type: ['a', 'b'], search: 'query', namespace: 'oops' }; + expect(() => client.createPointInTimeFinder(options)).toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + it('redirects request to underlying base client with default dependencies', () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + + const options = { type: ['a', 'b'], search: 'query' }; + client.createPointInTimeFinder(options); + + expect(baseClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(baseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, { + client, + }); + }); + + it('redirects request to underlying base client with custom dependencies', () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + + const options = { type: ['a', 'b'], search: 'query' }; + const dependencies = { + client: { + find: jest.fn(), + openPointInTimeForType: jest.fn(), + closePointInTime: jest.fn(), + }, + }; + client.createPointInTimeFinder(options, dependencies); + + expect(baseClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(baseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); + }); + }); }); }); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index f70714b8ad102..c544e2f46f058 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -18,6 +18,8 @@ import type { SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, SavedObjectsCreateOptions, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsOpenPointInTimeOptions, @@ -420,4 +422,31 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { namespace: spaceIdToNamespace(this.spaceId), }); } + + /** + * 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`. + * + * @param {object} findOptions - {@link SavedObjectsCreatePointInTimeFinderOptions} + * @param {object} [dependencies] - {@link SavedObjectsCreatePointInTimeFinderDependencies} + */ + createPointInTimeFinder( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ) { + throwErrorIfNamespaceSpecified(findOptions); + // We don't need to handle namespaces here, because `createPointInTimeFinder` + // is simply a helper that calls `find`, `openPointInTimeForType`, and + // `closePointInTime` internally, so namespaces will already be handled + // in those methods. + return this.client.createPointInTimeFinder(findOptions, { + client: this, + // Include dependencies last so that subsequent SO client wrappers have their settings applied. + ...dependencies, + }); + } } From a7ac120b697d1f00d0a953dc3b3c65fdc5c27bba Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 25 Mar 2021 03:54:39 +0000 Subject: [PATCH 46/88] fix(NA): support inspect flags on ensure_node_preserve_symlinks script (#95344) * fix(NA): support inspect flags on ensure_node_preserve_symlinks script * chore(NA): fix wording on function test runner schema file * chore(NA): update execargv array in case of --inspect port --- .../lib/config/schema.ts | 4 +- .../functional_tests/lib/run_kibana_server.js | 6 +-- .../ensure_node_preserve_symlinks.js | 43 ++++++++++++++++++- 3 files changed, 45 insertions(+), 8 deletions(-) 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/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); From 070ca4bb103171db8aefc0cd36631a67eef1db97 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 25 Mar 2021 04:06:44 +0000 Subject: [PATCH 47/88] skip flaky suite (#89318 , #89319) --- src/core/server/http/cookie_session_storage.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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); From 6b5023a681673a0930b2dd34c203bbca3cb0beac Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 25 Mar 2021 07:34:01 +0100 Subject: [PATCH 48/88] [Discover] Deangularize controller part 1 (#93896) * Refactor minimumVisibleRows * Extract setupVisualization function * Extract getDimensions function * Inline breadcrumb and help menu function exec to discover.tsx * Extract getStateDefault * Remove unnecessary code * Improve performance by React.memo --- .../__mocks__/index_pattern_with_timefield.ts | 8 +- .../discover/public/__mocks__/ui_settings.ts | 4 +- .../public/application/angular/discover.js | 395 ++++-------------- .../application/angular/discover_legacy.html | 3 - .../doc_table/create_doc_table_react.tsx | 36 +- .../angular/doc_table/lib/get_sort.ts | 4 +- .../application/components/constants.ts | 14 + .../components/create_discover_directive.ts | 3 - .../application/components/discover.tsx | 35 +- .../discover_grid_columns.test.tsx | 27 +- .../{help_menu_util.js => help_menu_util.ts} | 5 +- .../apply_aggs_to_search_source.test.ts | 88 ++++ .../histogram/apply_aggs_to_search_source.ts | 50 +++ .../histogram/get_dimensions.test.ts | 63 +++ .../components/histogram/get_dimensions.ts | 52 +++ .../application/components/histogram/index.ts | 10 + .../timechart_header.test.tsx | 17 +- .../timechart_header/timechart_header.tsx | 26 +- .../public/application/components/types.ts | 4 - .../helpers/get_result_state.test.ts | 45 ++ .../application/helpers/get_result_state.ts | 31 ++ .../helpers/get_state_defaults.test.ts | 65 +++ .../application/helpers/get_state_defaults.ts | 62 +++ test/functional/apps/discover/_discover.ts | 20 +- test/functional/apps/discover/_doc_table.ts | 2 + test/functional/page_objects/discover_page.ts | 9 + 26 files changed, 704 insertions(+), 374 deletions(-) create mode 100644 src/plugins/discover/public/application/components/constants.ts rename src/plugins/discover/public/application/components/help_menu/{help_menu_util.js => help_menu_util.ts} (81%) create mode 100644 src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts create mode 100644 src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts create mode 100644 src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts create mode 100644 src/plugins/discover/public/application/components/histogram/get_dimensions.ts create mode 100644 src/plugins/discover/public/application/components/histogram/index.ts create mode 100644 src/plugins/discover/public/application/helpers/get_result_state.test.ts create mode 100644 src/plugins/discover/public/application/helpers/get_result_state.ts create mode 100644 src/plugins/discover/public/application/helpers/get_state_defaults.test.ts create mode 100644 src/plugins/discover/public/application/helpers/get_state_defaults.ts diff --git a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts index b53e5328f21ba..ad84518af9de3 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts @@ -22,6 +22,8 @@ const fields = [ type: 'date', scripted: false, filterable: true, + aggregatable: true, + sortable: true, }, { name: 'message', @@ -34,12 +36,14 @@ const fields = [ type: 'string', scripted: false, filterable: true, + aggregatable: true, }, { name: 'bytes', type: 'number', scripted: false, filterable: true, + aggregatable: true, }, { name: 'scripted', @@ -55,14 +59,14 @@ fields.getByName = (name: string) => { const indexPattern = ({ id: 'index-pattern-with-timefield-id', - title: 'index-pattern-without-timefield', + title: 'index-pattern-with-timefield', metaFields: ['_index', '_score'], flattenHit: undefined, formatHit: jest.fn((hit) => hit._source), fields, getComputedFields: () => ({}), getSourceFiltering: () => ({}), - getFieldByName: () => ({}), + getFieldByName: (name: string) => fields.getByName(name), timeFieldName: 'timestamp', } as unknown) as IndexPattern; diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index 8bc6de1b9ca41..e021a39a568e9 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,12 +7,14 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { SAMPLE_SIZE_SETTING } from '../../common'; +import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING } from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { if (key === SAMPLE_SIZE_SETTING) { return 10; + } else if (key === DEFAULT_COLUMNS_SETTING) { + return ['default_column']; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 4a761f2fefa65..2c80fc111c740 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -9,8 +9,6 @@ import _ from 'lodash'; import { merge, Subject, Subscription } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import moment from 'moment'; -import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; @@ -23,7 +21,6 @@ import { } from '../../../../data/public'; import { getSortArray } from './doc_table'; import indexTemplateLegacy from './discover_legacy.html'; -import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; import { getAngularModule, @@ -36,25 +33,22 @@ import { subscribeWithScope, tabifyAggResponse, } from '../../kibana_services'; -import { - getRootBreadcrumbs, - getSavedSearchBreadcrumbs, - setBreadcrumbsTitle, -} from '../helpers/breadcrumbs'; +import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; +import { getStateDefaults } from '../helpers/get_state_defaults'; +import { getResultState } from '../helpers/get_result_state'; import { validateTimeRange } from '../helpers/validate_time_range'; import { addFatalError } from '../../../../kibana_legacy/public'; import { - DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, - SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern'; import { updateSearchSource } from '../helpers/update_search_source'; import { calcFieldCounts } from '../helpers/calc_field_counts'; -import { getDefaultSort } from './doc_table/lib/get_default_sort'; import { DiscoverSearchSessionManager } from './discover_search_session'; +import { applyAggsToSearchSource, getDimensions } from '../components/histogram'; +import { fetchStatuses } from '../components/constants'; const services = getServices(); @@ -70,13 +64,6 @@ const { uiSettings: config, } = getServices(); -const fetchStatuses = { - UNINITIALIZED: 'uninitialized', - LOADING: 'loading', - COMPLETE: 'complete', - ERROR: 'error', -}; - const app = getAngularModule(); app.config(($routeProvider) => { @@ -161,7 +148,7 @@ app.directive('discoverApp', function () { }; }); -function discoverController($route, $scope, Promise) { +function discoverController($route, $scope) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); @@ -191,7 +178,14 @@ function discoverController($route, $scope, Promise) { }); const stateContainer = getState({ - getStateDefaults, + getStateDefaults: () => + getStateDefaults({ + config, + data, + indexPattern: $scope.indexPattern, + savedSearch, + searchSource: persistentSearchSource, + }), storeInSessionStorage: config.get('state:storeInSessionStorage'), history, toasts: core.notifications.toasts, @@ -232,6 +226,21 @@ function discoverController($route, $scope, Promise) { query: true, } ); + const showUnmappedFields = $scope.useNewFieldsApi; + const updateSearchSourceHelper = () => { + const { indexPattern, useNewFieldsApi } = $scope; + const { columns, sort } = $scope.state; + updateSearchSource({ + persistentSearchSource, + volatileSearchSource: $scope.volatileSearchSource, + indexPattern, + services, + sort, + columns, + useNewFieldsApi, + showUnmappedFields, + }); + }; const appStateUnsubscribe = appStateContainer.subscribe(async (newState) => { const { state: newStatePartial } = splitState(newState); @@ -293,21 +302,6 @@ function discoverController($route, $scope, Promise) { } ); - // update data source when filters update - subscriptions.add( - subscribeWithScope( - $scope, - filterManager.getUpdates$(), - { - next: () => { - $scope.state.filters = filterManager.getAppFilters(); - $scope.updateDataSource(); - }, - }, - (error) => addFatalError(core.fatalErrors, error) - ) - ); - $scope.opts = { // number of records to fetch, then paginate through sampleSize: config.get(SAMPLE_SIZE_SETTING), @@ -329,8 +323,19 @@ function discoverController($route, $scope, Promise) { requests: new RequestAdapter(), }); - $scope.minimumVisibleRows = 50; + const shouldSearchOnPageLoad = () => { + // A saved search is created on every page load, so we check the ID to see if we're loading a + // previously saved search or if it is just transient + return ( + config.get(SEARCH_ON_PAGE_LOAD_SETTING) || + savedSearch.id !== undefined || + timefilter.getRefreshInterval().pause === false || + searchSessionManager.hasSearchSessionIdInURL() + ); + }; + $scope.fetchStatus = fetchStatuses.UNINITIALIZED; + $scope.resultState = shouldSearchOnPageLoad() ? 'loading' : 'uninitialized'; let abortController; $scope.$on('$destroy', () => { @@ -385,157 +390,12 @@ function discoverController($route, $scope, Promise) { volatileSearchSource.setParent(persistentSearchSource); $scope.volatileSearchSource = volatileSearchSource; - - const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; - chrome.docTitle.change(`Discover${pageTitleSuffix}`); - - setBreadcrumbsTitle(savedSearch, chrome); - - function getDefaultColumns() { - if (savedSearch.columns.length > 0) { - return [...savedSearch.columns]; - } - return [...config.get(DEFAULT_COLUMNS_SETTING)]; - } - - function getStateDefaults() { - const query = - persistentSearchSource.getField('query') || data.query.queryString.getDefaultQuery(); - const sort = getSortArray(savedSearch.sort, $scope.indexPattern); - const columns = getDefaultColumns(); - - const defaultState = { - query, - sort: !sort.length - ? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) - : sort, - columns, - index: $scope.indexPattern.id, - interval: 'auto', - filters: _.cloneDeep(persistentSearchSource.getOwnField('filter')), - }; - if (savedSearch.grid) { - defaultState.grid = savedSearch.grid; - } - if (savedSearch.hideChart) { - defaultState.hideChart = savedSearch.hideChart; - } - - return defaultState; - } - $scope.state.index = $scope.indexPattern.id; $scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern); - const shouldSearchOnPageLoad = () => { - // A saved search is created on every page load, so we check the ID to see if we're loading a - // previously saved search or if it is just transient - return ( - config.get(SEARCH_ON_PAGE_LOAD_SETTING) || - savedSearch.id !== undefined || - timefilter.getRefreshInterval().pause === false || - searchSessionManager.hasSearchSessionIdInURL() - ); - }; - - const init = _.once(() => { - $scope.updateDataSource().then(async () => { - const fetch$ = merge( - refetch$, - filterManager.getFetches$(), - timefilter.getFetch$(), - timefilter.getAutoRefreshFetch$(), - data.query.queryString.getUpdates$(), - searchSessionManager.newSearchSessionIdFromURL$ - ).pipe(debounceTime(100)); - - subscriptions.add( - subscribeWithScope( - $scope, - fetch$, - { - next: $scope.fetch, - }, - (error) => addFatalError(core.fatalErrors, error) - ) - ); - subscriptions.add( - subscribeWithScope( - $scope, - timefilter.getTimeUpdate$(), - { - next: () => { - $scope.updateTime(); - }, - }, - (error) => addFatalError(core.fatalErrors, error) - ) - ); - - $scope.$watchMulti( - ['rows', 'fetchStatus'], - (function updateResultState() { - let prev = {}; - const status = { - UNINITIALIZED: 'uninitialized', - LOADING: 'loading', // initial data load - READY: 'ready', // results came back - NO_RESULTS: 'none', // no results came back - }; - - function pick(rows, oldRows, fetchStatus) { - // initial state, pretend we're already loading if we're about to execute a search so - // that the uninitilized message doesn't flash on screen - if (!$scope.fetchError && rows == null && oldRows == null && shouldSearchOnPageLoad()) { - return status.LOADING; - } - - if (fetchStatus === fetchStatuses.UNINITIALIZED) { - return status.UNINITIALIZED; - } - - const rowsEmpty = _.isEmpty(rows); - if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return status.LOADING; - else if (!rowsEmpty) return status.READY; - else return status.NO_RESULTS; - } - - return function () { - const current = { - rows: $scope.rows, - fetchStatus: $scope.fetchStatus, - }; - - $scope.resultState = pick( - current.rows, - prev.rows, - current.fetchStatus, - prev.fetchStatus - ); - - prev = current; - }; - })() - ); - - if (getTimeField()) { - setupVisualization(); - $scope.updateTime(); - } - - init.complete = true; - if (shouldSearchOnPageLoad()) { - refetch$.next(); - } - }); - }); - $scope.opts.fetch = $scope.fetch = function () { - // ignore requests to fetch before the app inits - if (!init.complete) return; $scope.fetchCounter++; $scope.fetchError = undefined; - $scope.minimumVisibleRows = 50; if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { $scope.resultState = 'none'; return; @@ -546,17 +406,23 @@ function discoverController($route, $scope, Promise) { abortController = new AbortController(); const searchSessionId = searchSessionManager.getNextSearchSessionId(); + updateSearchSourceHelper(); - $scope - .updateDataSource() - .then(setupVisualization) - .then(function () { - $scope.fetchStatus = fetchStatuses.LOADING; - logInspectorRequest({ searchSessionId }); - return $scope.volatileSearchSource.fetch({ - abortSignal: abortController.signal, - sessionId: searchSessionId, - }); + $scope.opts.chartAggConfigs = applyAggsToSearchSource( + getTimeField() && !$scope.state.hideChart, + volatileSearchSource, + $scope.state.interval, + $scope.indexPattern, + data + ); + + $scope.fetchStatus = fetchStatuses.LOADING; + $scope.resultState = getResultState($scope.fetchStatus, $scope.rows); + logInspectorRequest({ searchSessionId }); + return $scope.volatileSearchSource + .fetch({ + abortSignal: abortController.signal, + sessionId: searchSessionId, }) .then(onResults) .catch((error) => { @@ -565,40 +431,14 @@ function discoverController($route, $scope, Promise) { $scope.fetchStatus = fetchStatuses.NO_RESULTS; $scope.fetchError = error; - data.search.showError(error); + }) + .finally(() => { + $scope.resultState = getResultState($scope.fetchStatus, $scope.rows); + $scope.$apply(); }); }; - function getDimensions(aggs, timeRange) { - const [metric, agg] = aggs; - agg.params.timeRange = timeRange; - const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; - agg.buckets.setBounds(bounds); - - const { esUnit, esValue } = agg.buckets.getInterval(); - return { - x: { - accessor: 0, - label: agg.makeLabel(), - format: agg.toSerializedFieldFormat(), - params: { - date: true, - interval: moment.duration(esValue, esUnit), - intervalESValue: esValue, - intervalESUnit: esUnit, - format: agg.buckets.getScaledDateFormat(), - bounds: agg.buckets.getBounds(), - }, - }, - y: { - accessor: 1, - format: metric.toSerializedFieldFormat(), - label: metric.makeLabel(), - }, - }; - } - function onResults(resp) { inspectorRequest .stats(getResponseInspectorStats(resp, $scope.volatileSearchSource)) @@ -607,11 +447,10 @@ function discoverController($route, $scope, Promise) { if (getTimeField() && !$scope.state.hideChart) { const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp); $scope.volatileSearchSource.rawResponse = resp; - $scope.histogramData = discoverResponseHandler( - tabifiedData, - getDimensions($scope.opts.chartAggConfigs.aggs, $scope.timeRange) - ); - $scope.updateTime(); + const dimensions = getDimensions($scope.opts.chartAggConfigs, data); + if (dimensions) { + $scope.histogramData = discoverResponseHandler(tabifiedData, dimensions); + } } $scope.hits = resp.hits.total; @@ -640,15 +479,6 @@ function discoverController($route, $scope, Promise) { }); } - $scope.updateTime = function () { - const { from, to } = timefilter.getTime(); - // this is the timerange for the histogram, should be refactored - $scope.timeRange = { - from: dateMath.parse(from), - to: dateMath.parse(to, { roundUp: true }), - }; - }; - $scope.resetQuery = function () { history.push( $route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/' @@ -656,88 +486,39 @@ function discoverController($route, $scope, Promise) { $route.reload(); }; - $scope.onSkipBottomButtonClick = async () => { - // show all the Rows - $scope.minimumVisibleRows = $scope.hits; - - // delay scrolling to after the rows have been rendered - const bottomMarker = document.getElementById('discoverBottomMarker'); - const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - - while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { - await wait(50); - } - bottomMarker.focus(); - await wait(50); - bottomMarker.blur(); - }; - $scope.newQuery = function () { history.push('/'); }; - const showUnmappedFields = $scope.useNewFieldsApi; - $scope.unmappedFieldsConfig = { showUnmappedFields, }; - $scope.updateDataSource = () => { - const { indexPattern, useNewFieldsApi } = $scope; - const { columns, sort } = $scope.state; - updateSearchSource({ - persistentSearchSource, - volatileSearchSource: $scope.volatileSearchSource, - indexPattern, - services, - sort, - columns, - useNewFieldsApi, - showUnmappedFields, - }); - return Promise.resolve(); - }; - - async function setupVisualization() { - // If no timefield has been specified we don't create a histogram of messages - if (!getTimeField() || $scope.state.hideChart) { - if ($scope.volatileSearchSource.getField('aggs')) { - // cleanup aggs field in case it was set before - $scope.volatileSearchSource.removeField('aggs'); - } - return; - } - const { interval: histogramInterval } = $scope.state; + const fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getAutoRefreshFetch$(), + data.query.queryString.getUpdates$(), + searchSessionManager.newSearchSessionIdFromURL$ + ).pipe(debounceTime(100)); - const visStateAggs = [ - { - type: 'count', - schema: 'metric', - }, + subscriptions.add( + subscribeWithScope( + $scope, + fetch$, { - type: 'date_histogram', - schema: 'segment', - params: { - field: getTimeField(), - interval: histogramInterval, - timeRange: timefilter.getTime(), - }, + next: $scope.fetch, }, - ]; - $scope.opts.chartAggConfigs = data.search.aggs.createAggConfigs( - $scope.indexPattern, - visStateAggs - ); - - $scope.volatileSearchSource.setField('aggs', function () { - if (!$scope.opts.chartAggConfigs) return; - return $scope.opts.chartAggConfigs.toDsl(); - }); - } - - addHelpMenuToAppChrome(chrome); + (error) => addFatalError(core.fatalErrors, error) + ) + ); - init(); - // Propagate current app state to url, then start syncing - replaceUrlAppState().then(() => startStateSync()); + // Propagate current app state to url, then start syncing and fetching + replaceUrlAppState().then(() => { + startStateSync(); + if (shouldSearchOnPageLoad()) { + refetch$.next(); + } + }); } diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index a01f285b1a150..f14800f81d08e 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -7,15 +7,12 @@ histogram-data="histogramData" hits="hits" index-pattern="indexPattern" - minimum-visible-rows="minimumVisibleRows" - on-skip-bottom-button-click="onSkipBottomButtonClick" opts="opts" reset-query="resetQuery" result-state="resultState" rows="rows" search-source="volatileSearchSource" state="state" - time-range="timeRange" top-nav-menu="topNavMenu" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index e768b750aa134..ba4d56b935512 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -8,11 +8,12 @@ import angular, { auto, ICompileService, IScope } from 'angular'; import { render } from 'react-dom'; -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState, useCallback } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices, IIndexPattern } from '../../../kibana_services'; import { IndexPatternField } from '../../../../../data/common/index_patterns'; +import { SkipBottomButton } from '../../components/skip_bottom_button'; export interface DocTableLegacyProps { columns: string[]; @@ -97,18 +98,42 @@ function getRenderFn(domNode: Element, props: any) { export function DocTableLegacy(renderProps: DocTableLegacyProps) { const ref = useRef(null); const scope = useRef(); + const [rows, setRows] = useState(renderProps.rows); + const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); + const onSkipBottomButtonClick = useCallback(async () => { + // delay scrolling to after the rows have been rendered + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + // show all the rows + setMinimumVisibleRows(renderProps.rows.length); + + while (renderProps.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker!.focus(); + await wait(50); + bottomMarker!.blur(); + }, [setMinimumVisibleRows, renderProps.rows]); + + useEffect(() => { + if (minimumVisibleRows > 50) { + setMinimumVisibleRows(50); + } + setRows(renderProps.rows); + }, [renderProps.rows, minimumVisibleRows, setMinimumVisibleRows]); useEffect(() => { if (ref && ref.current && !scope.current) { - const fn = getRenderFn(ref.current, renderProps); + const fn = getRenderFn(ref.current, { ...renderProps, rows, minimumVisibleRows }); fn().then((newScope) => { scope.current = newScope; }); } else if (scope && scope.current) { - scope.current.renderProps = renderProps; + scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows }; scope.current.$apply(); } - }, [renderProps]); + }, [renderProps, minimumVisibleRows, rows]); + useEffect(() => { return () => { if (scope.current) { @@ -118,6 +143,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) { }, []); return (
+
{renderProps.rows.length === renderProps.sampleSize ? (
- +
diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts index 4838a4019357c..4b16c1aa3dcc6 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts @@ -14,9 +14,9 @@ export type SortPairArr = [string, string]; export type SortPair = SortPairArr | SortPairObj; export type SortInput = SortPair | SortPair[]; -export function isSortable(fieldName: string, indexPattern: IndexPattern) { +export function isSortable(fieldName: string, indexPattern: IndexPattern): boolean { const field = indexPattern.getFieldByName(fieldName); - return field && field.sortable; + return !!(field && field.sortable); } function createSortObject( diff --git a/src/plugins/discover/public/application/components/constants.ts b/src/plugins/discover/public/application/components/constants.ts new file mode 100644 index 0000000000000..42845e83b7435 --- /dev/null +++ b/src/plugins/discover/public/application/components/constants.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 fetchStatuses = { + UNINITIALIZED: 'uninitialized', + LOADING: 'loading', + COMPLETE: 'complete', + ERROR: 'error', +}; diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 8d1360aeaddad..5abf87fdfbc08 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -16,8 +16,6 @@ export function createDiscoverDirective(reactDirective: any) { ['histogramData', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], - ['minimumVisibleRows', { watchDepth: 'reference' }], - ['onSkipBottomButtonClick', { watchDepth: 'reference' }], ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], @@ -26,7 +24,6 @@ export function createDiscoverDirective(reactDirective: any) { ['searchSource', { watchDepth: 'reference' }], ['showSaveQuery', { watchDepth: 'reference' }], ['state', { watchDepth: 'reference' }], - ['timeRange', { watchDepth: 'reference' }], ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], ['updateSavedQueryId', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 056581e30b4d6..9615a1c10ea8e 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import './discover.scss'; -import React, { useState, useRef, useMemo, useCallback } from 'react'; +import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -30,7 +30,6 @@ import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives' import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; -import { SkipBottomButton } from './skip_bottom_button'; import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; @@ -42,11 +41,15 @@ import { DocViewFilterFn } from '../doc_views/doc_views_types'; import { DiscoverGrid } from './discover_grid/discover_grid'; import { DiscoverTopNav } from './discover_topnav'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { setBreadcrumbsTitle } from '../helpers/breadcrumbs'; +import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); const SidebarMemoized = React.memo(DiscoverSidebarResponsive); const DataGridMemoized = React.memo(DiscoverGrid); const TopNavMemoized = React.memo(DiscoverTopNav); +const TimechartHeaderMemoized = React.memo(TimechartHeader); +const DiscoverHistogramMemoized = React.memo(DiscoverHistogram); export function Discover({ fetch, @@ -58,14 +61,12 @@ export function Discover({ hits, indexPattern, minimumVisibleRows, - onSkipBottomButtonClick, opts, resetQuery, resultState, rows, searchSource, state, - timeRange, unmappedFieldsConfig, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); @@ -81,13 +82,16 @@ export function Discover({ }, [state, opts]); const hideChart = useMemo(() => state.hideChart, [state]); const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; - const { trackUiMetric, capabilities, indexPatterns } = services; + const { trackUiMetric, capabilities, indexPatterns, chrome, docLinks } = services; + const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; - const bucketInterval = - bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + const bucketInterval = useMemo(() => { + const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; + return bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) ? bucketAggConfig.buckets?.getInterval() : undefined; + }, [opts.chartAggConfigs]); + const contentCentered = resultState === 'uninitialized'; const isLegacy = services.uiSettings.get('doc_table:legacy'); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); @@ -101,6 +105,14 @@ export function Discover({ [opts] ); + useEffect(() => { + const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; + chrome.docTitle.change(`Discover${pageTitleSuffix}`); + + setBreadcrumbsTitle(savedSearch, chrome); + addHelpMenuToAppChrome(chrome, docLinks); + }, [savedSearch, chrome, docLinks]); + const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( () => getStateColumnActions({ @@ -293,9 +305,9 @@ export function Discover({ {!hideChart && ( - )} - {isLegacy && } {!hideChart && opts.timefield && ( @@ -342,7 +353,7 @@ export function Discover({ className={isLegacy ? 'dscHistogram' : 'dscHistogramGrid'} data-test-subj="discoverChart" > - diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx index 1a721a400803e..93b5bf8fde0c1 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -64,11 +64,14 @@ describe('Discover grid columns ', function () { "showMoveLeft": false, "showMoveRight": false, }, - "cellActions": undefined, + "cellActions": Array [ + [Function], + [Function], + ], "display": undefined, "id": "extension", "isSortable": false, - "schema": "kibana-json", + "schema": "string", }, Object { "actions": Object { @@ -80,7 +83,7 @@ describe('Discover grid columns ', function () { "display": undefined, "id": "message", "isSortable": false, - "schema": "kibana-json", + "schema": "string", }, ] `); @@ -101,12 +104,15 @@ describe('Discover grid columns ', function () { "showMoveLeft": true, "showMoveRight": true, }, - "cellActions": undefined, + "cellActions": Array [ + [Function], + [Function], + ], "display": "Time (timestamp)", "id": "timestamp", "initialWidth": 180, - "isSortable": false, - "schema": "kibana-json", + "isSortable": true, + "schema": "datetime", }, Object { "actions": Object { @@ -117,11 +123,14 @@ describe('Discover grid columns ', function () { "showMoveLeft": true, "showMoveRight": true, }, - "cellActions": undefined, + "cellActions": Array [ + [Function], + [Function], + ], "display": undefined, "id": "extension", "isSortable": false, - "schema": "kibana-json", + "schema": "string", }, Object { "actions": Object { @@ -136,7 +145,7 @@ describe('Discover grid columns ', function () { "display": undefined, "id": "message", "isSortable": false, - "schema": "kibana-json", + "schema": "string", }, ] `); diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js b/src/plugins/discover/public/application/components/help_menu/help_menu_util.ts similarity index 81% rename from src/plugins/discover/public/application/components/help_menu/help_menu_util.js rename to src/plugins/discover/public/application/components/help_menu/help_menu_util.ts index 1a6815b40b581..d0d5cfde1fe06 100644 --- a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js +++ b/src/plugins/discover/public/application/components/help_menu/help_menu_util.ts @@ -7,10 +7,9 @@ */ import { i18n } from '@kbn/i18n'; -import { getServices } from '../../../kibana_services'; -const { docLinks } = getServices(); +import { ChromeStart, DocLinksStart } from 'kibana/public'; -export function addHelpMenuToAppChrome(chrome) { +export function addHelpMenuToAppChrome(chrome: ChromeStart, docLinks: DocLinksStart) { chrome.setHelpExtension({ appName: i18n.translate('discover.helpMenu.appName', { defaultMessage: 'Discover', diff --git a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts new file mode 100644 index 0000000000000..29c93886ebba3 --- /dev/null +++ b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { SearchSource } from '../../../../../data/public'; +import { dataPluginMock } from '../../../../../data/public/mocks'; +import { applyAggsToSearchSource } from './apply_aggs_to_search_source'; + +describe('applyAggsToSearchSource', () => { + test('enabled = true', () => { + const indexPattern = indexPatternWithTimefieldMock; + const setField = jest.fn(); + const searchSource = ({ + setField, + removeField: jest.fn(), + } as unknown) as SearchSource; + + const dataMock = dataPluginMock.createStartContract(); + + const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock); + + expect(aggsConfig!.aggs).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "id": "1", + "params": Object {}, + "schema": "metric", + "type": "count", + }, + Object { + "enabled": true, + "id": "2", + "params": Object { + "drop_partials": false, + "extended_bounds": Object {}, + "field": "timestamp", + "interval": "auto", + "min_doc_count": 1, + "scaleMetricValues": false, + "useNormalizedEsInterval": true, + }, + "schema": "segment", + "type": "date_histogram", + }, + ] + `); + + expect(setField).toHaveBeenCalledWith('aggs', expect.any(Function)); + const dslFn = setField.mock.calls[0][1]; + expect(dslFn()).toMatchInlineSnapshot(` + Object { + "2": Object { + "date_histogram": Object { + "field": "timestamp", + "min_doc_count": 1, + "time_zone": "America/New_York", + }, + }, + } + `); + }); + + test('enabled = false', () => { + const indexPattern = indexPatternWithTimefieldMock; + const setField = jest.fn(); + const getField = jest.fn(() => { + return true; + }); + const removeField = jest.fn(); + const searchSource = ({ + getField, + setField, + removeField, + } as unknown) as SearchSource; + + const dataMock = dataPluginMock.createStartContract(); + + const aggsConfig = applyAggsToSearchSource(false, searchSource, 'auto', indexPattern, dataMock); + expect(aggsConfig).toBeFalsy(); + expect(getField).toHaveBeenCalledWith('aggs'); + expect(removeField).toHaveBeenCalledWith('aggs'); + }); +}); diff --git a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts new file mode 100644 index 0000000000000..c5fb366f81c8c --- /dev/null +++ b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { IndexPattern, SearchSource } from '../../../../../data/common'; +import { DataPublicPluginStart } from '../../../../../data/public'; + +/** + * Helper function to apply or remove aggregations to a given search source used for gaining data + * for Discover's histogram vis + */ +export function applyAggsToSearchSource( + enabled: boolean, + searchSource: SearchSource, + histogramInterval: string, + indexPattern: IndexPattern, + data: DataPublicPluginStart +) { + if (!enabled) { + if (searchSource.getField('aggs')) { + // clean up fields in case it was set before + searchSource.removeField('aggs'); + } + return; + } + const visStateAggs = [ + { + type: 'count', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + params: { + field: indexPattern.timeFieldName!, + interval: histogramInterval, + timeRange: data.query.timefilter.timefilter.getTime(), + }, + }, + ]; + const chartAggConfigs = data.search.aggs.createAggConfigs(indexPattern, visStateAggs); + + searchSource.setField('aggs', function () { + return chartAggConfigs.toDsl(); + }); + return chartAggConfigs; +} diff --git a/src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts b/src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts new file mode 100644 index 0000000000000..ad7031f331992 --- /dev/null +++ b/src/plugins/discover/public/application/components/histogram/get_dimensions.test.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 { dataPluginMock } from '../../../../../data/public/mocks'; + +import { getDimensions } from './get_dimensions'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { SearchSource } from '../../../../../data/common/search/search_source'; +import { applyAggsToSearchSource } from './apply_aggs_to_search_source'; +import { calculateBounds } from '../../../../../data/common/query/timefilter'; + +test('getDimensions', () => { + const indexPattern = indexPatternWithTimefieldMock; + const setField = jest.fn(); + const searchSource = ({ + setField, + removeField: jest.fn(), + } as unknown) as SearchSource; + + const dataMock = dataPluginMock.createStartContract(); + dataMock.query.timefilter.timefilter.getTime = () => { + return { from: 'now-30y', to: 'now' }; + }; + dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); + }; + + const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock); + const actual = getDimensions(aggsConfig!, dataMock); + expect(actual).toMatchInlineSnapshot(` + Object { + "x": Object { + "accessor": 0, + "format": Object { + "id": "date", + "params": Object { + "pattern": "HH:mm:ss.SSS", + }, + }, + "label": "timestamp per 0 milliseconds", + "params": Object { + "bounds": undefined, + "date": true, + "format": "HH:mm:ss.SSS", + "interval": "P365D", + "intervalESUnit": "d", + "intervalESValue": 365, + }, + }, + "y": Object { + "accessor": 1, + "format": Object { + "id": "number", + }, + "label": "Count", + }, + } + `); +}); diff --git a/src/plugins/discover/public/application/components/histogram/get_dimensions.ts b/src/plugins/discover/public/application/components/histogram/get_dimensions.ts new file mode 100644 index 0000000000000..6743c1c8431b9 --- /dev/null +++ b/src/plugins/discover/public/application/components/histogram/get_dimensions.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import { IAggConfigs, TimeRangeBounds } from '../../../../../data/common'; +import { DataPublicPluginStart, search } from '../../../../../data/public'; + +export function getDimensions(aggs: IAggConfigs, data: DataPublicPluginStart) { + const [metric, agg] = aggs.aggs; + const { from, to } = data.query.timefilter.timefilter.getTime(); + agg.params.timeRange = { + from: dateMath.parse(from), + to: dateMath.parse(to, { roundUp: true }), + }; + const bounds = agg.params.timeRange + ? data.query.timefilter.timefilter.calculateBounds(agg.params.timeRange) + : null; + const buckets = search.aggs.isDateHistogramBucketAggConfig(agg) ? agg.buckets : undefined; + + if (!buckets) { + return; + } + + buckets.setBounds(bounds as TimeRangeBounds); + + const { esUnit, esValue } = buckets.getInterval(); + return { + x: { + accessor: 0, + label: agg.makeLabel(), + format: agg.toSerializedFieldFormat(), + params: { + date: true, + interval: moment.duration(esValue, esUnit), + intervalESValue: esValue, + intervalESUnit: esUnit, + format: buckets.getScaledDateFormat(), + bounds: buckets.getBounds(), + }, + }, + y: { + accessor: 1, + format: metric.toSerializedFieldFormat(), + label: metric.makeLabel(), + }, + }; +} diff --git a/src/plugins/discover/public/application/components/histogram/index.ts b/src/plugins/discover/public/application/components/histogram/index.ts new file mode 100644 index 0000000000000..4af75de0a029d --- /dev/null +++ b/src/plugins/discover/public/application/components/histogram/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { applyAggsToSearchSource } from './apply_aggs_to_search_source'; +export { getDimensions } from './get_dimensions'; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index ff8f14115e492..74836711373b2 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -12,6 +12,7 @@ import { ReactWrapper } from 'enzyme'; import { TimechartHeader, TimechartHeaderProps } from './timechart_header'; import { EuiIconTip } from '@elastic/eui'; import { findTestSubject } from '@elastic/eui/lib/test'; +import { DataPublicPluginStart } from '../../../../../data/public'; describe('timechart header', function () { let props: TimechartHeaderProps; @@ -19,10 +20,18 @@ describe('timechart header', function () { beforeAll(() => { props = { - timeRange: { - from: 'May 14, 2020 @ 11:05:13.590', - to: 'May 14, 2020 @ 11:20:13.590', - }, + data: { + query: { + timefilter: { + timefilter: { + getTime: () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }, + }, + }, + }, + } as DataPublicPluginStart, + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', stateInterval: 's', options: [ { diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx index 0379059b80e58..a2fc17e05a203 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx @@ -15,9 +15,11 @@ import { EuiSelect, EuiIconTip, } from '@elastic/eui'; +import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import dateMath from '@elastic/datemath'; +import { DataPublicPluginStart } from '../../../../../data/public'; import './timechart_header.scss'; -import moment from 'moment'; export interface TimechartHeaderProps { /** @@ -32,13 +34,7 @@ export interface TimechartHeaderProps { description?: string; scale?: number; }; - /** - * Range of dates to be displayed - */ - timeRange?: { - from: string; - to: string; - }; + data: DataPublicPluginStart; /** * Interval Options */ @@ -56,21 +52,27 @@ export interface TimechartHeaderProps { export function TimechartHeader({ bucketInterval, dateFormat, - timeRange, + data, options, onChangeInterval, stateInterval, }: TimechartHeaderProps) { + const { timefilter } = data.query.timefilter; + const { from, to } = timefilter.getTime(); + const timeRange = { + from: dateMath.parse(from), + to: dateMath.parse(to, { roundUp: true }), + }; const [interval, setInterval] = useState(stateInterval); const toMoment = useCallback( - (datetime: string) => { + (datetime: moment.Moment | undefined) => { if (!datetime) { return ''; } if (!dateFormat) { - return datetime; + return String(datetime); } - return moment(datetime).format(dateFormat); + return datetime.format(dateFormat); }, [dateFormat] ); diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index db1cd89422454..23a3cc9a9bc74 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -158,10 +158,6 @@ export interface DiscoverProps { * Current app state of URL */ state: AppState; - /** - * Currently selected time range - */ - timeRange?: { from: string; to: string }; /** * An object containing properties for unmapped fields behavior */ diff --git a/src/plugins/discover/public/application/helpers/get_result_state.test.ts b/src/plugins/discover/public/application/helpers/get_result_state.test.ts new file mode 100644 index 0000000000000..98e2b854ca5ab --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_result_state.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { getResultState, resultStatuses } from './get_result_state'; +import { fetchStatuses } from '../components/constants'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; + +describe('getResultState', () => { + test('fetching uninitialized', () => { + const actual = getResultState(fetchStatuses.UNINITIALIZED, []); + expect(actual).toBe(resultStatuses.UNINITIALIZED); + }); + + test('fetching complete with no records', () => { + const actual = getResultState(fetchStatuses.COMPLETE, []); + expect(actual).toBe(resultStatuses.NO_RESULTS); + }); + + test('fetching ongoing aka loading', () => { + const actual = getResultState(fetchStatuses.LOADING, []); + expect(actual).toBe(resultStatuses.LOADING); + }); + + test('fetching ready', () => { + const record = ({ _id: 123 } as unknown) as ElasticSearchHit; + const actual = getResultState(fetchStatuses.COMPLETE, [record]); + expect(actual).toBe(resultStatuses.READY); + }); + + test('re-fetching after already data is available', () => { + const record = ({ _id: 123 } as unknown) as ElasticSearchHit; + const actual = getResultState(fetchStatuses.LOADING, [record]); + expect(actual).toBe(resultStatuses.READY); + }); + + test('after a fetch error when data was successfully fetched before ', () => { + const record = ({ _id: 123 } as unknown) as ElasticSearchHit; + const actual = getResultState(fetchStatuses.ERROR, [record]); + expect(actual).toBe(resultStatuses.READY); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_result_state.ts b/src/plugins/discover/public/application/helpers/get_result_state.ts new file mode 100644 index 0000000000000..6f69832f369fd --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_result_state.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 { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { fetchStatuses } from '../components/constants'; + +export const resultStatuses = { + UNINITIALIZED: 'uninitialized', + LOADING: 'loading', // initial data load + READY: 'ready', // results came back + NO_RESULTS: 'none', // no results came back +}; + +/** + * Returns the current state of the result, depends on fetchStatus and the given fetched rows + * Determines what is displayed in Discover main view (loading view, data view, empty data view, ...) + */ +export function getResultState(fetchStatus: string, rows: ElasticSearchHit[]) { + if (fetchStatus === fetchStatuses.UNINITIALIZED) { + return resultStatuses.UNINITIALIZED; + } + + const rowsEmpty = !Array.isArray(rows) || rows.length === 0; + if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return resultStatuses.LOADING; + else if (!rowsEmpty) return resultStatuses.READY; + else return resultStatuses.NO_RESULTS; +} diff --git a/src/plugins/discover/public/application/helpers/get_state_defaults.test.ts b/src/plugins/discover/public/application/helpers/get_state_defaults.test.ts new file mode 100644 index 0000000000000..7ce5b9286c775 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_state_defaults.test.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 { getStateDefaults } from './get_state_defaults'; +import { createSearchSourceMock, dataPluginMock } from '../../../../data/public/mocks'; +import { uiSettingsMock } from '../../__mocks__/ui_settings'; +import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; +import { savedSearchMock } from '../../__mocks__/saved_search'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; + +describe('getStateDefaults', () => { + test('index pattern with timefield', () => { + const actual = getStateDefaults({ + config: uiSettingsMock, + data: dataPluginMock.createStartContract(), + indexPattern: indexPatternWithTimefieldMock, + savedSearch: savedSearchMock, + searchSource: createSearchSourceMock({ index: indexPatternWithTimefieldMock }), + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "default_column", + ], + "filters": undefined, + "index": "index-pattern-with-timefield-id", + "interval": "auto", + "query": undefined, + "sort": Array [ + Array [ + "timestamp", + "desc", + ], + ], + } + `); + }); + + test('index pattern without timefield', () => { + const actual = getStateDefaults({ + config: uiSettingsMock, + data: dataPluginMock.createStartContract(), + indexPattern: indexPatternMock, + savedSearch: savedSearchMock, + searchSource: createSearchSourceMock({ index: indexPatternMock }), + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "default_column", + ], + "filters": undefined, + "index": "the-index-pattern-id", + "interval": "auto", + "query": undefined, + "sort": Array [], + } + `); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_state_defaults.ts b/src/plugins/discover/public/application/helpers/get_state_defaults.ts new file mode 100644 index 0000000000000..3e012a1f85fd6 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_state_defaults.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { cloneDeep } from 'lodash'; +import { IUiSettingsClient } from 'kibana/public'; +import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; +import { getSortArray } from '../angular/doc_table'; +import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort'; +import { SavedSearch } from '../../saved_searches'; +import { SearchSource } from '../../../../data/common/search/search_source'; +import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; + +import { AppState } from '../angular/discover_state'; + +function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) { + if (savedSearch.columns && savedSearch.columns.length > 0) { + return [...savedSearch.columns]; + } + return [...config.get(DEFAULT_COLUMNS_SETTING)]; +} + +export function getStateDefaults({ + config, + data, + indexPattern, + savedSearch, + searchSource, +}: { + config: IUiSettingsClient; + data: DataPublicPluginStart; + indexPattern: IndexPattern; + savedSearch: SavedSearch; + searchSource: SearchSource; +}) { + const query = searchSource.getField('query') || data.query.queryString.getDefaultQuery(); + const sort = getSortArray(savedSearch.sort, indexPattern); + const columns = getDefaultColumns(savedSearch, config); + + const defaultState = { + query, + sort: !sort.length + ? getDefaultSort(indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) + : sort, + columns, + index: indexPattern.id, + interval: 'auto', + filters: cloneDeep(searchSource.getOwnField('filter')), + } as AppState; + if (savedSearch.grid) { + defaultState.grid = savedSearch.grid; + } + if (savedSearch.hideChart) { + defaultState.hideChart = savedSearch.hideChart; + } + + return defaultState; +} diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index aeb02e5c30eb8..def175474d40e 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -105,21 +105,27 @@ 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); 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/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'); } From 69bb5979ce23eedf8ff926e12728bbc6819e5ece Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 25 Mar 2021 09:04:40 +0100 Subject: [PATCH 49/88] [Uptime] Support agent data streams (#91469) Co-authored-by: Dominique Clarke Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/constants/settings_defaults.ts | 2 +- .../settings/add_connector_flyout.tsx | 1 + .../uptime/public/lib/helper/rtl_helpers.tsx | 31 ++++++++++--- .../uptime/public/pages/settings.test.tsx | 45 ++++++++++++++++++- .../plugins/uptime/public/pages/settings.tsx | 5 ++- .../uptime/public/pages/translations.ts | 4 ++ .../get_monitor_charts.test.ts.snap | 2 +- .../server/lib/requests/get_certs.test.ts | 2 +- .../requests/get_monitor_availability.test.ts | 10 ++--- .../lib/requests/get_monitor_status.test.ts | 10 ++--- .../lib/requests/get_network_events.test.ts | 2 +- .../server/lib/requests/get_pings.test.ts | 10 ++--- 12 files changed, 98 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/plugins/uptime/common/constants/settings_defaults.ts index b57e3dfc86bea..b57bc8a85d7cd 100644 --- a/x-pack/plugins/uptime/common/constants/settings_defaults.ts +++ b/x-pack/plugins/uptime/common/constants/settings_defaults.ts @@ -8,7 +8,7 @@ import { DynamicSettings } from '../runtime_types'; export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = { - heartbeatIndices: 'heartbeat-8*', + heartbeatIndices: 'heartbeat-8*,synthetics-*', certAgeThreshold: 730, certExpirationThreshold: 30, defaultConnectors: [], diff --git a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx index adddaf6d02829..b2363a1b21f05 100644 --- a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx @@ -68,6 +68,7 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { return ( <> setAddFlyoutVisibility(true)} diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index a2a67fe7347f5..54839e9009896 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -23,6 +23,8 @@ import { import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; import { stringifyUrlParams } from './stringify_url_params'; +import { ClientPluginsStart } from '../../apps/plugin'; +import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks'; interface KibanaProps { services?: KibanaServices; @@ -55,20 +57,39 @@ interface RenderRouterOptions extends KibanaProviderOptions(key: string): T { + return ('MMM D, YYYY @ HH:mm:ss.SSS' as unknown) as T; +} + +function setSetting$(key: string): T { + return (of('MMM D, YYYY @ HH:mm:ss.SSS') as unknown) as T; +} + /* default mock core */ const defaultCore = coreMock.createStart(); -const mockCore: () => any = () => { - const core = { +const mockCore: () => Partial = () => { + const core: Partial = { ...defaultCore, application: { + ...defaultCore.application, getUrlForApp: () => '/app/uptime', navigateToUrl: jest.fn(), + capabilities: { + ...defaultCore.application.capabilities, + uptime: { + 'alerting:save': true, + configureSettings: true, + save: true, + show: true, + }, + }, }, uiSettings: { - get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', - get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), + ...defaultCore.uiSettings, + get: getSetting, + get$: setSetting$, }, - usageCollection: { reportUiCounter: () => {} }, + triggersActionsUi: triggersActionsUiMock.createStart(), }; return core; diff --git a/x-pack/plugins/uptime/public/pages/settings.test.tsx b/x-pack/plugins/uptime/public/pages/settings.test.tsx index b02f304f1a7e7..95fed208f6b0a 100644 --- a/x-pack/plugins/uptime/public/pages/settings.test.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.test.tsx @@ -4,10 +4,53 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React from 'react'; -import { isValidCertVal } from './settings'; +import { isValidCertVal, SettingsPage } from './settings'; +import { render } from '../lib/helper/rtl_helpers'; +import { fireEvent, waitFor } from '@testing-library/dom'; +import { act } from 'react-dom/test-utils'; +import * as alertApi from '../state/api/alerts'; describe('settings', () => { + describe('form', () => { + beforeAll(() => { + jest.spyOn(alertApi, 'fetchActionTypes').mockImplementation(async () => [ + { + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + id: '.slack', + minimumLicenseRequired: 'gold', + name: 'Slack', + }, + ]); + }); + + it('handles no spaces error', async () => { + const { getByText, getByTestId } = render(); + + expect(getByText('heartbeat-8*,synthetics-*')); + + fireEvent.input(getByTestId('heartbeat-indices-input-loaded'), { + target: { value: 'heartbeat-8*, synthetics-*' }, + }); + + await waitFor(() => expect(getByText('Index names must not contain space'))); + }); + + it('it show select a connector flyout', async () => { + const { getByText, getByTestId } = render(); + + expect(getByText('heartbeat-8*,synthetics-*')); + + act(() => { + fireEvent.click(getByTestId('createConnectorButton')); + }); + await waitFor(() => expect(getByText('Select a connector'))); + }); + }); + describe('isValidCertVal', () => { it('handles NaN values', () => { expect(isValidCertVal(NaN)).toMatchInlineSnapshot(`"Must be a number."`); diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index 4b476dd93b3bc..f806ebbd09cc3 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -34,6 +34,7 @@ import { VALUE_MUST_BE_AN_INTEGER, } from '../../common/translations'; import { AlertDefaultsForm } from '../components/settings/alert_defaults_form'; +import { BLANK_STR, SPACE_STR } from './translations'; interface SettingsPageFieldErrors { heartbeatIndices: string | ''; @@ -65,7 +66,9 @@ const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldEr if (formFields) { const { certAgeThreshold, certExpirationThreshold, heartbeatIndices } = formFields; - const indError = heartbeatIndices.match(/^\S+$/) ? '' : Translations.BLANK_STR; + const indErrorSpace = heartbeatIndices.includes(' ') ? SPACE_STR : ''; + + const indError = indErrorSpace || (heartbeatIndices.match(/^\S+$/) ? '' : BLANK_STR); const expError = isValidCertVal(certExpirationThreshold); const ageError = isValidCertVal(certAgeThreshold); diff --git a/x-pack/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts index e6ea04013a22a..95a8dd8ee3696 100644 --- a/x-pack/plugins/uptime/public/pages/translations.ts +++ b/x-pack/plugins/uptime/public/pages/translations.ts @@ -30,3 +30,7 @@ export const settings = { export const BLANK_STR = i18n.translate('xpack.uptime.settings.blank.error', { defaultMessage: 'May not be blank.', }); + +export const SPACE_STR = i18n.translate('xpack.uptime.settings.noSpace.error', { + defaultMessage: 'Index names must not contain space', +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/__snapshots__/get_monitor_charts.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__snapshots__/get_monitor_charts.test.ts.snap index dd47945d9eaba..2d1bdf791e39a 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__snapshots__/get_monitor_charts.test.ts.snap +++ b/x-pack/plugins/uptime/server/lib/requests/__snapshots__/get_monitor_charts.test.ts.snap @@ -55,7 +55,7 @@ Array [ }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.test.ts index 399fca82d8d15..333824df174a6 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.test.ts @@ -212,7 +212,7 @@ describe('getCerts', () => { }, ], }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ], ] diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.test.ts index 29011ecf54871..0f1bd606307e5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.test.ts @@ -241,7 +241,7 @@ describe('monitor availability', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); }); @@ -387,7 +387,7 @@ describe('monitor availability', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); @@ -701,7 +701,7 @@ describe('monitor availability', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); @@ -799,7 +799,7 @@ describe('monitor availability', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `); @@ -929,7 +929,7 @@ describe('monitor availability', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts index d0aaeafbe650d..6d88ccb9a9eff 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts @@ -185,7 +185,7 @@ describe('getMonitorStatus', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); }); @@ -287,7 +287,7 @@ describe('getMonitorStatus', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); }); @@ -474,7 +474,7 @@ describe('getMonitorStatus', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); }); @@ -581,7 +581,7 @@ describe('getMonitorStatus', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); }); @@ -694,7 +694,7 @@ describe('getMonitorStatus', () => { }, "size": 0, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", } `); expect(result.length).toBe(3); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index 9d4e42337fd75..a9c29012141da 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -210,7 +210,7 @@ describe('getNetworkEvents', () => { "size": 1000, "track_total_hits": true, }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ], ] diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.test.ts index b0e7a703d6b5a..03eb10ab3dacc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.test.ts @@ -175,7 +175,7 @@ describe('getAll', () => { }, ], }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `); @@ -244,7 +244,7 @@ describe('getAll', () => { }, ], }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `); @@ -313,7 +313,7 @@ describe('getAll', () => { }, ], }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `); @@ -387,7 +387,7 @@ describe('getAll', () => { }, ], }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `); @@ -461,7 +461,7 @@ describe('getAll', () => { }, ], }, - "index": "heartbeat-8*", + "index": "heartbeat-8*,synthetics-*", }, ] `); From b88f02ffb423579b982ade89b0ba1367c7d3c333 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 25 Mar 2021 09:05:26 +0100 Subject: [PATCH 50/88] [Uptime] unskip overview test (#95290) --- x-pack/test/functional/apps/uptime/overview.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index b9c1767e4a8cf..894604978598e 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -15,8 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/89072 - describe.skip('overview page', function () { + describe('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; @@ -96,7 +95,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await uptime.pageUrlContains('pagination'); await uptime.setMonitorListPageSize(50); // the pagination parameter should be cleared after a size change - await uptime.pageUrlContains('pagination', false); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await retry.try(async () => { + await uptime.pageUrlContains('pagination', false); + }); }); it('pagination size updates to reflect current selection', async () => { From 9724051f928357c7db4220a577845c973eb70ed2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 25 Mar 2021 09:06:58 +0100 Subject: [PATCH 51/88] [Observability] add loading state in use fetcher (#95292) --- x-pack/plugins/observability/public/hooks/use_fetcher.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/observability/public/hooks/use_fetcher.tsx b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx index 2a529f634e7a8..8e30f270bc58c 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx @@ -18,6 +18,7 @@ export interface FetcherResult { data?: Data; status: FETCH_STATUS; error?: Error; + loading?: boolean; } // fetcher functions can return undefined OR a promise. Previously we had a more simple type @@ -38,6 +39,7 @@ export function useFetcher( const [result, setResult] = useState>>({ data: undefined, status: FETCH_STATUS.PENDING, + loading: true, }); const [counter, setCounter] = useState(0); useEffect(() => { @@ -51,6 +53,7 @@ export function useFetcher( data: preservePreviousData ? prevResult.data : undefined, status: FETCH_STATUS.LOADING, error: undefined, + loading: true, })); try { @@ -65,6 +68,7 @@ export function useFetcher( data: preservePreviousData ? prevResult.data : undefined, status: FETCH_STATUS.FAILURE, error: e, + loading: false, })); } } @@ -76,6 +80,7 @@ export function useFetcher( return useMemo(() => { return { ...result, + loading: result.status === FETCH_STATUS.LOADING || result.status === FETCH_STATUS.PENDING, refetch: () => { // this will invalidate the deps to `useEffect` and will result in a new request setCounter((count) => count + 1); From 238791b9421cda571011fd416ae0ffd090b1e94d Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Thu, 25 Mar 2021 09:47:16 +0100 Subject: [PATCH 52/88] ES client : use the new type definitions (#83808) * Use client from branch * Get type checking working in core * Fix types in other plugins * Update client types + remove type errors from core * migrate Task Manager Elasticsearch typing from legacy library to client library * use SortOrder instead o string in alerts * Update client types + fix core type issues * fix maps ts errors * Update Lens types * Convert Search Profiler body from a string to an object to conform to SearchRequest type. * Fix SOT types * Fix/mute Security/Spaces plugins type errors. * Fix bootstrap types * Fix painless_lab * corrected es typing in Event Log * Use new types from client for inferred search responses * Latest type defs * Integrate latest type defs for APM/UX * fix core errors * fix telemetry errors * fix canvas errors * fix data_enhanced errors * fix event_log errors * mute lens errors * fix or mute maps errors * fix reporting errors * fix security errors * mute errors in task_manager * fix errors in telemetry_collection_xpack * fix errors in data plugins * fix errors in alerts * mute errors in index_management * fix task_manager errors * mute or fix lens errors * fix upgrade_assistant errors * fix or mute errors in index_lifecycle_management * fix discover errors * fix core tests * ML changes * fix core type errors * mute error in kbn-es-archiver * fix error in data plugin * fix error in telemetry plugin * fix error in discover * fix discover errors * fix errors in task_manager * fix security errors * fix wrong conflict resolution * address errors with upstream code * update deps to the last commit * remove outdated comments * fix core errors * fix errors after update * adding more expect errors to ML * pull the lastest changes * fix core errors * fix errors in infra plugin * fix errors in uptime plugin * fix errors in ml * fix errors in xpack telemetry * fix or mute errors in transform * fix errors in upgrade assistant * fix or mute fleet errors * start fixing apm errors * fix errors in osquery * fix telemetry tests * core cleanup * fix asMutableArray imports * cleanup * data_enhanced cleanup * cleanup events_log * cleaup * fix error in kbn-es-archiver * fix errors in kbn-es-archiver * fix errors in kbn-es-archiver * fix ES typings for Hit * fix SO * fix actions plugin * fix fleet * fix maps * fix stack_alerts * fix eslint problems * fix event_log unit tests * fix failures in data_enhanced tests * fix test failure in kbn-es-archiver * fix test failures in index_pattern_management * fixing ML test * remove outdated comment in kbn-es-archiver * fix error type in ml * fix eslint errors in osquery plugin * fix runtime error in infra plugin * revert changes to event_log cluser exist check * fix eslint error in osquery * fixing ML endpoint argument types * fx types * Update api-extractor docs * attempt fix for ese test * Fix lint error * Fix types for ts refs * Fix data_enhanced unit test * fix lens types * generate docs * Fix a number of type issues in monitoring and ml * fix triggers_actions_ui * Fix ILM functional test * Put search.d.ts typings back * fix data plugin * Update typings in typings/elasticsearch * Update snapshots * mute errors in task_manager * mute fleet errors * lens. remove unnecessary ts-expect-errors * fix errors in stack_alerts * mute errors in osquery * fix errors in security_solution * fix errors in lists * fix errors in cases * mute errors in search_examples * use KibanaClient to enforce promise-based API * fix errors in test/ folder * update comment * fix errors in x-pack/test folder * fix errors in ml plugin * fix optional fields in ml api_integartoon tests * fix another casting problem in ml tests * fix another ml test failure * fix fleet problem after conflict resolution * rollback changes in security_solution. trying to fix test * Update type for discover rows * uncomment runtime_mappings as its outdated * address comments from Wylie * remove eslint error due to any * mute error due to incompatibility * Apply suggestions from code review Co-authored-by: John Schulz * fix type error in lens tests * Update x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts Co-authored-by: Alison Goryachev * Update x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts Co-authored-by: Alison Goryachev * update deps * fix errors in core types * fix errors for the new elastic/elasticsearch version * remove unused type * remove unnecessary manual type cast and put optional chaining back * ML: mute Datafeed is missing indices_options * Apply suggestions from code review Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> * use canary pacakge instead of git commit Co-authored-by: Josh Dover Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> Co-authored-by: Gidi Meir Morris Co-authored-by: Nathan Reese Co-authored-by: Wylie Conlon Co-authored-by: CJ Cenizal Co-authored-by: Aleh Zasypkin Co-authored-by: Dario Gieselaar Co-authored-by: restrry Co-authored-by: James Gowdy Co-authored-by: John Schulz Co-authored-by: Alison Goryachev --- ...gin-core-public.savedobjectsfindoptions.md | 4 +- ...lic.savedobjectsfindoptions.searchafter.md | 2 +- ...ublic.savedobjectsfindoptions.sortorder.md | 2 +- ...gin-core-server.savedobjectsfindoptions.md | 4 +- ...ver.savedobjectsfindoptions.searchafter.md | 2 +- ...erver.savedobjectsfindoptions.sortorder.md | 2 +- ...ugin-core-server.savedobjectsfindresult.md | 2 +- ...core-server.savedobjectsfindresult.sort.md | 2 +- ...n-plugins-data-public.iessearchresponse.md | 2 +- ...-plugins-data-public.searchsource.fetch.md | 4 +- ...n-plugins-data-server.iessearchresponse.md | 2 +- .../search_examples/public/search/app.tsx | 3 +- .../public/search_sessions/app.tsx | 4 +- package.json | 2 +- .../src/actions/empty_kibana_index.ts | 4 +- packages/kbn-es-archiver/src/actions/load.ts | 4 +- packages/kbn-es-archiver/src/actions/save.ts | 4 +- .../kbn-es-archiver/src/actions/unload.ts | 4 +- .../kbn-es-archiver/src/client_headers.ts | 2 +- packages/kbn-es-archiver/src/es_archiver.ts | 6 +- .../lib/docs/generate_doc_records_stream.ts | 4 +- .../src/lib/docs/index_doc_records_stream.ts | 4 +- .../src/lib/indices/__mocks__/stubs.ts | 4 +- .../lib/indices/create_index_stream.test.ts | 1 - .../src/lib/indices/create_index_stream.ts | 11 +- .../src/lib/indices/delete_index.ts | 6 +- .../src/lib/indices/delete_index_stream.ts | 4 +- .../indices/generate_index_records_stream.ts | 4 +- .../src/lib/indices/kibana_index.ts | 31 +- src/core/public/public.api.md | 5 +- .../core_usage_data_service.test.ts | 16 +- .../core_usage_data_service.ts | 12 +- src/core/server/elasticsearch/client/mocks.ts | 6 +- .../client/retry_call_cluster.test.ts | 6 +- .../export/saved_objects_exporter.test.ts | 7 +- .../import/lib/check_origin_conflicts.ts | 2 +- .../server/saved_objects/mappings/types.ts | 2 +- .../core/build_active_mappings.test.ts | 1 + .../migrations/core/call_cluster.ts | 14 +- .../migrations/core/elastic_index.test.ts | 72 +-- .../migrations/core/elastic_index.ts | 15 +- .../migrations/core/index_migrator.test.ts | 16 +- .../migrations/core/index_migrator.ts | 12 +- .../migrations/kibana/kibana_migrator.test.ts | 26 +- .../migrationsv2/actions/index.ts | 93 ++- .../integration_tests/actions.test.ts | 6 +- .../integration_tests/migration.test.ts | 8 +- .../saved_objects/migrationsv2/model.ts | 1 - .../service/lib/included_fields.ts | 5 +- .../service/lib/point_in_time_finder.ts | 14 +- .../service/lib/repository.test.js | 27 +- .../saved_objects/service/lib/repository.ts | 169 +++-- .../service/lib/search_dsl/search_dsl.test.ts | 10 +- .../service/lib/search_dsl/search_dsl.ts | 7 +- .../service/lib/search_dsl/sorting_params.ts | 5 +- .../service/saved_objects_client.ts | 2 +- src/core/server/saved_objects/types.ts | 5 +- .../version/encode_hit_version.ts | 2 +- .../saved_objects/version/encode_version.ts | 2 +- src/core/server/server.api.md | 7 +- .../data/common/search/es_search/types.ts | 9 +- .../utils/courier_inspector_stats.ts | 4 +- .../search_source/fetch/request_error.ts | 6 +- .../search/search_source/fetch/types.ts | 7 +- .../search_source/legacy/call_client.ts | 4 +- .../search/search_source/legacy/fetch_soon.ts | 8 +- .../search/search_source/legacy/types.ts | 7 +- src/plugins/data/public/public.api.md | 9 +- .../search/expressions/es_raw_response.ts | 8 +- .../public/search/fetch/handle_response.tsx | 4 +- .../shard_failure_modal.tsx | 4 +- .../shard_failure_open_modal_button.tsx | 4 +- .../autocomplete/value_suggestions_route.ts | 7 +- .../index_patterns/fetcher/lib/es_api.ts | 13 +- .../field_capabilities/field_caps_response.ts | 18 +- .../fetcher/lib/field_capabilities/index.ts | 1 - .../fetcher/lib/resolve_time_pattern.ts | 4 +- .../data/server/search/collectors/fetch.ts | 6 +- .../search/es_search/es_search_strategy.ts | 3 +- .../server/search/es_search/response_utils.ts | 8 +- .../data/server/search/routes/call_msearch.ts | 6 +- src/plugins/data/server/server.api.md | 42 +- .../doc_table/create_doc_table_react.tsx | 3 +- .../context_app/context_app_legacy.tsx | 1 + .../discover_grid/discover_grid_flyout.tsx | 2 +- .../get_render_cell_value.test.tsx | 21 +- .../components/table/table.test.tsx | 4 +- .../application/doc_views/doc_views_types.ts | 4 +- .../embeddable/search_embeddable.ts | 2 +- src/plugins/embeddable/public/public.api.md | 1 + .../routes/preview_scripted_field.test.ts | 20 +- .../server/routes/preview_scripted_field.ts | 6 +- .../kibana/get_saved_object_counts.ts | 1 + .../server/check_cluster_data.test.ts | 4 + .../security_oss/server/check_cluster_data.ts | 8 +- .../telemetry_collection/get_cluster_info.ts | 18 +- .../get_data_telemetry/get_data_telemetry.ts | 8 +- .../get_local_stats.test.ts | 49 +- .../telemetry_collection/get_local_stats.ts | 5 +- .../telemetry_collection/get_nodes_usage.ts | 5 +- .../vis_type_vega/public/data_model/types.ts | 6 +- .../usage_collector/get_usage_collector.ts | 6 +- .../usage_collector/get_usage_collector.ts | 2 +- test/api_integration/apis/home/sample_data.ts | 8 +- .../apis/saved_objects/migrations.ts | 8 +- .../telemetry/telemetry_optin_notice_seen.ts | 8 +- .../apis/ui_metric/ui_metric.ts | 4 +- test/common/services/elasticsearch.ts | 6 +- typings/elasticsearch/aggregations.d.ts | 466 -------------- typings/elasticsearch/index.d.ts | 128 +--- typings/elasticsearch/search.d.ts | 577 ++++++++++++++++++ .../actions/server/actions_client.test.ts | 6 + .../plugins/actions/server/actions_client.ts | 4 +- .../server/builtin_action_types/es_index.ts | 4 +- .../server/usage/actions_telemetry.test.ts | 2 + .../actions/server/usage/actions_telemetry.ts | 22 +- .../server/alerts_client/alerts_client.ts | 3 +- .../server/usage/alerts_telemetry.test.ts | 1 + .../alerting/server/usage/alerts_telemetry.ts | 58 +- .../VisitorBreakdownMap/EmbeddedMap.test.tsx | 2 +- .../create-functional-tests-archive/index.ts | 3 +- .../scripts/upload-telemetry-data/index.ts | 10 +- .../server/lib/alerts/alerting_es_client.ts | 4 +- .../chart_preview/get_transaction_duration.ts | 6 +- .../register_error_count_alert_type.test.ts | 30 + ..._transaction_error_rate_alert_type.test.ts | 40 ++ .../collect_data_telemetry/index.ts | 4 +- .../collect_data_telemetry/tasks.ts | 15 +- .../apm/server/lib/apm_telemetry/index.ts | 2 +- .../index.ts | 2 +- .../get_latency_distribution.ts | 2 +- .../index.ts | 6 +- .../process_significant_term_aggs.ts | 2 +- .../lib/errors/get_error_group_sample.ts | 5 +- .../apm/server/lib/errors/get_error_groups.ts | 3 +- .../create_apm_event_client/index.ts | 4 +- .../create_internal_es_client/index.ts | 12 +- .../server/lib/helpers/setup_request.test.ts | 12 +- .../lib/helpers/transaction_error_rate.ts | 2 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 8 +- .../rum_client/get_page_load_distribution.ts | 16 +- .../lib/rum_client/get_pl_dist_breakdown.ts | 6 +- .../lib/rum_client/get_web_core_vitals.ts | 2 +- .../lib/service_map/get_service_anomalies.ts | 7 +- .../lib/service_map/get_trace_sample_ids.ts | 5 +- .../__snapshots__/queries.test.ts.snap | 16 +- .../annotations/get_stored_annotations.ts | 8 +- .../get_destination_map.ts | 17 +- .../get_service_error_groups/index.ts | 4 +- .../services/get_service_metadata_details.ts | 3 +- .../get_service_transaction_stats.ts | 4 +- .../get_services_from_metric_documents.ts | 2 +- .../convert_settings_to_string.ts | 4 +- .../create_agent_config_index.ts | 4 +- .../find_exact_configuration.ts | 6 +- .../search_configurations.ts | 6 +- .../custom_link/create_custom_link_index.ts | 8 +- .../settings/custom_link/get_transaction.ts | 19 +- .../settings/custom_link/list_custom_links.ts | 5 +- .../apm/server/lib/traces/get_trace_items.ts | 3 +- .../__snapshots__/queries.test.ts.snap | 16 +- .../server/lib/transaction_groups/fetcher.ts | 5 +- .../get_transaction_group_stats.ts | 16 +- .../distribution/get_buckets/index.ts | 8 +- .../transactions/get_anomaly_data/fetcher.ts | 8 +- .../lib/transactions/get_transaction/index.ts | 5 +- .../plugins/apm/server/projections/metrics.ts | 3 +- .../projections/rum_page_load_transactions.ts | 8 +- .../apm/server/projections/transactions.ts | 3 +- .../plugins/apm/server/projections/typings.ts | 15 +- .../util/merge_projection/index.test.ts | 6 +- .../util/merge_projection/index.ts | 10 +- .../collectors/custom_element_collector.ts | 7 +- .../server/collectors/workpad_collector.ts | 6 +- .../cases/server/services/alerts/index.ts | 3 +- .../data_enhanced/server/collectors/fetch.ts | 3 +- .../data_enhanced/server/routes/session.ts | 2 +- .../server/search/eql_search_strategy.ts | 11 +- .../server/search/es_search_strategy.test.ts | 2 +- .../server/search/es_search_strategy.ts | 13 +- .../search/session/get_search_status.ts | 1 + .../data_enhanced/server/search/types.ts | 3 +- .../server/es/cluster_client_adapter.test.ts | 92 ++- .../server/es/cluster_client_adapter.ts | 129 ++-- .../file_upload/server/analyze_file.tsx | 13 +- .../server/routes/data_streams/handlers.ts | 1 + .../fleet/server/services/agents/actions.ts | 2 +- .../fleet/server/services/agents/crud.ts | 28 +- .../fleet/server/services/agents/helpers.ts | 15 +- .../server/services/agents/reassign.test.ts | 4 +- .../server/services/agents/status.test.ts | 8 +- .../server/services/agents/unenroll.test.ts | 6 +- .../services/api_keys/enrollment_api_key.ts | 9 +- .../server/services/artifacts/artifacts.ts | 10 +- .../server/services/artifacts/mappings.ts | 4 +- .../fleet/server/services/artifacts/mocks.ts | 4 +- .../epm/elasticsearch/template/template.ts | 1 + .../epm/elasticsearch/transform/install.ts | 1 + .../epm/elasticsearch/transform/remove.ts | 1 + .../fleet_server/saved_object_migrations.ts | 1 + .../api/index/register_add_policy_route.ts | 1 + .../api/policies/register_delete_route.ts | 1 + .../api/policies/register_fetch_route.ts | 1 + .../templates/register_add_policy_route.ts | 10 +- .../api/data_streams/register_get_route.ts | 4 +- .../public/utils/logs_overview_fetchers.ts | 6 +- .../framework/kibana_framework_adapter.ts | 4 +- .../evaluate_condition.ts | 1 + .../metric_threshold/lib/evaluate_alert.ts | 8 +- .../log_entries_search_strategy.ts | 1 + .../log_entries/log_entry_search_strategy.ts | 1 + .../log_entries/queries/log_entries.ts | 5 +- .../services/log_entries/queries/log_entry.ts | 5 +- .../server/routes/existing_fields.test.ts | 10 +- .../lens/server/routes/existing_fields.ts | 18 +- .../plugins/lens/server/routes/field_stats.ts | 17 +- x-pack/plugins/lens/server/usage/task.ts | 3 + .../lens/server/usage/visualization_counts.ts | 1 + .../services/items/create_list_item.test.ts | 2 + .../server/services/items/create_list_item.ts | 3 +- .../services/items/find_list_item.test.ts | 2 + .../server/services/items/find_list_item.ts | 7 +- .../server/services/items/get_list_item.ts | 4 +- .../services/items/update_list_item.test.ts | 1 + .../server/services/items/update_list_item.ts | 3 +- .../items/write_list_items_to_stream.test.ts | 2 + .../items/write_list_items_to_stream.ts | 12 +- .../server/services/lists/create_list.test.ts | 3 + .../server/services/lists/create_list.ts | 3 +- .../lists/server/services/lists/find_list.ts | 5 +- .../lists/server/services/lists/get_list.ts | 3 +- .../server/services/lists/update_list.test.ts | 2 + .../server/services/lists/update_list.ts | 3 +- .../services/utils/get_search_after_scroll.ts | 4 +- .../get_search_after_with_tie_breaker.ts | 15 +- .../utils/get_sort_with_tie_breaker.ts | 14 +- ...sform_elastic_named_search_to_list_item.ts | 4 +- .../utils/transform_elastic_to_list.ts | 5 +- .../utils/transform_elastic_to_list_item.ts | 35 +- .../classes/sources/es_source/es_source.ts | 22 +- x-pack/plugins/maps/server/mvt/get_tile.ts | 2 + .../types/anomaly_detection_jobs/datafeed.ts | 69 ++- .../types/anomaly_detection_jobs/job.ts | 141 +++-- x-pack/plugins/ml/common/types/fields.ts | 3 +- x-pack/plugins/ml/common/util/job_utils.ts | 4 +- .../plugins/ml/common/util/parse_interval.ts | 5 +- .../components/data_grid/common.ts | 1 + .../chart_loader.ts | 1 + .../job_creator/advanced_job_creator.ts | 10 +- .../new_job/common/job_creator/job_creator.ts | 16 +- .../job_creator/util/default_configs.ts | 2 + .../util/filter_runtime_mappings.test.ts | 3 + .../categorization_examples_loader.ts | 1 + .../common/results_loader/results_loader.ts | 1 + .../advanced_detector_modal.tsx | 6 +- .../estimate_bucket_span.ts | 1 + .../metric_selection_summary.tsx | 1 + .../multi_metric_view/metric_selection.tsx | 2 + .../metric_selection_summary.tsx | 2 + .../population_view/metric_selection.tsx | 2 + .../metric_selection_summary.tsx | 2 + .../single_metric_view/metric_selection.tsx | 1 + .../metric_selection_summary.tsx | 1 + .../components/time_range_step/time_range.tsx | 1 + .../public/application/util/string_utils.ts | 2 +- .../ml/server/lib/alerts/alerting_service.ts | 4 +- .../ml/server/lib/ml_client/ml_client.ts | 32 +- .../plugins/ml/server/lib/ml_client/search.ts | 9 +- x-pack/plugins/ml/server/lib/node_utils.ts | 2 +- x-pack/plugins/ml/server/lib/query_utils.ts | 6 +- .../models/annotation_service/annotation.ts | 2 + .../calculate_model_memory_limit.ts | 11 +- .../models/calendar/calendar_manager.ts | 18 +- .../server/models/calendar/event_manager.ts | 13 +- .../ml/server/models/calendar/index.ts | 1 - .../analytics_audit_messages.ts | 6 +- .../models/data_frame_analytics/validation.ts | 5 +- .../models/data_recognizer/data_recognizer.ts | 5 +- .../models/data_visualizer/data_visualizer.ts | 3 + .../models/fields_service/fields_service.ts | 8 + .../ml/server/models/filter/filter_manager.ts | 56 +- .../ml/server/models/job_service/datafeeds.ts | 26 +- .../ml/server/models/job_service/jobs.ts | 26 +- .../models/job_service/model_snapshots.ts | 11 +- .../new_job/categorization/examples.ts | 28 +- .../new_job/categorization/top_categories.ts | 2 + .../job_service/new_job_caps/field_service.ts | 11 +- .../models/job_service/new_job_caps/rollup.ts | 5 +- .../models/job_validation/job_validation.ts | 1 + .../validate_model_memory_limit.test.ts | 51 +- .../get_partition_fields_values.ts | 3 +- .../models/results_service/results_service.ts | 5 + .../ml/server/routes/anomaly_detectors.ts | 28 +- .../ml/server/routes/data_frame_analytics.ts | 9 +- x-pack/plugins/ml/server/routes/datafeeds.ts | 18 +- .../ml/server/routes/trained_models.ts | 5 +- .../plugins/ml/server/saved_objects/checks.ts | 16 +- .../shared_services/providers/system.ts | 11 +- .../lib/alerts/fetch_available_ccs.test.ts | 5 +- .../lib/alerts/fetch_ccr_read_exceptions.ts | 5 +- .../lib/alerts/fetch_cluster_health.test.ts | 3 +- .../server/lib/alerts/fetch_cluster_health.ts | 12 +- .../server/lib/alerts/fetch_clusters.test.ts | 5 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 8 +- .../lib/alerts/fetch_cpu_usage_node_stats.ts | 4 +- .../fetch_disk_usage_node_stats.test.ts | 1 + .../lib/alerts/fetch_disk_usage_node_stats.ts | 3 +- .../fetch_elasticsearch_versions.test.ts | 3 +- .../alerts/fetch_elasticsearch_versions.ts | 14 +- .../lib/alerts/fetch_index_shard_size.ts | 5 +- .../lib/alerts/fetch_kibana_versions.test.ts | 1 + .../lib/alerts/fetch_kibana_versions.ts | 2 +- .../server/lib/alerts/fetch_licenses.test.ts | 3 +- .../server/lib/alerts/fetch_licenses.ts | 12 +- .../alerts/fetch_logstash_versions.test.ts | 1 + .../lib/alerts/fetch_logstash_versions.ts | 2 +- .../alerts/fetch_memory_usage_node_stats.ts | 1 + .../fetch_missing_monitoring_data.test.ts | 2 + .../alerts/fetch_missing_monitoring_data.ts | 4 +- .../alerts/fetch_nodes_from_cluster_stats.ts | 9 +- .../fetch_thread_pool_rejections_stats.ts | 9 +- x-pack/plugins/observability/server/index.ts | 4 +- .../annotations/create_annotations_client.ts | 29 +- .../server/lib/annotations/mappings.ts | 4 +- .../server/utils/create_or_update_index.ts | 31 +- .../search_strategy/osquery/actions/index.ts | 6 +- .../search_strategy/osquery/results/index.ts | 4 +- .../action_results/action_results_table.tsx | 9 +- .../osquery/public/actions/actions_table.tsx | 1 + .../osquery/public/results/results_table.tsx | 2 + .../osquery/factory/actions/all/index.ts | 1 + .../osquery/factory/actions/results/index.ts | 1 + .../osquery/factory/agents/index.ts | 6 +- .../osquery/factory/results/index.ts | 1 + .../painless_lab/server/routes/api/execute.ts | 1 + .../generate_csv/generate_csv.ts | 2 +- .../server/usage/get_reporting_usage.ts | 9 +- .../server/usage/fetch_tag_usage_data.ts | 32 +- .../searchprofiler/server/routes/profile.ts | 13 +- .../common/model/authenticated_user.mock.ts | 6 +- .../authentication/api_keys/api_keys.test.ts | 27 +- .../authentication/api_keys/api_keys.ts | 37 +- .../server/authentication/providers/base.ts | 3 +- .../authentication/providers/kerberos.test.ts | 4 +- .../authentication/providers/kerberos.ts | 8 +- .../authentication/providers/oidc.test.ts | 3 +- .../authentication/providers/saml.test.ts | 3 +- .../authentication/providers/token.test.ts | 1 + .../server/authentication/providers/token.ts | 17 +- .../server/authentication/tokens.test.ts | 39 +- .../security/server/authentication/tokens.ts | 18 +- .../authorization/check_privileges.test.ts | 18 +- .../server/authorization/check_privileges.ts | 32 +- .../authorization/privileges/get_builtin.ts | 3 +- .../server/routes/authorization/roles/get.ts | 8 +- .../routes/authorization/roles/get_all.ts | 7 +- .../roles/model/elasticsearch_role.ts | 2 +- .../server/routes/authorization/roles/put.ts | 10 +- .../server/routes/indices/get_fields.ts | 36 +- .../server/routes/role_mapping/get.ts | 9 +- .../security/server/routes/users/get.ts | 4 +- .../session_management/session_index.test.ts | 70 ++- .../session_management/session_index.ts | 17 +- .../common/search_strategy/common/index.ts | 7 +- .../security_solution/index.ts | 12 +- .../public/common/containers/source/index.tsx | 2 +- .../server/endpoint/routes/resolver/entity.ts | 8 +- .../routes/resolver/queries/events.ts | 11 +- .../resolver/tree/queries/descendants.ts | 6 +- .../routes/resolver/tree/queries/lifecycle.ts | 5 +- .../routes/resolver/tree/queries/stats.ts | 5 +- .../index/delete_all_index.ts | 1 + .../index/get_index_exists.test.ts | 2 + .../index/get_index_exists.ts | 4 +- .../migrations/create_migration.ts | 2 +- .../get_index_versions_by_index.test.ts | 2 +- .../get_signal_versions_by_index.test.ts | 2 +- .../get_signal_versions_by_index.ts | 5 +- .../get_signals_indices_in_range.ts | 7 +- .../notifications/get_signals.ts | 4 +- .../rules_notification_alert_type.ts | 1 + .../routes/index/get_index_version.ts | 13 +- .../signals/__mocks__/es_results.ts | 4 +- .../signals/build_bulk_body.test.ts | 12 + .../signals/build_bulk_body.ts | 4 +- .../signals/build_event_type_signal.test.ts | 3 + .../signals/build_event_type_signal.ts | 7 +- .../signals/build_events_query.ts | 12 +- .../signals/build_rule.test.ts | 6 + .../detection_engine/signals/build_rule.ts | 3 + .../signals/build_signal.test.ts | 8 + .../detection_engine/signals/build_signal.ts | 16 +- .../signals/bulk_create_ml_signals.ts | 13 +- .../signals/filters/filter_events.ts | 5 +- .../filters/filter_events_against_list.ts | 11 +- .../detection_engine/signals/filters/types.ts | 11 +- .../build_risk_score_from_mapping.test.ts | 1 + .../build_rule_name_from_mapping.test.ts | 1 + .../build_severity_from_mapping.test.ts | 1 + .../signals/search_after_bulk_create.test.ts | 17 + .../signals/search_after_bulk_create.ts | 4 + .../signals/send_telemetry_events.ts | 3 +- .../signals/signal_rule_alert_type.test.ts | 6 +- .../signals/single_bulk_create.test.ts | 3 + .../signals/single_bulk_create.ts | 12 +- .../signals/single_search_after.test.ts | 5 +- .../signals/single_search_after.ts | 12 +- .../build_threat_mapping_filter.mock.ts | 5 +- .../threat_mapping/create_threat_signals.ts | 1 + .../enrich_signal_threat_matches.ts | 2 +- .../signals/threat_mapping/get_threat_list.ts | 17 +- .../signals/threat_mapping/types.ts | 6 +- .../bulk_create_threshold_signals.ts | 8 +- .../threshold/find_threshold_signals.ts | 5 +- .../threshold/get_threshold_bucket_filters.ts | 2 +- .../lib/detection_engine/signals/types.ts | 10 +- .../detection_engine/signals/utils.test.ts | 14 +- .../lib/detection_engine/signals/utils.ts | 16 +- .../server/lib/machine_learning/index.ts | 11 +- .../factory/hosts/all/query.all_hosts.dsl.ts | 2 +- .../hosts/authentications/dsl/query.dsl.ts | 16 +- .../query.hosts_kpi_authentications.dsl.ts | 4 +- .../kpi/hosts/query.hosts_kpi_hosts.dsl.ts | 2 +- .../query.hosts_kpi_unique_ips.dsl.ts | 4 +- .../factory/hosts/overview/index.ts | 1 + .../hosts/overview/query.overview_host.dsl.ts | 3 +- .../hosts/uncommon_processes/dsl/query.dsl.ts | 20 +- .../factory/matrix_histogram/index.ts | 1 + .../network/details/__mocks__/index.ts | 20 +- .../details/query.details_network.dsl.ts | 6 +- .../factory/network/dns/index.ts | 1 + .../factory/network/http/index.ts | 1 + .../factory/network/kpi/dns/index.ts | 1 + .../network/kpi/network_events/index.ts | 1 + .../network/kpi/tls_handshakes/index.ts | 1 + .../network/kpi/unique_private_ips/index.ts | 1 + .../factory/network/overview/index.ts | 1 + .../timeline/factory/events/all/index.ts | 1 + .../timeline/factory/events/details/index.ts | 1 + .../usage/detections/detections_helpers.ts | 10 +- .../spaces_usage_collector.ts | 1 + .../common/build_sorted_events_query.ts | 15 +- .../alert_types/es_query/expression.tsx | 8 +- .../alert_types/es_query/action_context.ts | 4 +- .../alert_types/es_query/alert_type.test.ts | 8 + .../server/alert_types/es_query/alert_type.ts | 10 +- .../geo_containment/es_query_builder.ts | 7 +- .../geo_containment/geo_containment.ts | 8 +- .../monitoring/workload_statistics.test.ts | 167 ++--- .../server/monitoring/workload_statistics.ts | 102 +++- .../mark_available_tasks_as_claimed.test.ts | 39 +- .../mark_available_tasks_as_claimed.ts | 37 +- .../server/queries/query_clauses.test.ts | 21 +- .../server/queries/query_clauses.ts | 214 +------ .../server/queries/task_claiming.test.ts | 8 +- .../server/queries/task_claiming.ts | 11 +- .../task_manager/server/task_store.test.ts | 23 +- .../plugins/task_manager/server/task_store.ts | 44 +- .../telemetry_collection/get_license.ts | 21 +- .../get_stats_with_xpack.test.ts | 5 +- .../server/routes/api/field_histograms.ts | 1 + .../transform/server/routes/api/privileges.ts | 5 +- .../transform/server/routes/api/transforms.ts | 10 +- .../routes/api/transforms_audit_messages.ts | 21 +- .../server/data/lib/time_series_query.ts | 9 +- .../server/data/routes/indices.ts | 13 +- .../server/lib/es_indices_state_check.ts | 4 +- .../server/lib/es_migration_apis.test.ts | 4 + .../server/lib/es_migration_apis.ts | 4 +- .../lib/reindexing/reindex_actions.test.ts | 1 + .../lib/reindexing/reindex_service.test.ts | 25 +- .../server/lib/reindexing/reindex_service.ts | 19 +- .../uptime/common/utils/as_mutable_array.ts | 41 ++ x-pack/plugins/uptime/server/lib/lib.ts | 6 +- .../uptime/server/lib/requests/get_certs.ts | 6 +- .../lib/requests/get_journey_details.ts | 9 +- .../lib/requests/get_journey_failed_steps.ts | 12 +- .../lib/requests/get_journey_screenshot.ts | 3 +- .../server/lib/requests/get_journey_steps.ts | 20 +- .../lib/requests/get_last_successful_step.ts | 3 +- .../server/lib/requests/get_latest_monitor.ts | 7 +- .../lib/requests/get_monitor_availability.ts | 10 +- .../lib/requests/get_monitor_details.ts | 5 +- .../lib/requests/get_monitor_duration.ts | 3 +- .../lib/requests/get_monitor_locations.ts | 5 +- .../server/lib/requests/get_monitor_states.ts | 2 +- .../server/lib/requests/get_monitor_status.ts | 8 +- .../server/lib/requests/get_network_events.ts | 3 +- .../server/lib/requests/get_ping_histogram.ts | 4 +- .../uptime/server/lib/requests/get_pings.ts | 5 +- .../uptime/server/lib/requests/helper.ts | 3 +- .../requests/search/find_potential_matches.ts | 9 +- .../apps/index_lifecycle_management.ts | 1 + x-pack/test/accessibility/apps/ml.ts | 5 +- .../api_integration/apis/es/has_privileges.ts | 1 + .../api_integration/apis/lens/telemetry.ts | 5 +- .../apis/ml/annotations/create_annotations.ts | 1 + .../apis/ml/annotations/delete_annotations.ts | 1 + .../apis/ml/annotations/get_annotations.ts | 1 + .../apis/ml/annotations/update_annotations.ts | 9 +- .../apis/ml/anomaly_detectors/get.ts | 1 + .../apis/ml/calendars/create_calendars.ts | 6 +- .../apis/ml/calendars/delete_calendars.ts | 1 + .../apis/ml/calendars/get_calendars.ts | 6 + .../apis/ml/calendars/helpers.ts | 9 +- .../apis/ml/calendars/update_calendars.ts | 2 + .../apis/ml/jobs/common_jobs.ts | 3 + .../ml/results/get_anomalies_table_data.ts | 2 + .../apis/ml/results/get_categorizer_stats.ts | 2 + .../apis/ml/results/get_stopped_partitions.ts | 2 + .../api_integration/apis/security/roles.ts | 1 + .../apis/telemetry/telemetry_local.ts | 2 +- .../telemetry/telemetry_optin_notice_seen.ts | 8 +- .../basic/tests/cases/patch_cases.ts | 26 +- .../tests/cases/sub_cases/patch_sub_cases.ts | 36 +- .../case_api_integration/common/lib/utils.ts | 52 +- .../tests/create_signals_migrations.ts | 1 + .../detection_engine_api_integration/utils.ts | 13 +- .../fleet_api_integration/apis/agents/acks.ts | 5 +- .../apis/agents/checkin.ts | 1 + .../apis/agents/unenroll.ts | 4 +- .../fleet_api_integration/apis/fleet_setup.ts | 1 + .../aggregated_scripted_job.ts | 4 + .../apps/ml/anomaly_detection/annotations.ts | 2 + .../ml/anomaly_detection/anomaly_explorer.ts | 2 + .../anomaly_detection/single_metric_viewer.ts | 4 + .../apps/ml/permissions/full_ml_access.ts | 5 +- .../apps/ml/permissions/read_ml_access.ts | 5 +- .../apps/ml/settings/calendar_creation.ts | 1 + .../apps/ml/settings/calendar_edit.ts | 2 + x-pack/test/functional/services/ml/api.ts | 85 ++- .../functional/services/ml/common_config.ts | 3 + .../apps/ml/alert_flyout.ts | 2 + x-pack/test/lists_api_integration/utils.ts | 5 +- .../trial/tests/annotations.ts | 11 +- .../common/lib/create_users_and_roles.ts | 6 +- .../tests/session_idle/cleanup.ts | 6 +- .../tests/session_lifespan/cleanup.ts | 6 +- .../apis/package.ts | 21 +- .../services/resolver.ts | 13 +- yarn.lock | 29 +- 541 files changed, 3666 insertions(+), 3051 deletions(-) delete mode 100644 typings/elasticsearch/aggregations.d.ts create mode 100644 typings/elasticsearch/search.d.ts create mode 100644 x-pack/plugins/uptime/common/utils/as_mutable_array.ts 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.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/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.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/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/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..66a6ef1d4558b 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ }, "dependencies": { "@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/filesaver": "1.1.2", 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/src/core/public/public.api.md b/src/core/public/public.api.md index 396bf16cbdc6f..5c034e68a3736 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'; @@ -1225,12 +1226,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; 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; }; -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/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/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/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/point_in_time_finder.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts index b8f459151e7b3..9a8dcceafebb2 100644 --- a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts @@ -5,7 +5,7 @@ * 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 '../'; @@ -96,12 +96,12 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { 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; @@ -159,7 +159,7 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { }: { findOptions: SavedObjectsFindOptions; id?: string; - searchAfter?: unknown[]; + searchAfter?: estypes.Id[]; }) { try { return await this.#client.find({ @@ -168,8 +168,8 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { 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) { @@ -181,7 +181,7 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { } } - 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.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index bff23895fe459..37572c83e4c88 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -2782,18 +2782,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() ); @@ -3837,6 +3839,7 @@ describe('SavedObjectsRepository', () => { id: '6.0.0-alpha1', ...mockTimestampFields, version: mockVersion, + references: [], attributes: { buildNum: 8468, apiCallsCount: 100, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index a302cfe5a1e6f..aa1e62c1652ca 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -7,13 +7,9 @@ */ import { omit, isObject } from 'lodash'; -import { - ElasticsearchClient, - DeleteDocumentResponse, - GetResponse, - SearchResponse, -} from '../../../elasticsearch/'; -import { Logger } from '../../../logging'; +import type { estypes } from '@elastic/elasticsearch'; +import type { ElasticsearchClient } from '../../../elasticsearch/'; +import type { Logger } from '../../../logging'; import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { ISavedObjectsPointInTimeFinder, @@ -397,7 +393,7 @@ export class SavedObjectsRepository { _source: ['type', 'namespaces'], })); const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget( + ? await this.client.mget( { body: { docs: bulkGetDocs, @@ -425,8 +421,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', @@ -441,7 +438,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)) { @@ -500,7 +500,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) { @@ -564,10 +564,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, @@ -586,13 +586,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 }, }), }, @@ -636,7 +637,7 @@ export class SavedObjectsRepository { } } - const { body, statusCode } = await this.client.delete( + const { body, statusCode } = await this.client.delete( { id: rawId, index: this.getIndexForType(type), @@ -652,6 +653,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 @@ -813,15 +815,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, @@ -841,7 +846,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) { @@ -860,13 +865,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; } @@ -925,10 +932,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, @@ -947,7 +954,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, @@ -955,6 +963,7 @@ export class SavedObjectsRepository { } as any) as SavedObject; } + // @ts-expect-error MultiGetHit._source is optional return this.getSavedObjectFromSource(type, id, doc); }), }; @@ -980,7 +989,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), @@ -988,9 +997,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); } @@ -1026,7 +1039,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), @@ -1059,15 +1072,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: [ @@ -1090,23 +1104,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, @@ -1153,7 +1172,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), @@ -1173,11 +1192,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), ]; } @@ -1185,7 +1204,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 }), @@ -1325,7 +1343,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, @@ -1343,6 +1361,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 @@ -1477,9 +1496,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 { @@ -1491,10 +1511,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` @@ -1543,7 +1566,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( @@ -1636,7 +1659,7 @@ export class SavedObjectsRepository { } return { - updated: body.updated, + updated: body.updated!, }; } @@ -1745,7 +1768,7 @@ export class SavedObjectsRepository { 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, @@ -1783,17 +1806,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], }; } @@ -1852,9 +1874,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(); } @@ -1912,6 +1938,7 @@ export class SavedObjectsRepository { const { body } = await this.client.closePointInTime({ body: { id }, }); + return body; } @@ -2054,7 +2081,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), @@ -2065,8 +2092,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); } @@ -2091,7 +2117,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), @@ -2100,17 +2126,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; @@ -2220,3 +2249,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/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.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 9fa2896b7bbfe..9a0ccb88d3555 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -162,7 +162,7 @@ export interface SavedObjectsFindResult extends SavedObject { * await savedObjectsClient.closePointInTime(page2.pit_id); * ``` */ - sort?: unknown[]; + sort?: string[]; } /** 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 3d2023108c46a..73f8a44075162 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -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'; @@ -2507,12 +2508,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; @@ -2543,7 +2544,7 @@ export interface SavedObjectsFindResponse { // @public (undocumented) export interface SavedObjectsFindResult extends SavedObject { score: number; - sort?: unknown[]; + sort?: string[]; } // @public 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/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/public/public.api.md b/src/plugins/data/public/public.api.md index 5dc5a8ab2ce93..4eae5629af3a6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -28,6 +28,7 @@ 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'; @@ -92,8 +93,6 @@ 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'; @@ -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) // @@ -2415,9 +2414,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; 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/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/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/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 489a23eb83897..bdcc13ce4c061 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -12,7 +12,8 @@ import { IRouter, SharedGlobalConfig } from 'kibana/server'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { IFieldType, Filter } from '../index'; +import type { estypes } from '@elastic/elasticsearch'; +import type { IFieldType } from '../index'; import { findIndexPatternById, getFieldByName } from '../index_patterns'; import { getRequestAbortedSignal } from '../lib'; @@ -73,7 +74,7 @@ async function getBody( { timeout, terminate_after }: Record, field: IFieldType | string, query: string, - filters: Filter[] = [] + filters: estypes.QueryContainer[] = [] ) { const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); @@ -82,7 +83,7 @@ async function getBody( q.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, (match) => `\\${match}`); // Helps ensure that the regex is not evaluated eagerly against the terms dictionary - const executionHint = 'map'; + const executionHint = 'map' as const; // We don't care about the accuracy of the counts, just the content of the terms, so this reduces // the amount of information that needs to be transmitted to the coordinating node diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts index db950e7aa48f9..69a9280dd93d8 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts @@ -8,17 +8,6 @@ import { ElasticsearchClient } from 'kibana/server'; import { convertEsError } from './errors'; -import { FieldCapsResponse } from './field_capabilities'; - -export interface IndicesAliasResponse { - [index: string]: IndexAliasResponse; -} - -export interface IndexAliasResponse { - aliases: { - [aliasName: string]: Record; - }; -} /** * Call the index.getAlias API for a list of indices. @@ -67,7 +56,7 @@ export async function callFieldCapsApi( fieldCapsOptions: { allow_no_indices: boolean } = { allow_no_indices: false } ) { try { - return await callCluster.fieldCaps({ + return await callCluster.fieldCaps({ index: indices, fields: '*', ignore_unavailable: true, diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts index 31fd60b0382aa..c4c1ffa3cf9f9 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts @@ -7,23 +7,11 @@ */ import { uniq } from 'lodash'; +import type { estypes } from '@elastic/elasticsearch'; import { castEsToKbnFieldTypeName } from '../../../../../common'; import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; import { FieldDescriptor } from '../../../fetcher'; -interface FieldCapObject { - type: string; - searchable: boolean; - aggregatable: boolean; - indices?: string[]; - non_searchable_indices?: string[]; - non_aggregatable_indices?: string[]; -} - -export interface FieldCapsResponse { - fields: Record>; -} - /** * Read the response from the _field_caps API to determine the type and * "aggregatable"/"searchable" status of each field. @@ -80,7 +68,9 @@ export interface FieldCapsResponse { * @param {FieldCapsResponse} fieldCapsResponse * @return {Array} */ -export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): FieldDescriptor[] { +export function readFieldCapsResponse( + fieldCapsResponse: estypes.FieldCapabilitiesResponse +): FieldDescriptor[] { const capsByNameThenType = fieldCapsResponse.fields; const kibanaFormattedCaps = Object.keys(capsByNameThenType).reduce<{ diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/index.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/index.ts index d7150d81e3803..773a615727ee5 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/index.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/index.ts @@ -7,5 +7,4 @@ */ export { getFieldCapabilities } from './field_capabilities'; -export { FieldCapsResponse } from './field_caps_response'; export { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts index d01f74429c3a5..32b9d8c7f893f 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts @@ -12,7 +12,7 @@ import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; import { timePatternToWildcard } from './time_pattern_to_wildcard'; -import { callIndexAliasApi, IndicesAliasResponse } from './es_api'; +import { callIndexAliasApi } from './es_api'; /** * Convert a time pattern into a list of indexes it could @@ -28,7 +28,7 @@ import { callIndexAliasApi, IndicesAliasResponse } from './es_api'; export async function resolveTimePattern(callCluster: ElasticsearchClient, timePattern: string) { const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern)); - const allIndexDetails = chain(aliases.body) + const allIndexDetails = chain(aliases.body) .reduce( (acc: string[], index: any, indexName: string) => acc.concat(indexName, Object.keys(index.aliases || {})), diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 6dfc29e2cf2a6..aed35d73c7eb6 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -9,18 +9,16 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { SharedGlobalConfig } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { CollectedUsage, ReportedUsage } from './register'; interface SearchTelemetry { 'search-telemetry': CollectedUsage; } -type ESResponse = SearchResponse; export function fetchProvider(config$: Observable) { return async ({ esClient }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); - const { body: esResponse } = await esClient.search( + const { body: esResponse } = await esClient.search( { index: config.kibana.index, body: { @@ -37,7 +35,7 @@ export function fetchProvider(config$: Observable) { averageDuration: null, }; } - const { successCount, errorCount, totalDuration } = esResponse.hits.hits[0]._source[ + const { successCount, errorCount, totalDuration } = esResponse.hits.hits[0]._source![ 'search-telemetry' ]; const averageDuration = totalDuration / successCount; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index cc81dce94c4ec..1afe627545248 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -8,7 +8,6 @@ import { from, Observable } from 'rxjs'; import { first, tap } from 'rxjs/operators'; -import type { SearchResponse } from 'elasticsearch'; import type { Logger, SharedGlobalConfig } from 'kibana/server'; import type { ISearchStrategy } from '../types'; import type { SearchUsage } from '../collectors'; @@ -44,7 +43,7 @@ export const esSearchStrategyProvider = ( ...getShardTimeout(config), ...request.params, }; - const promise = esClient.asCurrentUser.search>(params); + const promise = esClient.asCurrentUser.search(params); const { body } = await shimAbortSignal(promise, abortSignal); const response = shimHitsTotal(body, options); return toKibanaSearchResponse(response); diff --git a/src/plugins/data/server/search/es_search/response_utils.ts b/src/plugins/data/server/search/es_search/response_utils.ts index 975ce392656b1..3bee63624ef67 100644 --- a/src/plugins/data/server/search/es_search/response_utils.ts +++ b/src/plugins/data/server/search/es_search/response_utils.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ISearchOptions } from '../../../common'; /** @@ -14,7 +14,7 @@ import { ISearchOptions } from '../../../common'; * not included as it is already included in `successful`. * @internal */ -export function getTotalLoaded(response: SearchResponse) { +export function getTotalLoaded(response: estypes.SearchResponse) { const { total, failed, successful } = response._shards; const loaded = failed + successful; return { total, loaded }; @@ -24,7 +24,7 @@ export function getTotalLoaded(response: SearchResponse) { * Get the Kibana representation of this response (see `IKibanaSearchResponse`). * @internal */ -export function toKibanaSearchResponse(rawResponse: SearchResponse) { +export function toKibanaSearchResponse(rawResponse: estypes.SearchResponse) { return { rawResponse, isPartial: false, @@ -41,7 +41,7 @@ export function toKibanaSearchResponse(rawResponse: SearchResponse) { * @internal */ export function shimHitsTotal( - response: SearchResponse, + response: estypes.SearchResponse, { legacyHitsTotal = true }: ISearchOptions = {} ) { if (!legacyHitsTotal) return response; diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts index 8648d3f4526fe..0c238adf831bd 100644 --- a/src/plugins/data/server/search/routes/call_msearch.ts +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -8,7 +8,6 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SearchResponse } from 'elasticsearch'; import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server'; import type { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source'; @@ -66,6 +65,7 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { try { const promise = esClient.asCurrentUser.msearch( { + // @ts-expect-error @elastic/elasticsearch client types don't support plain string bodies body: convertRequestBody(params.body, timeout), }, { @@ -78,9 +78,7 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { body: { ...response, body: { - responses: response.body.responses?.map((r: SearchResponse) => - shimHitsTotal(r) - ), + responses: response.body.responses?.map((r) => shimHitsTotal(r)), }, }, }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index e04fcdfa08f36..12458d7a74d9f 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -25,6 +25,7 @@ import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { estypes } from '@elastic/elasticsearch'; import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; @@ -64,7 +65,6 @@ import { SavedObjectsFindOptions } from 'kibana/server'; import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsUpdateResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; -import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { SharedGlobalConfig as SharedGlobalConfig_2 } from 'kibana/server'; import { ToastInputFields } from 'src/core/public/notifications'; @@ -596,7 +596,7 @@ export function getTime(indexPattern: IIndexPattern | undefined, timeRange: Time }): import("../..").RangeFilter | undefined; // @internal -export function getTotalLoaded(response: SearchResponse): { +export function getTotalLoaded(response: estypes.SearchResponse): { total: number; loaded: number; }; @@ -631,7 +631,7 @@ export interface IEsSearchRequest extends IKibanaSearchRequest = IKibanaSearchResponse>; +export type IEsSearchResponse = IKibanaSearchResponse>; // Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1384,30 +1384,26 @@ export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage, { isR export const shimAbortSignal: (promise: TransportRequestPromise, signal?: AbortSignal | undefined) => TransportRequestPromise; // @internal -export function shimHitsTotal(response: SearchResponse, { legacyHitsTotal }?: ISearchOptions): { +export function shimHitsTotal(response: estypes.SearchResponse, { legacyHitsTotal }?: ISearchOptions): { hits: { total: any; - max_score: number; - hits: { - _index: string; - _type: string; - _id: string; - _score: number; - _source: unknown; - _version?: number | undefined; - _explanation?: import("elasticsearch").Explanation | undefined; - fields?: any; - highlight?: any; - inner_hits?: any; - matched_queries?: string[] | undefined; - sort?: string[] | undefined; - }[]; + hits: estypes.Hit[]; + max_score?: number | undefined; }; took: number; timed_out: boolean; + _shards: estypes.ShardStatistics; + aggregations?: Record | undefined; + _clusters?: estypes.ClusterStatistics | undefined; + documents?: unknown[] | undefined; + fields?: Record | undefined; + max_score?: number | undefined; + num_reduce_phases?: number | undefined; + profile?: estypes.Profile | undefined; + pit_id?: string | undefined; _scroll_id?: string | undefined; - _shards: import("elasticsearch").ShardsResponse; - aggregations?: any; + suggest?: Record[]> | undefined; + terminated_early?: boolean | undefined; }; // Warning: (ae-missing-release-tag) "shouldReadFieldFromDocValues" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1425,10 +1421,10 @@ export type TimeRange = { }; // @internal -export function toKibanaSearchResponse(rawResponse: SearchResponse): { +export function toKibanaSearchResponse(rawResponse: estypes.SearchResponse): { total: number; loaded: number; - rawResponse: SearchResponse; + rawResponse: estypes.SearchResponse; isPartial: boolean; isRunning: boolean; }; diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index ba4d56b935512..0202f88e0e902 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -9,6 +9,7 @@ import angular, { auto, ICompileService, IScope } from 'angular'; import { render } from 'react-dom'; import React, { useRef, useEffect, useState, useCallback } from 'react'; +import type { estypes } from '@elastic/elasticsearch'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices, IIndexPattern } from '../../../kibana_services'; @@ -20,7 +21,7 @@ export interface DocTableLegacyProps { searchDescription?: string; searchTitle?: string; onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - rows: Array>; + rows: estypes.Hit[]; indexPattern: IIndexPattern; minimumVisibleRows: number; onAddColumn?: (column: string) => void; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index 8dded3598c279..5031f78c49fcc 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -87,6 +87,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { minimumVisibleRows, useNewFieldsApi, } = renderProps; + // @ts-expect-error doesn't implement full DocTableLegacyProps interface return { columns, indexPattern, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index 87b9c6243abd8..dbc94e5021294 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -140,7 +140,7 @@ export function DiscoverGridFlyout({ iconType="documents" flush="left" href={getContextUrl( - hit._id, + String(hit._id), indexPattern.id, columns, services.filterManager, diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx index f1025a0881d1f..74cf083d82653 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { ReactWrapper, shallow } from 'enzyme'; import { getRenderCellValueFn } from './get_render_cell_value'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; jest.mock('../../../../../kibana_react/public', () => ({ useUiSetting: () => true, @@ -26,7 +27,7 @@ jest.mock('../../../kibana_services', () => ({ }), })); -const rowsSource = [ +const rowsSource: ElasticSearchHit[] = [ { _id: '1', _index: 'test', @@ -34,12 +35,12 @@ const rowsSource = [ _score: 1, _source: { bytes: 100, extension: '.gz' }, highlight: { - extension: '@kibana-highlighted-field.gz@/kibana-highlighted-field', + extension: ['@kibana-highlighted-field.gz@/kibana-highlighted-field'], }, }, ]; -const rowsFields = [ +const rowsFields: ElasticSearchHit[] = [ { _id: '1', _index: 'test', @@ -48,12 +49,12 @@ const rowsFields = [ _source: undefined, fields: { bytes: [100], extension: ['.gz'] }, highlight: { - extension: '@kibana-highlighted-field.gz@/kibana-highlighted-field', + extension: ['@kibana-highlighted-field.gz@/kibana-highlighted-field'], }, }, ]; -const rowsFieldsWithTopLevelObject = [ +const rowsFieldsWithTopLevelObject: ElasticSearchHit[] = [ { _id: '1', _index: 'test', @@ -62,7 +63,7 @@ const rowsFieldsWithTopLevelObject = [ _source: undefined, fields: { 'object.value': [100], extension: ['.gz'] }, highlight: { - extension: '@kibana-highlighted-field.gz@/kibana-highlighted-field', + extension: ['@kibana-highlighted-field.gz@/kibana-highlighted-field'], }, }, ]; @@ -167,7 +168,9 @@ describe('Discover grid cell rendering', function () { }, "_type": "test", "highlight": Object { - "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field", + "extension": Array [ + "@kibana-highlighted-field.gz@/kibana-highlighted-field", + ], }, } } @@ -264,7 +267,9 @@ describe('Discover grid cell rendering', function () { ], }, "highlight": Object { - "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field", + "extension": Array [ + "@kibana-highlighted-field.gz@/kibana-highlighted-field", + ], }, } } diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 6de41aa0643a5..8997c1d13a474 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -87,7 +87,7 @@ describe('DocViewTable at Discover', () => { scripted: 123, _underscore: 123, }, - }; + } as any; const props = { hit, @@ -185,7 +185,7 @@ describe('DocViewTable at Discover Context', () => { Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \ Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', }, - }; + } as any; const props = { hit, columns: ['extension'], diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index b06b242ee9ea3..02ac951f7f57c 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -8,7 +8,7 @@ import { ComponentType } from 'react'; import { IScope } from 'angular'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { IndexPattern } from '../../../../data/public'; export interface AngularDirective { @@ -18,7 +18,7 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = SearchResponse['hits']['hits'][number]; +export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number]; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 829d23fa071fb..e7349ed22355a 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -357,7 +357,7 @@ export class SearchEmbeddable // Apply the changes to the angular scope this.searchScope.$apply(() => { this.searchScope!.hits = resp.hits.hits; - this.searchScope!.totalHitCount = resp.hits.total; + this.searchScope!.totalHitCount = resp.hits.total as number; this.searchScope!.isLoading = false; }); } catch (error) { diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 189f71b85206b..b9719542adc81 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -15,6 +15,7 @@ import * as CSS from 'csstype'; import { DetailedPeerCertificate } from 'tls'; import { EmbeddableStart as EmbeddableStart_2 } from 'src/plugins/embeddable/public/plugin'; 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'; diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts index 385b4f04c40f1..1343b20365a44 100644 --- a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts @@ -46,8 +46,8 @@ describe('preview_scripted_field route', () => { expect(mockClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "_source": undefined, "body": Object { + "_source": undefined, "query": Object { "match_all": Object {}, }, @@ -59,10 +59,10 @@ describe('preview_scripted_field route', () => { }, }, }, + "size": 10, + "timeout": "30s", }, "index": "kibana_sample_data_logs", - "size": 10, - "timeout": "30s", } `); @@ -102,12 +102,12 @@ describe('preview_scripted_field route', () => { expect(mockClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "_source": Array [ - "a", - "b", - "c", - ], "body": Object { + "_source": Array [ + "a", + "b", + "c", + ], "query": Object { "bool": Object { "some": "query", @@ -121,10 +121,10 @@ describe('preview_scripted_field route', () => { }, }, }, + "size": 10, + "timeout": "30s", }, "index": "kibana_sample_data_logs", - "size": 10, - "timeout": "30s", } `); }); diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts index 276f6dc0db8bf..cc161859f4189 100644 --- a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts @@ -30,10 +30,10 @@ export function registerPreviewScriptedFieldRoute(router: IRouter): void { try { const response = await client.search({ index, - _source: additionalFields && additionalFields.length > 0 ? additionalFields : undefined, - size: 10, - timeout: '30s', body: { + _source: additionalFields && additionalFields.length > 0 ? additionalFields : undefined, + size: 10, + timeout: '30s', query: query ?? { match_all: {} }, script_fields: { [name]: { 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/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts index 0670eb3116b07..9e9459a68754c 100644 --- a/src/plugins/security_oss/server/check_cluster_data.test.ts +++ b/src/plugins/security_oss/server/check_cluster_data.test.ts @@ -27,6 +27,7 @@ describe('checkClusterForUserData', () => { 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/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/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/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/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/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/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_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/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/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/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 1b1075f4d7cf1..1b29191d9063e 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; 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/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/scripts/create-functional-tests-archive/index.ts b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts index 7eb6aba0d005c..7f62fd3998060 100644 --- a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts +++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts @@ -10,6 +10,7 @@ import { execSync } from 'child_process'; import moment from 'moment'; import path from 'path'; import fs from 'fs'; +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { getEsClient } from '../shared/get_es_client'; import { parseIndexUrl } from '../shared/parse_index_url'; @@ -116,7 +117,7 @@ async function run() { const query = { bool: { - should: should.map(({ bool }) => ({ bool })), + should: should.map(({ bool }) => ({ bool })) as QueryContainer[], minimum_should_match: 1, }, }; diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index e4aedf452002d..25554eeeaf81d 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -13,7 +13,6 @@ // - Validate whether we can run the queries we want to on the telemetry data import { merge, chunk, flatten, omit } from 'lodash'; -import { Client } from '@elastic/elasticsearch'; import { argv } from 'yargs'; import { Logger } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -26,6 +25,7 @@ import { generateSampleDocuments } from './generate-sample-documents'; import { readKibanaConfig } from '../shared/read-kibana-config'; import { getHttpAuth } from '../shared/get-http-auth'; import { createOrUpdateIndex } from '../shared/create-or-update-index'; +import { getEsClient } from '../shared/get_es_client'; async function uploadData() { const githubToken = process.env.GITHUB_TOKEN; @@ -43,8 +43,8 @@ async function uploadData() { const httpAuth = getHttpAuth(config); - const client = new Client({ - nodes: [config['elasticsearch.hosts']], + const client = getEsClient({ + node: config['elasticsearch.hosts'], ...(httpAuth ? { auth: { ...httpAuth, username: 'elastic' }, @@ -83,10 +83,10 @@ async function uploadData() { apmAgentConfigurationIndex: '.apm-agent-configuration', }, search: (body) => { - return unwrapEsResponse(client.search(body as any)); + return unwrapEsResponse(client.search(body)) as Promise; }, indicesStats: (body) => { - return unwrapEsResponse(client.indices.stats(body)); + return unwrapEsResponse(client.indices.stats(body)); }, transportRequest: ((params) => { return unwrapEsResponse( diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index c4fef64f515d1..9a0ba514bb479 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -25,8 +25,8 @@ export function alertingEsClient( >, params: TParams ): Promise>> { - return services.scopedClusterClient.asCurrentUser.search({ + return (services.scopedClusterClient.asCurrentUser.search({ ...params, ignore_unavailable: true, - }); + }) as unknown) as Promise>>; } diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts index bea90109725d0..508b9419344cd 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MetricsAggregationResponsePart } from '../../../../../../../typings/elasticsearch/aggregations'; +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -45,7 +45,7 @@ export function getTransactionDurationChartPreview({ : []), ...rangeQuery(start, end), ...environmentQuery(environment), - ], + ] as QueryContainer[], }, }; @@ -85,7 +85,7 @@ export function getTransactionDurationChartPreview({ const x = bucket.key; const y = aggregationType === 'avg' - ? (bucket.agg as MetricsAggregationResponsePart).value + ? (bucket.agg as { value: number | null }).value : (bucket.agg as { values: Record }).values[ percentilesKey ]; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts index 167cb133102f2..d7dd7aee3ca25 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts @@ -54,10 +54,20 @@ describe('Error count alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { + relation: 'eq', value: 0, }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); @@ -89,7 +99,9 @@ describe('Error count alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { + relation: 'eq', value: 2, }, }, @@ -111,6 +123,14 @@ describe('Error count alert', () => { ], }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); @@ -177,7 +197,9 @@ describe('Error count alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { + relation: 'eq', value: 2, }, }, @@ -193,6 +215,14 @@ describe('Error count alert', () => { ], }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts index c18f29b6267e0..148cd813a8a22 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts @@ -52,10 +52,20 @@ describe('Transaction error rate alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { + relation: 'eq', value: 0, }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); @@ -87,7 +97,9 @@ describe('Transaction error rate alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { + relation: 'eq', value: 4, }, }, @@ -126,6 +138,14 @@ describe('Transaction error rate alert', () => { ], }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); @@ -196,7 +216,9 @@ describe('Transaction error rate alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { + relation: 'eq', value: 4, }, }, @@ -221,6 +243,14 @@ describe('Transaction error rate alert', () => { ], }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); @@ -274,8 +304,10 @@ describe('Transaction error rate alert', () => { services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { + hits: [], total: { value: 4, + relation: 'eq', }, }, aggregations: { @@ -286,6 +318,14 @@ describe('Transaction error rate alert', () => { buckets: [{ key: 'foo' }, { key: 'bar' }], }, }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, }) ); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 98063e3e1e3fd..87686d2c30cae 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import { Logger } from 'kibana/server'; -import { RequestParams } from '@elastic/elasticsearch'; +import { IndicesStats } from '@elastic/elasticsearch/api/requestParams'; import { ESSearchRequest, ESSearchResponse, @@ -22,7 +22,7 @@ type TelemetryTaskExecutor = (params: { params: TSearchRequest ): Promise>; indicesStats( - params: RequestParams.IndicesStats + params: IndicesStats // promise returned by client has an abort property // so we cannot use its ReturnType ): Promise<{ diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index e9744c6614641..e2a39b521466a 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -4,10 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { ValuesType } from 'utility-types'; import { flatten, merge, sortBy, sum, pickBy } from 'lodash'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { CompositeAggregationSource } from '@elastic/elasticsearch/api/types'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { ProcessorEvent } from '../../../../common/processor_event'; import { TelemetryTask } from '.'; import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; @@ -60,9 +59,7 @@ export const tasks: TelemetryTask[] = [ // the transaction count for that time range. executor: async ({ indices, search }) => { async function getBucketCountFromPaginatedQuery( - sources: Array< - ValuesType[string] - >, + sources: CompositeAggregationSource[], prevResult?: { transaction_count: number; expected_metric_document_count: number; @@ -151,7 +148,7 @@ export const tasks: TelemetryTask[] = [ }, size: 1, sort: { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, }, }) @@ -317,7 +314,7 @@ export const tasks: TelemetryTask[] = [ service_environments: { composite: { size: 1000, - sources: [ + sources: asMutableArray([ { [SERVICE_ENVIRONMENT]: { terms: { @@ -333,7 +330,7 @@ export const tasks: TelemetryTask[] = [ }, }, }, - ], + ] as const), }, }, }, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 848da41bc8372..88ef1203bae9f 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -76,7 +76,7 @@ export async function createApmTelemetry({ }); const search: CollectTelemetryParams['search'] = (params) => - unwrapEsResponse(esClient.asInternalUser.search(params)); + unwrapEsResponse(esClient.asInternalUser.search(params)) as any; const indicesStats: CollectTelemetryParams['indicesStats'] = (params) => unwrapEsResponse(esClient.asInternalUser.indices.stats(params)); diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts index f613a0dbca402..c668f3bb28713 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -11,7 +11,7 @@ import { processSignificantTermAggs, TopSigTerm, } from '../process_significant_term_aggs'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { environmentQuery, diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts index b800a21ffc341..50613c10ff7c0 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts @@ -6,7 +6,7 @@ */ import { isEmpty, dropRightWhile } from 'lodash'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts index 6afca46ec7391..9472d385a26c6 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { environmentQuery, @@ -73,6 +73,10 @@ export async function getCorrelationsForSlowTransactions({ setup, }); + if (!durationForPercentile) { + return {}; + } + const response = await withApmSpan('get_significant_terms', () => { const params = { apm: { events: [ProcessorEvent.transaction] }, diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts index 2732cd45c342e..94ed3dc3b6999 100644 --- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts +++ b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts @@ -9,7 +9,7 @@ import { orderBy } from 'lodash'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../../typings/elasticsearch/aggregations'; +} from '../../../../../../typings/elasticsearch'; export interface TopSigTerm { fieldName: string; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts index d4ad2c8a9b2cb..57fb486180993 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { ERROR_GROUP_ID, SERVICE_NAME, @@ -54,10 +55,10 @@ export function getErrorGroupSample({ should: [{ term: { [TRANSACTION_SAMPLED]: true } }], }, }, - sort: [ + sort: asMutableArray([ { _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top { '@timestamp': { order: 'desc' } }, // sort by timestamp to get the most recent error - ], + ] as const), }, }; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 1c262ebf882b2..f5b22e5349756 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SortOptions } from '../../../../../../typings/elasticsearch/aggregations'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -48,7 +47,7 @@ export function getErrorGroups({ serviceName, }); - const order: SortOptions = sortByLatestOccurrence + const order = sortByLatestOccurrence ? { max_timestamp: sortDirection, } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index e04b3a70a7593..e20103cc6ddca 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -13,7 +13,7 @@ import { } from '../../../../../../../../src/core/server'; import { ESSearchRequest, - ESSearchResponse, + InferSearchResponseOf, } from '../../../../../../../../typings/elasticsearch'; import { unwrapEsResponse } from '../../../../../../observability/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; @@ -54,7 +54,7 @@ type ESSearchRequestOf = Omit< type TypedSearchResponse< TParams extends APMEventESSearchRequest -> = ESSearchResponse< +> = InferSearchResponseOf< TypeOfProcessorEvent>, ESSearchRequestOf >; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 4faf80d7ca8db..2e83baece01a9 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -6,8 +6,12 @@ */ import { KibanaRequest } from 'src/core/server'; -import { RequestParams } from '@elastic/elasticsearch'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { + CreateIndexRequest, + DeleteRequest, + IndexRequest, +} from '@elastic/elasticsearch/api/types'; import { unwrapEsResponse } from '../../../../../../observability/server'; import { APMRequestHandlerContext } from '../../../../routes/typings'; import { @@ -21,7 +25,7 @@ import { } from '../call_async_with_debug'; import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; -export type APMIndexDocumentParams = RequestParams.Index; +export type APMIndexDocumentParams = IndexRequest; export type APMInternalClient = ReturnType; @@ -73,14 +77,14 @@ export function createInternalESClient({ params, }); }, - delete: (params: RequestParams.Delete): Promise<{ result: string }> => { + delete: (params: DeleteRequest): Promise<{ result: string }> => { return callEs({ operationName: 'delete', cb: () => asInternalUser.delete(params), params, }); }, - indicesCreate: (params: RequestParams.IndicesCreate) => { + indicesCreate: (params: CreateIndexRequest) => { return callEs({ operationName: 'indices.create', cb: () => asInternalUser.indices.create(params), diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 30b81a15f5efa..51f386d59c04a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -157,7 +157,9 @@ describe('setupRequest', () => { apm: { events: [ProcessorEvent.transaction], }, - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + body: { + query: { bool: { filter: [{ term: { field: 'someTerm' } }] } }, + }, }); const params = mockContext.core.elasticsearch.client.asCurrentUser.search.mock @@ -166,7 +168,7 @@ describe('setupRequest', () => { query: { bool: { filter: [ - { term: 'someTerm' }, + { term: { field: 'someTerm' } }, { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, { range: { 'observer.version_major': { gte: 7 } } }, ], @@ -183,7 +185,9 @@ describe('setupRequest', () => { apm: { events: [ProcessorEvent.error], }, - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + body: { + query: { bool: { filter: [{ term: { field: 'someTerm' } }] } }, + }, }, { includeLegacyData: true, @@ -196,7 +200,7 @@ describe('setupRequest', () => { query: { bool: { filter: [ - { term: 'someTerm' }, + { term: { field: 'someTerm' } }, { terms: { [PROCESSOR_EVENT]: ['error'], diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 9bec5eb4a247c..11d65b7697e9a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -10,7 +10,7 @@ import { EventOutcome } from '../../../common/event_outcome'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../../typings/elasticsearch/aggregations'; +} from '../../../../../../typings/elasticsearch'; export const getOutcomeAggregation = () => ({ terms: { diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 9f83af989fc57..3b3ef8b9c4bcf 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -7,6 +7,7 @@ import { sum, round } from 'lodash'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { isFiniteNumber } from '../../../../../../common/utils/is_finite_number'; import { Setup, SetupTimeRange } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; @@ -126,10 +127,9 @@ export async function fetchAndTransformGcMetrics({ const data = timeseriesData.buckets.map((bucket) => { // derivative/value will be undefined for the first hit and if the `max` value is null const bucketValue = bucket.value?.value; - const y = - bucketValue !== null && bucketValue !== undefined && bucket.value - ? round(bucketValue * (60 / bucketSize), 1) - : null; + const y = isFiniteNumber(bucketValue) + ? round(bucketValue * (60 / bucketSize), 1) + : null; return { y, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index df51bf30c1a53..0f1d7146f8459 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -144,12 +144,16 @@ export async function getPageLoadDistribution({ } // calculate the diff to get actual page load on specific duration value - let pageDist = pageDistVals.map(({ key, value }, index: number, arr) => { - return { - x: microToSec(key), - y: index === 0 ? value : value - arr[index - 1].value, - }; - }); + let pageDist = pageDistVals.map( + ({ key, value: maybeNullValue }, index: number, arr) => { + // FIXME: values from percentile* aggs can be null + const value = maybeNullValue!; + return { + x: microToSec(key), + y: index === 0 ? value : value - arr[index - 1].value!, + }; + } + ); pageDist = removeZeroesFromTail(pageDist); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 12b7961d51610..6a6caab953733 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -98,10 +98,12 @@ export const getPageLoadDistBreakdown = async ({ return pageDistBreakdowns?.map(({ key, page_dist: pageDist }) => { let seriesData = pageDist.values?.map( - ({ key: pKey, value }, index: number, arr) => { + ({ key: pKey, value: maybeNullValue }, index: number, arr) => { + // FIXME: values from percentile* aggs can be null + const value = maybeNullValue!; return { x: microToSec(pKey), - y: index === 0 ? value : value - arr[index - 1].value, + y: index === 0 ? value : value - arr[index - 1].value!, }; } ); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 88228f33dd3af..9bde701df5672 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -117,7 +117,7 @@ export async function getWebCoreVitals({ } = response.aggregations ?? {}; const getRanksPercentages = ( - ranks?: Array<{ key: number; value: number }> + ranks?: Array<{ key: number; value: number | null }> ) => { const ranksVal = ranks?.map(({ value }) => value?.toFixed(0) ?? 0) ?? []; return [ diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 8c97a3993e8c0..bcddbff34a8f6 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import { sortBy, uniqBy } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; @@ -63,7 +64,7 @@ export async function getServiceAnomalies({ by_field_value: [TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD], }, }, - ], + ] as estypes.QueryContainer[], }, }, aggs: { @@ -73,7 +74,7 @@ export async function getServiceAnomalies({ sources: [ { serviceName: { terms: { field: 'partition_field_value' } } }, { jobId: { terms: { field: 'job_id' } } }, - ], + ] as Array>, }, aggs: { metrics: { @@ -83,7 +84,7 @@ export async function getServiceAnomalies({ { field: 'by_field_value' }, { field: 'result_type' }, { field: 'record_score' }, - ] as const, + ], sort: { record_score: 'desc' as const, }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 8bc1b1f0562f5..fa04b963388b2 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import { sortBy, take, uniq } from 'lodash'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SERVICE_ENVIRONMENT, @@ -73,7 +74,7 @@ export function getTraceSampleIds({ aggs: { connections: { composite: { - sources: [ + sources: asMutableArray([ { [SPAN_DESTINATION_SERVICE_RESOURCE]: { terms: { @@ -96,7 +97,7 @@ export function getTraceSampleIds({ }, }, }, - ], + ] as const), size: fingerprintBucketSize, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 7e7e073c0d2f6..9d05369aca840 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -131,9 +131,11 @@ Array [ }, "sample": Object { "top_metrics": Object { - "metrics": Object { - "field": "agent.name", - }, + "metrics": Array [ + Object { + "field": "agent.name", + }, + ], "sort": Object { "@timestamp": "desc", }, @@ -213,9 +215,11 @@ Array [ }, "latest": Object { "top_metrics": Object { - "metrics": Object { - "field": "agent.name", - }, + "metrics": Array [ + Object { + "field": "agent.name", + }, + ], "sort": Object { "@timestamp": "desc", }, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index e0329e5f60e19..3e1a8f26de6b4 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -57,17 +57,17 @@ export function getStoredAnnotations({ const response: ESSearchResponse< ESAnnotation, { body: typeof body } - > = await unwrapEsResponse( - client.search({ + > = await (unwrapEsResponse( + client.search({ index: annotationsClient.index, body, }) - ); + ) as any); return response.hits.hits.map((hit) => { return { type: AnnotationType.VERSION, - id: hit._id, + id: hit._id as string, '@timestamp': new Date(hit._source['@timestamp']).getTime(), text: hit._source.message, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts index e41a88649c5ff..db491012c986b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts @@ -6,6 +6,7 @@ */ import { isEqual, keyBy, mapValues } from 'lodash'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { pickKeys } from '../../../../common/utils/pick_keys'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { @@ -58,7 +59,7 @@ export const getDestinationMap = ({ connections: { composite: { size: 1000, - sources: [ + sources: asMutableArray([ { [SPAN_DESTINATION_SERVICE_RESOURCE]: { terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, @@ -67,16 +68,18 @@ export const getDestinationMap = ({ // make sure we get samples for both successful // and failed calls { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, - ], + ] as const), }, aggs: { sample: { top_hits: { size: 1, _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], - sort: { - '@timestamp': 'desc', - }, + sort: [ + { + '@timestamp': 'desc' as const, + }, + ], }, }, }, @@ -123,12 +126,12 @@ export const getDestinationMap = ({ }, }, size: outgoingConnections.length, - docvalue_fields: [ + docvalue_fields: asMutableArray([ SERVICE_NAME, SERVICE_ENVIRONMENT, AGENT_NAME, PARENT_ID, - ] as const, + ] as const), _source: false, }, }) diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts index 676ba1625cc61..7729822df30ca 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -90,11 +90,11 @@ export async function getServiceErrorGroups({ sample: { top_hits: { size: 1, - _source: [ + _source: ([ ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp', - ], + ] as any) as string, sort: { '@timestamp': 'desc', }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts index a71772d1429cb..e2341b306a878 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts @@ -6,7 +6,6 @@ */ import { ProcessorEvent } from '../../../common/processor_event'; -import { SortOptions } from '../../../../../../typings/elasticsearch'; import { AGENT, CLOUD, @@ -96,7 +95,7 @@ export function getServiceMetadataDetails({ terms: { field: SERVICE_VERSION, size: 10, - order: { _key: 'desc' } as SortOptions, + order: { _key: 'desc' as const }, }, }, availabilityZones: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 10c7420d0f3b0..1e36df379e964 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -108,9 +108,9 @@ export async function getServiceTransactionStats({ }, sample: { top_metrics: { - metrics: { field: AGENT_NAME } as const, + metrics: [{ field: AGENT_NAME } as const], sort: { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts index cabd44c1e6907..906cc62e64d1a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts @@ -59,7 +59,7 @@ export function getServicesFromMetricDocuments({ }, latest: { top_metrics: { - metrics: { field: AGENT_NAME } as const, + metrics: [{ field: AGENT_NAME } as const], sort: { '@timestamp': 'desc' }, }, }, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts index 2d6eff33b5b4e..0b826ea10b6c4 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { ESSearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../typings/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; // needed for backwards compatability // All settings except `transaction_sample_rate` and `transaction_max_spans` are stored as strings (they are stored as float and integer respectively) export function convertConfigSettingsToString( - hit: ESSearchHit + hit: SearchHit ) { const config = hit._source; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index 4f2225e3cec18..7ec850717dab1 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient, Logger } from 'src/core/server'; import { createOrUpdateIndex, - MappingsDefinition, + Mappings, } from '../../../../../observability/server'; import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; @@ -31,7 +31,7 @@ export async function createApmAgentConfigurationIndex({ }); } -const mappings: MappingsDefinition = { +const mappings: Mappings = { dynamic: 'strict', dynamic_templates: [ { diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 972c076d88e76..9fd4849c7640a 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESSearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../typings/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { SERVICE_ENVIRONMENT, @@ -46,9 +46,7 @@ export function findExactConfiguration({ params ); - const hit = resp.hits.hits[0] as - | ESSearchHit - | undefined; + const hit = resp.hits.hits[0] as SearchHit | undefined; if (!hit) { return; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 12ba0939508e3..7454128a741d5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESSearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../typings/elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT, @@ -75,9 +75,7 @@ export async function searchConfigurations({ params ); - const hit = resp.hits.hits[0] as - | ESSearchHit - | undefined; + const hit = resp.hits.hits[0] as SearchHit | undefined; if (!hit) { return; diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts index 037f54344d770..3965e363499fc 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts @@ -6,9 +6,10 @@ */ import { ElasticsearchClient, Logger } from 'src/core/server'; +import { PropertyBase } from '@elastic/elasticsearch/api/types'; import { createOrUpdateIndex, - MappingsDefinition, + Mappings, } from '../../../../../observability/server'; import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; @@ -31,7 +32,7 @@ export const createApmCustomLinkIndex = async ({ }); }; -const mappings: MappingsDefinition = { +const mappings: Mappings = { dynamic: 'strict', properties: { '@timestamp': { @@ -45,7 +46,8 @@ const mappings: MappingsDefinition = { type: 'keyword', }, }, - }, + // FIXME: PropertyBase type is missing .fields + } as PropertyBase, url: { type: 'keyword', }, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index 8e343ecfe6a64..d3d9b45285354 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { compact } from 'lodash'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; @@ -22,15 +23,15 @@ export function getTransaction({ return withApmSpan('get_transaction_for_custom_link', async () => { const { apmEventClient } = setup; - const esFilters = Object.entries(filters) - // loops through the filters splitting the value by comma and removing white spaces - .map(([key, value]) => { - if (value) { - return { terms: { [key]: splitFilterValueByComma(value) } }; - } - }) - // removes filters without value - .filter((value) => value); + const esFilters = compact( + Object.entries(filters) + // loops through the filters splitting the value by comma and removing white spaces + .map(([key, value]) => { + if (value) { + return { terms: { [key]: splitFilterValueByComma(value) } }; + } + }) + ); const params = { terminateAfter: 1, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts index 7437b8328b876..f6b41f462c99f 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { CustomLink, CustomLinkES, @@ -31,7 +32,7 @@ export function listCustomLinks({ should: [ { term: { [key]: value } }, { bool: { must_not: [{ exists: { field: key } }] } }, - ], + ] as QueryContainer[], }, }; }); @@ -48,7 +49,7 @@ export function listCustomLinks({ sort: [ { 'label.keyword': { - order: 'asc', + order: 'asc' as const, }, }, ], diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index 0b158d9e57285..a946fa66a3b92 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { ProcessorEvent } from '../../../common/processor_event'; import { TRACE_ID, @@ -75,7 +76,7 @@ export async function getTraceItems( filter: [ { term: { [TRACE_ID]: traceId } }, ...rangeQuery(start, end), - ], + ] as QueryContainer[], should: { exists: { field: PARENT_ID }, }, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 34c2f39ca04c0..a4ff487645a4b 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -14,9 +14,11 @@ Array [ "aggs": Object { "transaction_type": Object { "top_metrics": Object { - "metrics": Object { - "field": "transaction.type", - }, + "metrics": Array [ + Object { + "field": "transaction.type", + }, + ], "sort": Object { "@timestamp": "desc", }, @@ -210,9 +212,11 @@ Array [ "aggs": Object { "transaction_type": Object { "top_metrics": Object { - "metrics": Object { - "field": "transaction.type", - }, + "metrics": Array [ + Object { + "field": "transaction.type", + }, + ], "sort": Object { "@timestamp": "desc", }, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 6308236000a53..c1bf363b49d1c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -8,6 +8,7 @@ import { sortBy, take } from 'lodash'; import moment from 'moment'; import { Unionize } from 'utility-types'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { @@ -132,14 +133,14 @@ export function transactionGroupsFetcher( ...(isTopTraces ? { composite: { - sources: [ + sources: asMutableArray([ { [SERVICE_NAME]: { terms: { field: SERVICE_NAME } } }, { [TRANSACTION_NAME]: { terms: { field: TRANSACTION_NAME }, }, }, - ], + ] as const), size, }, } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 5409f919bf895..86be82faee578 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -6,9 +6,9 @@ */ import { merge } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { AggregationInputMap } from '../../../../../../typings/elasticsearch'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { withApmSpan } from '../../utils/with_apm_span'; @@ -23,8 +23,8 @@ type BucketKey = string | Record; function mergeRequestWithAggs< TRequestBase extends TransactionGroupRequestBase, - TInputMap extends AggregationInputMap ->(request: TRequestBase, aggs: TInputMap) { + TAggregationMap extends Record +>(request: TRequestBase, aggs: TAggregationMap) { return merge({}, request, { body: { aggs: { @@ -71,11 +71,13 @@ export function getCounts({ request, setup }: MetricParams) { transaction_type: { top_metrics: { sort: { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, - metrics: { - field: TRANSACTION_TYPE, - } as const, + metrics: [ + { + field: TRANSACTION_TYPE, + } as const, + ], }, }, }); diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 1771b5ead68a7..1a586d1d4dbb6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { withApmSpan } from '../../../../utils/with_apm_span'; import { SERVICE_NAME, @@ -86,7 +86,7 @@ export async function getBuckets({ ...rangeQuery(start, end), ...environmentQuery(environment), ...kqlQuery(kuery), - ]; + ] as QueryContainer[]; async function getSamplesForDistributionBuckets() { const response = await withApmSpan( @@ -106,7 +106,7 @@ export async function getBuckets({ should: [ { term: { [TRACE_ID]: traceId } }, { term: { [TRANSACTION_ID]: transactionId } }, - ], + ] as QueryContainer[], }, }, aggs: { @@ -122,7 +122,7 @@ export async function getBuckets({ _source: [TRANSACTION_ID, TRACE_ID], size: 10, sort: { - _score: 'desc', + _score: 'desc' as const, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index a35780539a256..8b068fd6bd2fb 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { rangeQuery } from '../../../../server/utils/queries'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; @@ -42,7 +44,7 @@ export function anomalySeriesFetcher({ { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, ...rangeQuery(start, end, 'timestamp'), - ], + ] as QueryContainer[], }, }, aggs: { @@ -60,11 +62,11 @@ export function anomalySeriesFetcher({ aggs: { anomaly_score: { top_metrics: { - metrics: [ + metrics: asMutableArray([ { field: 'record_score' }, { field: 'timestamp' }, { field: 'bucket_span' }, - ] as const, + ] as const), sort: { record_score: 'desc' as const, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index a089850e427e6..b4323ae7f51e2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -12,6 +12,7 @@ import { import { rangeQuery } from '../../../../server/utils/queries'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { withApmSpan } from '../../../utils/with_apm_span'; export function getTransaction({ @@ -34,11 +35,11 @@ export function getTransaction({ size: 1, query: { bool: { - filter: [ + filter: asMutableArray([ { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, ...rangeQuery(start, end), - ], + ]), }, }, }, diff --git a/x-pack/plugins/apm/server/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts index ca43d0a8fb3c8..68056f091c873 100644 --- a/x-pack/plugins/apm/server/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { Setup, SetupTimeRange } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, @@ -51,7 +52,7 @@ export function getMetricsProjection({ ...rangeQuery(start, end), ...environmentQuery(environment), ...kqlQuery(kuery), - ]; + ] as QueryContainer[]; return { apm: { diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 9c6ea6bc83511..29fc85128ff3f 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -46,9 +46,7 @@ export function getRumPageLoadTransactionsProjection({ ? [ { wildcard: { - 'url.full': { - value: `*${urlQuery}*`, - }, + 'url.full': `*${urlQuery}*`, }, }, ] @@ -92,9 +90,7 @@ export function getRumErrorsProjection({ ? [ { wildcard: { - 'url.full': { - value: `*${urlQuery}*`, - }, + 'url.full': `*${urlQuery}*`, }, }, ] diff --git a/x-pack/plugins/apm/server/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts index 7955518d56f03..dd16b0b910abf 100644 --- a/x-pack/plugins/apm/server/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { Setup, SetupTimeRange } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, @@ -61,7 +62,7 @@ export function getTransactionsProjection({ ...rangeQuery(start, end), ...environmentQuery(environment), ...kqlQuery(kuery), - ], + ] as QueryContainer[], }; return { diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 558f165d43cf5..bb90aa0bf5eb4 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,20 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { - AggregationOptionsByType, - AggregationInputMap, - ESSearchBody, -} from '../../../../../typings/elasticsearch'; +import { estypes } from '@elastic/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; export type Projection = Omit & { - body: Omit & { + body: Omit< + Required['body'], + 'aggs' | 'aggregations' + > & { aggs?: { [key: string]: { terms: AggregationOptionsByType['terms'] & { field: string }; - aggs?: AggregationInputMap; + aggs?: Record; }; }; }; diff --git a/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts index c38859056afeb..16b2c6f8e6061 100644 --- a/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts @@ -13,11 +13,11 @@ describe('mergeProjection', () => { mergeProjection( { apm: { events: [] }, - body: { query: { bool: { must: [{ terms: ['a'] }] } } }, + body: { query: { bool: { must: [{ terms: { field: ['a'] } }] } } }, }, { apm: { events: [] }, - body: { query: { bool: { must: [{ term: 'b' }] } } }, + body: { query: { bool: { must: [{ term: { field: 'b' } }] } } }, } ) ).toEqual({ @@ -29,7 +29,7 @@ describe('mergeProjection', () => { bool: { must: [ { - term: 'b', + term: { field: 'b' }, }, ], }, diff --git a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 7f08707064862..fb2d981dd4a1f 100644 --- a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -7,20 +7,12 @@ import { cloneDeep, isPlainObject, mergeWith } from 'lodash'; import { DeepPartial } from 'utility-types'; -import { - AggregationInputMap, - ESSearchBody, -} from '../../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { Projection } from '../../typings'; type PlainObject = Record; -type SourceProjection = Omit, 'body'> & { - body: Omit, 'aggs'> & { - aggs?: AggregationInputMap; - }; -}; +type SourceProjection = DeepPartial; type DeepMerge = U extends PlainObject ? T extends PlainObject diff --git a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts index 17b714f8b72b0..144d77df064c7 100644 --- a/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/custom_element_collector.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; import { get } from 'lodash'; import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; import { collectFns } from './collector_helpers'; @@ -125,12 +124,10 @@ const customElementCollector: TelemetryCollector = async function customElementC body: { query: { bool: { filter: { term: { type: CUSTOM_ELEMENT_TYPE } } } } }, }; - const { body: esResponse } = await esClient.search>( - customElementParams - ); + const { body: esResponse } = await esClient.search(customElementParams); if (get(esResponse, 'hits.hits.length') > 0) { - const customElements = esResponse.hits.hits.map((hit) => hit._source[CUSTOM_ELEMENT_TYPE]); + const customElements = esResponse.hits.hits.map((hit) => hit._source![CUSTOM_ELEMENT_TYPE]); return summarizeCustomElements(customElements); } diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 7cb1d17dff437..7342cb5d40357 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; import { CANVAS_TYPE } from '../../common/lib/constants'; @@ -230,7 +229,6 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr variables: variableInfo, }; } -type ESResponse = SearchResponse; const workpadCollector: TelemetryCollector = async function (kibanaIndex, esClient) { const searchParams = { @@ -241,10 +239,10 @@ const workpadCollector: TelemetryCollector = async function (kibanaIndex, esClie body: { query: { bool: { filter: { term: { type: CANVAS_TYPE } } } } }, }; - const { body: esResponse } = await esClient.search(searchParams); + const { body: esResponse } = await esClient.search(searchParams); if (get(esResponse, 'hits.hits.length') > 0) { - const workpads = esResponse.hits.hits.map((hit) => hit._source[CANVAS_TYPE]); + const workpads = esResponse.hits.hits.map((hit) => hit._source![CANVAS_TYPE]); return summarizeWorkpads(workpads); } diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index 6ce4db61ab956..db8e841f45ee4 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -84,8 +84,9 @@ export class AlertService { return; } - const results = await scopedClusterClient.mget({ body: { docs } }); + const results = await scopedClusterClient.mget({ body: { docs } }); + // @ts-expect-error @elastic/elasticsearch _source is optional return results.body; } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts index 428de148fdd4f..f0a1ebf52f6a1 100644 --- a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts +++ b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts @@ -36,7 +36,8 @@ export function fetchProvider(config$: Observable, logger: L }, }); - const { buckets } = esResponse.aggregations.persisted; + // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregations + const buckets: SessionPersistedTermsBucket[] = esResponse.aggregations!.persisted.buckets; if (!buckets.length) { return { transientCount: 0, persistedCount: 0, totalCount: 0 }; } diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts index 185032bd25bb6..0b786f44454a9 100644 --- a/x-pack/plugins/data_enhanced/server/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -98,7 +98,7 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger: page: schema.maybe(schema.number()), perPage: schema.maybe(schema.number()), sortField: schema.maybe(schema.string()), - sortOrder: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), filter: schema.maybe(schema.string()), searchFields: schema.maybe(schema.arrayOf(schema.string())), search: schema.maybe(schema.string()), diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index 526d01dade7a7..9c1bedc4d5f1c 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { ApiResponse } from '@elastic/elasticsearch'; import { tap } from 'rxjs/operators'; import type { IScopedClusterClient, Logger } from 'kibana/server'; import type { ISearchStrategy } from '../../../../../src/plugins/data/server'; @@ -51,13 +51,10 @@ export const eqlSearchStrategyProvider = ( ...request.params, }; const promise = id - ? client.get({ ...params, id }, request.options) - : client.search( - params as EqlSearchStrategyRequest['params'], - request.options - ); + ? client.get({ ...params, id }, request.options) + : client.search(params as EqlSearchStrategyRequest['params'], request.options); const response = await shimAbortSignal(promise, options.abortSignal); - return toEqlKibanaSearchResponse(response); + return toEqlKibanaSearchResponse(response as ApiResponse); }; const cancel = async () => { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index d529e981aaea1..2ae79f4e144e0 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -347,7 +347,7 @@ describe('ES search strategy', () => { expect(mockGetCaller).toBeCalled(); const request = mockGetCaller.mock.calls[0][0]; - expect(request).toEqual({ id, keep_alive: keepAlive }); + expect(request).toEqual({ id, body: { keep_alive: keepAlive } }); }); it('throws normalized error on ElasticsearchClientError', async () => { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index fc1cc63146358..aec2e7bd533ec 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -34,7 +34,6 @@ import { getIgnoreThrottled, } from './request_utils'; import { toAsyncKibanaSearchResponse } from './response_utils'; -import { AsyncSearchResponse } from './types'; import { ConfigSchema } from '../../config'; import { getKbnServerError, KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; @@ -66,12 +65,14 @@ export const enhancedEsSearchStrategyProvider = ( ...(await getDefaultAsyncSubmitParams(uiSettingsClient, config, options)), ...request.params, }; - const promise = id - ? client.get({ ...params, id }) - : client.submit(params); + const promise = id ? client.get({ ...params, id }) : client.submit(params); const { body } = await shimAbortSignal(promise, options.abortSignal); const response = shimHitsTotal(body.response, options); - return toAsyncKibanaSearchResponse({ ...body, response }); + + return toAsyncKibanaSearchResponse( + // @ts-expect-error @elastic/elasticsearch start_time_in_millis expected to be number + { ...body, response } + ); }; const cancel = async () => { @@ -167,7 +168,7 @@ export const enhancedEsSearchStrategyProvider = ( extend: async (id, keepAlive, options, { esClient }) => { logger.debug(`extend ${id} by ${keepAlive}`); try { - await esClient.asCurrentUser.asyncSearch.get({ id, keep_alive: keepAlive }); + await esClient.asCurrentUser.asyncSearch.get({ id, body: { keep_alive: keepAlive } }); } catch (e) { throw getKbnServerError(e); } diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts index 9fe4c293a8741..dffccbee9db92 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts @@ -18,6 +18,7 @@ export async function getSearchStatus( ): Promise> { // TODO: Handle strategies other than the default one try { + // @ts-expect-error @elastic/elasticsearch status method is not defined const apiResponse: ApiResponse = await client.asyncSearch.status({ id: asyncId, }); diff --git a/x-pack/plugins/data_enhanced/server/search/types.ts b/x-pack/plugins/data_enhanced/server/search/types.ts index df3ca86466aa9..e2a4e2ce74f15 100644 --- a/x-pack/plugins/data_enhanced/server/search/types.ts +++ b/x-pack/plugins/data_enhanced/server/search/types.ts @@ -5,11 +5,12 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { SearchResponse, ShardsResponse } from 'elasticsearch'; export interface AsyncSearchResponse { id?: string; - response: SearchResponse; + response: estypes.SearchResponse; start_time_in_millis: number; expiration_time_in_millis: number; is_partial: boolean; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 73bf17195a5db..efe3186f97805 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -327,7 +327,15 @@ describe('queryEventsBySavedObject', () => { asApiResponse({ hits: { hits: [], - total: { value: 0 }, + total: { relation: 'eq', value: 0 }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: 0, + total: 0, + skipped: 0, }, }) ); @@ -391,11 +399,13 @@ describe('queryEventsBySavedObject', () => { }, }, "size": 10, - "sort": Object { - "@timestamp": Object { - "order": "asc", + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + }, }, - }, + ], }, "index": "index-name", "track_total_hits": true, @@ -408,7 +418,15 @@ describe('queryEventsBySavedObject', () => { asApiResponse({ hits: { hits: [], - total: { value: 0 }, + total: { relation: 'eq', value: 0 }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: 0, + total: 0, + skipped: 0, }, }) ); @@ -474,11 +492,13 @@ describe('queryEventsBySavedObject', () => { }, }, "size": 10, - "sort": Object { - "@timestamp": Object { - "order": "asc", + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + }, }, - }, + ], }, "index": "index-name", "track_total_hits": true, @@ -491,7 +511,15 @@ describe('queryEventsBySavedObject', () => { asApiResponse({ hits: { hits: [], - total: { value: 0 }, + total: { relation: 'eq', value: 0 }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: 0, + total: 0, + skipped: 0, }, }) ); @@ -507,7 +535,7 @@ describe('queryEventsBySavedObject', () => { expect(query).toMatchObject({ index: 'index-name', body: { - sort: { 'event.end': { order: 'desc' } }, + sort: [{ 'event.end': { order: 'desc' } }], }, }); }); @@ -517,7 +545,15 @@ describe('queryEventsBySavedObject', () => { asApiResponse({ hits: { hits: [], - total: { value: 0 }, + total: { relation: 'eq', value: 0 }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: 0, + total: 0, + skipped: 0, }, }) ); @@ -591,11 +627,13 @@ describe('queryEventsBySavedObject', () => { }, }, "size": 10, - "sort": Object { - "@timestamp": Object { - "order": "asc", + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + }, }, - }, + ], }, "index": "index-name", "track_total_hits": true, @@ -608,7 +646,15 @@ describe('queryEventsBySavedObject', () => { asApiResponse({ hits: { hits: [], - total: { value: 0 }, + total: { relation: 'eq', value: 0 }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: 0, + total: 0, + skipped: 0, }, }) ); @@ -690,11 +736,13 @@ describe('queryEventsBySavedObject', () => { }, }, "size": 10, - "sort": Object { - "@timestamp": Object { - "order": "asc", + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + }, }, - }, + ], }, "index": "index-name", "track_total_hits": true, diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index f025801a45955..5d7be2278d55d 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -7,9 +7,10 @@ import { Subject } from 'rxjs'; import { bufferTime, filter as rxFilter, switchMap } from 'rxjs/operators'; -import { reject, isUndefined } from 'lodash'; +import { reject, isUndefined, isNumber } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, ElasticsearchClient } from 'src/core/server'; +import { estypes } from '@elastic/elasticsearch'; import { EsContext } from '.'; import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; @@ -135,14 +136,13 @@ export class ClusterClientAdapter { } public async doesIndexTemplateExist(name: string): Promise { - let result; try { const esClient = await this.elasticsearchClientPromise; - result = (await esClient.indices.existsTemplate({ name })).body; + const { body } = await esClient.indices.existsTemplate({ name }); + return body as boolean; } catch (err) { throw new Error(`error checking existance of index template: ${err.message}`); } - return result as boolean; } public async createIndexTemplate(name: string, template: Record): Promise { @@ -162,20 +162,16 @@ export class ClusterClientAdapter { } public async doesAliasExist(name: string): Promise { - let result; try { const esClient = await this.elasticsearchClientPromise; - result = (await esClient.indices.existsAlias({ name })).body; + const { body } = await esClient.indices.existsAlias({ name }); + return body as boolean; } catch (err) { throw new Error(`error checking existance of initial index: ${err.message}`); } - return result as boolean; } - public async createIndex( - name: string, - body: string | Record = {} - ): Promise { + public async createIndex(name: string, body: Record = {}): Promise { try { const esClient = await this.elasticsearchClientPromise; await esClient.indices.create({ @@ -228,64 +224,67 @@ export class ClusterClientAdapter { }); throw err; } - const body = { - size: perPage, - from: (page - 1) * perPage, - sort: { [sort_field]: { order: sort_order } }, - query: { - bool: { - filter: dslFilterQuery, - must: reject( - [ - { - nested: { - path: 'kibana.saved_objects', - query: { - bool: { - must: [ - { - term: { - 'kibana.saved_objects.rel': { - value: SAVED_OBJECT_REL_PRIMARY, - }, - }, - }, - { - term: { - 'kibana.saved_objects.type': { - value: type, - }, - }, - }, - { - terms: { - // default maximum of 65,536 terms, configurable by index.max_terms_count - 'kibana.saved_objects.id': ids, - }, - }, - namespaceQuery, - ], + const musts: estypes.QueryContainer[] = [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: SAVED_OBJECT_REL_PRIMARY, }, }, }, - }, - start && { - range: { - '@timestamp': { - gte: start, + { + term: { + 'kibana.saved_objects.type': { + value: type, + }, }, }, - }, - end && { - range: { - '@timestamp': { - lte: end, + { + terms: { + // default maximum of 65,536 terms, configurable by index.max_terms_count + 'kibana.saved_objects.id': ids, }, }, - }, - ], - isUndefined - ), + namespaceQuery, + ], + }, + }, + }, + }, + ]; + if (start) { + musts.push({ + range: { + '@timestamp': { + gte: start, + }, + }, + }); + } + if (end) { + musts.push({ + range: { + '@timestamp': { + lte: end, + }, + }, + }); + } + + const body: estypes.SearchRequest['body'] = { + size: perPage, + from: (page - 1) * perPage, + sort: [{ [sort_field]: { order: sort_order } }], + query: { + bool: { + filter: dslFilterQuery, + must: reject(musts, isUndefined), }, }, }; @@ -295,7 +294,7 @@ export class ClusterClientAdapter { body: { hits: { hits, total }, }, - } = await esClient.search({ + } = await esClient.search({ index, track_total_hits: true, body, @@ -303,8 +302,8 @@ export class ClusterClientAdapter { return { page, per_page: perPage, - total: total.value, - data: hits.map((hit: { _source: unknown }) => hit._source) as IValidatedEvent[], + total: isNumber(total) ? total : total.value, + data: hits.map((hit) => hit._source), }; } catch (err) { throw new Error( diff --git a/x-pack/plugins/file_upload/server/analyze_file.tsx b/x-pack/plugins/file_upload/server/analyze_file.tsx index 394573eb0cca5..2239697083492 100644 --- a/x-pack/plugins/file_upload/server/analyze_file.tsx +++ b/x-pack/plugins/file_upload/server/analyze_file.tsx @@ -6,13 +6,7 @@ */ import { IScopedClusterClient } from 'kibana/server'; -import { - AnalysisResult, - FormattedOverrides, - InputData, - InputOverrides, - FindFileStructureResponse, -} from '../common'; +import { AnalysisResult, FormattedOverrides, InputData, InputOverrides } from '../common'; export async function analyzeFile( client: IScopedClusterClient, @@ -20,9 +14,7 @@ export async function analyzeFile( overrides: InputOverrides ): Promise { overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; - const { - body, - } = await client.asInternalUser.textStructure.findStructure({ + const { body } = await client.asInternalUser.textStructure.findStructure({ body: data, ...overrides, }); @@ -31,6 +23,7 @@ export async function analyzeFile( return { ...(hasOverrides && { overrides: reducedOverrides }), + // @ts-expect-error type incompatible with FindFileStructureResponse results: body, }; } diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index a928c80f6dd81..c684c05003612 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -96,6 +96,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // Query backing indices to extract data stream dataset, namespace, and type values const { body: { + // @ts-expect-error @elastic/elasticsearch aggregations are not typed aggregations: { dataset, namespace, type }, }, } = await esClient.search({ diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index 3be69893ab252..bcece7283270b 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -359,7 +359,7 @@ export async function getLatestConfigChangeAction( search: policyId, searchFields: ['policy_id'], sortField: 'created_at', - sortOrder: 'DESC', + sortOrder: 'desc', }); if (res.saved_objects[0]) { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 9aa7bbc9f2b18..b89b2b6d351b8 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -6,11 +6,11 @@ */ import Boom from '@hapi/boom'; -import type { SearchResponse, MGetResponse, GetResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import type { AgentSOAttributes, Agent, BulkActionResult, ListWithKuery } from '../../types'; -import type { ESSearchResponse } from '../../../../../../typings/elasticsearch'; + import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; @@ -120,7 +120,7 @@ export async function getAgentsByKuery( const kueryNode = _joinFilters(filters); const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; - const res = await esClient.search>({ + const res = await esClient.search({ index: AGENTS_INDEX, from: (page - 1) * perPage, size: perPage, @@ -140,7 +140,7 @@ export async function getAgentsByKuery( return { agents, - total: res.body.hits.total.value, + total: (res.body.hits.total as estypes.TotalHits).value, page, perPage, }; @@ -183,13 +183,14 @@ export async function countInactiveAgents( track_total_hits: true, body, }); + // @ts-expect-error value is number | TotalHits return res.body.hits.total.value; } export async function getAgentById(esClient: ElasticsearchClient, agentId: string) { const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`); try { - const agentHit = await esClient.get({ + const agentHit = await esClient.get({ index: AGENTS_INDEX, id: agentId, }); @@ -210,16 +211,16 @@ export async function getAgentById(esClient: ElasticsearchClient, agentId: strin export function isAgentDocument( maybeDocument: any -): maybeDocument is GetResponse { +): maybeDocument is estypes.MultiGetHit { return '_id' in maybeDocument && '_source' in maybeDocument; } -export type ESAgentDocumentResult = GetResponse; +export type ESAgentDocumentResult = estypes.MultiGetHit; export async function getAgentDocuments( esClient: ElasticsearchClient, agentIds: string[] ): Promise { - const res = await esClient.mget>({ + const res = await esClient.mget({ index: AGENTS_INDEX, body: { docs: agentIds.map((_id) => ({ _id })) }, }); @@ -247,7 +248,7 @@ export async function getAgentByAccessAPIKeyId( esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const res = await esClient.search>({ + const res = await esClient.search({ index: AGENTS_INDEX, q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); @@ -309,10 +310,11 @@ export async function bulkUpdateAgents( }); return { - items: res.body.items.map((item: { update: { _id: string; error?: Error } }) => ({ - id: item.update._id, - success: !item.update.error, - error: item.update.error, + items: res.body.items.map((item: estypes.BulkResponseItemContainer) => ({ + id: item.update!._id as string, + success: !item.update!.error, + // @ts-expect-error ErrorCause is not assignable to Error + error: item.update!.error as Error, })), }; } diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 89f37a01a6b00..c003d3d546e83 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -5,25 +5,26 @@ * 2.0. */ -import type { GetResponse, SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; -import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import type { SearchHit } from '../../../../../../typings/elasticsearch'; import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; type FleetServerAgentESResponse = - | GetResponse - | ESSearchHit - | SearchResponse['hits']['hits'][0]; + | estypes.MultiGetHit + | estypes.SearchResponse['hits']['hits'][0] + | SearchHit; export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent { + // @ts-expect-error @elastic/elasticsearch MultiGetHit._source is optional return { id: hit._id, ...hit._source, - policy_revision: hit._source.policy_revision_idx, + policy_revision: hit._source?.policy_revision_idx, current_error_events: [], access_api_key: undefined, status: undefined, - packages: hit._source.packages ?? [], + packages: hit._source?.packages ?? [], }; } diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 987f461587233..f040ba57c38be 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -47,7 +47,6 @@ describe('reassignAgent (singular)', () => { expect(esClient.update).toBeCalledTimes(1); const calledWith = esClient.update.mock.calls[0]; expect(calledWith[0]?.id).toBe(agentInUnmanagedDoc._id); - // @ts-expect-error expect(calledWith[0]?.body?.doc).toHaveProperty('policy_id', unmanagedAgentPolicySO.id); }); @@ -91,7 +90,6 @@ describe('reassignAgents (plural)', () => { // calls ES update with correct values const calledWith = esClient.bulk.mock.calls[0][0]; // only 1 are unmanaged and bulk write two line per update - // @ts-expect-error expect(calledWith.body.length).toBe(2); // @ts-expect-error expect(calledWith.body[0].update._id).toEqual(agentInUnmanagedDoc._id); @@ -150,8 +148,8 @@ function createClientsMock() { throw new Error(`${id} not found`); } }); - // @ts-expect-error esClientMock.bulk.mockResolvedValue({ + // @ts-expect-error not full interface body: { items: [] }, }); diff --git a/x-pack/plugins/fleet/server/services/agents/status.test.ts b/x-pack/plugins/fleet/server/services/agents/status.test.ts index b11ea7ae7f87c..35300dfc02769 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.test.ts @@ -12,8 +12,8 @@ import { getAgentStatusById } from './status'; describe('Agent status service', () => { it('should return inactive when agent is not active', async () => { const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - // @ts-expect-error mockElasticsearchClient.get.mockResolvedValue({ + // @ts-expect-error not full interface body: { _id: 'id', _source: { @@ -29,8 +29,8 @@ describe('Agent status service', () => { it('should return online when agent is active', async () => { const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - // @ts-expect-error mockElasticsearchClient.get.mockResolvedValue({ + // @ts-expect-error not full interface body: { _id: 'id', _source: { @@ -47,8 +47,8 @@ describe('Agent status service', () => { it('should return enrolling when agent is active but never checkin', async () => { const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - // @ts-expect-error mockElasticsearchClient.get.mockResolvedValue({ + // @ts-expect-error not full interface body: { _id: 'id', _source: { @@ -64,8 +64,8 @@ describe('Agent status service', () => { it('should return unenrolling when agent is unenrolling', async () => { const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - // @ts-expect-error mockElasticsearchClient.get.mockResolvedValue({ + // @ts-expect-error not full interface body: { _id: 'id', diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index 23ba8ac7bbd7f..3d0692c242096 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -65,10 +65,8 @@ describe('unenrollAgents (plural)', () => { // calls ES update with correct values const calledWith = esClient.bulk.mock.calls[1][0]; const ids = calledWith?.body - // @ts-expect-error .filter((i: any) => i.update !== undefined) .map((i: any) => i.update._id); - // @ts-expect-error const docs = calledWith?.body.filter((i: any) => i.doc).map((i: any) => i.doc); expect(ids).toHaveLength(2); expect(ids).toEqual(idsToUnenroll); @@ -90,10 +88,8 @@ describe('unenrollAgents (plural)', () => { const onlyUnmanaged = [agentInUnmanagedDoc._id, agentInUnmanagedDoc2._id]; const calledWith = esClient.bulk.mock.calls[1][0]; const ids = calledWith?.body - // @ts-expect-error .filter((i: any) => i.update !== undefined) .map((i: any) => i.update._id); - // @ts-expect-error const docs = calledWith?.body.filter((i: any) => i.doc).map((i: any) => i.doc); expect(ids).toHaveLength(onlyUnmanaged.length); expect(ids).toEqual(onlyUnmanaged); @@ -149,8 +145,8 @@ function createClientMock() { throw new Error('not found'); } }); - // @ts-expect-error esClientMock.bulk.mockResolvedValue({ + // @ts-expect-error not full interface body: { items: [] }, }); diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 4365c3913f433..b3edb20d51c4f 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -8,7 +8,6 @@ import uuid from 'uuid'; import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; -import type { GetResponse } from 'elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; @@ -42,10 +41,12 @@ export async function listEnrollmentApiKeys( q: kuery, }); + // @ts-expect-error @elastic/elasticsearch const items = res.body.hits.hits.map(esDocToEnrollmentApiKey); return { items, + // @ts-expect-error value is number | TotalHits total: res.body.hits.total.value, page, perPage, @@ -57,11 +58,12 @@ export async function getEnrollmentAPIKey( id: string ): Promise { try { - const res = await esClient.get>({ + const res = await esClient.get({ index: ENROLLMENT_API_KEYS_INDEX, id, }); + // @ts-expect-error esDocToEnrollmentApiKey doesn't accept optional _source return esDocToEnrollmentApiKey(res.body); } catch (e) { if (e instanceof ResponseError && e.statusCode === 404) { @@ -226,11 +228,12 @@ export async function generateEnrollmentAPIKey( } export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { - const res = await esClient.search>({ + const res = await esClient.search({ index: ENROLLMENT_API_KEYS_INDEX, q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, }); + // @ts-expect-error esDocToEnrollmentApiKey doesn't accept optional _source const [enrollmentAPIKey] = res.body.hits.hits.map(esDocToEnrollmentApiKey); if (enrollmentAPIKey?.api_key_id !== apiKeyId) { diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts index 9a12f6a3c0bdf..77785aeb026c1 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts @@ -16,7 +16,6 @@ import type { ElasticsearchClient } from 'kibana/server'; import type { ListResult } from '../../../common'; import { FLEET_SERVER_ARTIFACTS_INDEX } from '../../../common'; -import type { ESSearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { ArtifactsElasticsearchError } from '../../errors'; @@ -38,11 +37,12 @@ export const getArtifact = async ( id: string ): Promise => { try { - const esData = await esClient.get>({ + const esData = await esClient.get({ index: FLEET_SERVER_ARTIFACTS_INDEX, id, }); + // @ts-expect-error @elastic/elasticsearch _source is optional return esSearchHitToArtifact(esData.body); } catch (e) { if (isElasticsearchItemNotFoundError(e)) { @@ -92,9 +92,7 @@ export const listArtifacts = async ( const { perPage = 20, page = 1, kuery = '', sortField = 'created', sortOrder = 'asc' } = options; try { - const searchResult = await esClient.search< - ESSearchResponse - >({ + const searchResult = await esClient.search({ index: FLEET_SERVER_ARTIFACTS_INDEX, sort: `${sortField}:${sortOrder}`, q: kuery, @@ -103,9 +101,11 @@ export const listArtifacts = async ( }); return { + // @ts-expect-error @elastic/elasticsearch _source is optional items: searchResult.body.hits.hits.map((hit) => esSearchHitToArtifact(hit)), page, perPage, + // @ts-expect-error doesn't handle total as number total: searchResult.body.hits.total.value, }; } catch (e) { diff --git a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts index 43aa111f2efcf..3b81e47577ff7 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts @@ -5,13 +5,13 @@ * 2.0. */ -import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import type { SearchHit } from '../../../../../../typings/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, NewArtifact } from './types'; import { ARTIFACT_DOWNLOAD_RELATIVE_PATH } from './constants'; export const esSearchHitToArtifact = < - T extends Pick, '_id' | '_source'> + T extends Pick, '_id' | '_source'> >({ _id: id, _source: { diff --git a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts index 5569e4ac77d20..1a10f93f678b3 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts @@ -10,7 +10,7 @@ import type { ApiResponse } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; -import type { ESSearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import type { SearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, ArtifactsClientInterface } from './types'; import { newArtifactToElasticsearchProperties } from './mappings'; @@ -77,7 +77,7 @@ export const generateEsRequestErrorApiResponseMock = ( ); }; -export const generateArtifactEsGetSingleHitMock = (): ESSearchHit => { +export const generateArtifactEsGetSingleHitMock = (): SearchHit => { const { id, created, ...newArtifact } = generateArtifactMock(); const _source = { ...newArtifactToElasticsearchProperties(newArtifact), diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 456ed95a8c3e4..0b95f8d76627a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -497,6 +497,7 @@ const updateExistingDataStream = async ({ await esClient.indices.putMapping({ index: dataStreamName, body: mappings, + // @ts-expect-error @elastic/elasticsearch doesn't declare it on PutMappingRequest write_index_only: true, }); // if update fails, rollover data stream diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index cbd09b8d1e7a8..7d62c0ef41c8d 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -123,6 +123,7 @@ async function handleTransformInstall({ await esClient.transform.putTransform({ transform_id: transform.installationName, defer_validation: true, + // @ts-expect-error expect object, but given a string body: transform.content, }); } catch (err) { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts index 248c03e43add9..39681401ac955 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts @@ -55,6 +55,7 @@ export const deleteTransforms = async (esClient: ElasticsearchClient, transformI await esClient.transport.request( { method: 'DELETE', + // @ts-expect-error @elastic/elasticsearch Transform is empty interface path: `/${transform?.dest?.index}`, }, { diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 1d5a788c3a2c2..f078b214e4dfd 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -169,6 +169,7 @@ async function migrateAgentPolicies() { track_total_hits: true, }); + // @ts-expect-error value is number | TotalHits if (res.body.hits.total.value === 0) { return agentPolicyService.createFleetPolicyChangeFleetServer( soClient, diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts index a18459d5d21b9..77f14decc5642 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_add_policy_route.ts @@ -24,6 +24,7 @@ async function addLifecyclePolicy( }, }; + // @ts-expect-error @elastic/elasticsearch UpdateIndexSettingsRequest does not support index property return client.indices.putSettings({ index: indexName, body }); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts index 72768a1540adc..069adc139a86d 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts @@ -17,6 +17,7 @@ async function deletePolicies(client: ElasticsearchClient, policyName: string): ignore: [404], }; + // @ts-expect-error @elastic/elasticsearch DeleteSnapshotLifecycleRequest.policy_id is required return client.ilm.deleteLifecycle({ policy: policyName }, options); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts index 75320b6f2d242..9aa5e3c24d010 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts @@ -33,6 +33,7 @@ async function fetchPolicies(client: ElasticsearchClient): Promise( + 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/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/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/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 2cb00644f56d4..451b2284ba310 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,9 +9,9 @@ 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, @@ -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({ 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..439764f80186e 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 @@ -107,6 +107,7 @@ const getData = async ( 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 = [ 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..f6214edc5d0ab 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 @@ -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/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/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/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/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index a13163d8f774a..acfed44c5259e 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -28,6 +28,7 @@ describe('crete_list_item', () => { const options = getCreateListItemOptionsMock(); const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.index.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const listItem = await createListItem({ ...options, esClient }); @@ -54,6 +55,7 @@ describe('crete_list_item', () => { options.id = undefined; const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.index.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const list = await createListItem({ ...options, esClient }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index a5369bbfe7ca4..cf8a43be796df 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -6,7 +6,6 @@ */ import uuid from 'uuid'; -import { CreateDocumentResponse } from 'elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; import { @@ -69,7 +68,7 @@ export const createListItem = async ({ ...baseBody, ...elasticQuery, }; - const { body: response } = await esClient.index({ + const { body: response } = await esClient.index({ body, id, index: listItemIndex, diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.test.ts b/x-pack/plugins/lists/server/services/items/find_list_item.test.ts index 29e6f2f845002..c76d1c505df0c 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.test.ts @@ -20,9 +20,11 @@ describe('find_list_item', () => { const options = getFindListItemOptionsMock(); const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.count.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ count: 1 }) ); esClient.search.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _scroll_id: '123', _shards: getShardMock(), diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts index 727c55d53e459..3e37ccb0cfb1f 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts @@ -6,7 +6,6 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { Filter, @@ -75,8 +74,9 @@ export const findListItem = async ({ sortOrder, }); - const { body: respose } = await esClient.count<{ count: number }>({ + const { body: respose } = await esClient.count({ body: { + // @ts-expect-error GetQueryFilterReturn is not assignable to QueryContainer query, }, ignore_unavailable: true, @@ -87,8 +87,9 @@ export const findListItem = async ({ // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const { body: response } = await esClient.search>({ + const { body: response } = await esClient.search({ body: { + // @ts-expect-error GetQueryFilterReturn is not assignable to QueryContainer query, search_after: scroll.searchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index eb05a899478a5..519ebaedfddbc 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -6,7 +6,6 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; import { transformElasticToListItem } from '../utils'; @@ -26,7 +25,7 @@ export const getListItem = async ({ // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const { body: listItemES } = await esClient.search>({ + const { body: listItemES } = await esClient.search({ body: { query: { term: { @@ -40,6 +39,7 @@ export const getListItem = async ({ }); if (listItemES.hits.hits.length) { + // @ts-expect-error @elastic/elasticsearch _source is optional const type = findSourceType(listItemES.hits.hits[0]._source); if (type != null) { const listItems = transformElasticToListItem({ response: listItemES, type }); diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts index ae6b6ad3faecf..195bce879f34d 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts @@ -34,6 +34,7 @@ describe('update_list_item', () => { const options = getUpdateListItemOptionsMock(); const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.update.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const updatedList = await updateListItem({ ...options, esClient }); diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 645508691acc8..89c7e77707d8f 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CreateDocumentResponse } from 'elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; import { @@ -62,7 +61,7 @@ export const updateListItem = async ({ ...elasticQuery, }; - const { body: response } = await esClient.update({ + const { body: response } = await esClient.update({ ...decodeVersion(_version), body: { doc, diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts index b096adb2d1a13..ee4f3af9cdd5c 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -305,7 +305,9 @@ describe('write_list_items_to_stream', () => { test('it will throw an exception with a status code if the hit_source is not a data type we expect', () => { const options = getWriteResponseHitsToStreamOptionsMock(); + // @ts-expect-error _source is optional options.response.hits.hits[0]._source.ip = undefined; + // @ts-expect-error _source is optional options.response.hits.hits[0]._source.keyword = undefined; const expected = `Encountered an error where hit._source was an unexpected type: ${JSON.stringify( options.response.hits.hits[0]._source diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts index 9bdcb58835ab0..3679680ad79bd 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -7,7 +7,7 @@ import { PassThrough } from 'stream'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; import { SearchEsListItemSchema } from '../../../common/schemas'; @@ -95,8 +95,9 @@ export const writeNextResponse = async ({ export const getSearchAfterFromResponse = ({ response, }: { - response: SearchResponse; + response: estypes.SearchResponse; }): string[] | undefined => + // @ts-expect-error @elastic/elasticsearch SortResults contains null response.hits.hits.length > 0 ? response.hits.hits[response.hits.hits.length - 1].sort : undefined; @@ -115,7 +116,7 @@ export const getResponse = async ({ listId, listItemIndex, size = SIZE, -}: GetResponseOptions): Promise> => { +}: GetResponseOptions): Promise> => { return (( await esClient.search({ body: { @@ -131,11 +132,11 @@ export const getResponse = async ({ index: listItemIndex, size, }) - ).body as unknown) as SearchResponse; + ).body as unknown) as estypes.SearchResponse; }; export interface WriteResponseHitsToStreamOptions { - response: SearchResponse; + response: estypes.SearchResponse; stream: PassThrough; stringToAppend: string | null | undefined; } @@ -148,6 +149,7 @@ export const writeResponseHitsToStream = ({ const stringToAppendOrEmpty = stringToAppend ?? ''; response.hits.hits.forEach((hit) => { + // @ts-expect-error @elastic/elasticsearch _source is optional const value = findSourceValue(hit._source); if (value != null) { stream.push(`${value}${stringToAppendOrEmpty}`); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index 6fc556955fae3..e6213a1c6eabe 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -29,6 +29,7 @@ describe('crete_list', () => { const options = getCreateListOptionsMock(); const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.index.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const list = await createList({ ...options, esClient }); @@ -44,6 +45,7 @@ describe('crete_list', () => { }; const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.index.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const list = await createList({ ...options, esClient }); @@ -74,6 +76,7 @@ describe('crete_list', () => { options.id = undefined; const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.index.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const list = await createList({ ...options, esClient }); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 2671a23266ec9..baed699dc992f 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -6,7 +6,6 @@ */ import uuid from 'uuid'; -import { CreateDocumentResponse } from 'elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; import { encodeHitVersion } from '../utils/encode_hit_version'; @@ -73,7 +72,7 @@ export const createList = async ({ updated_by: user, version, }; - const { body: response } = await esClient.index({ + const { body: response } = await esClient.index({ body, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts index c5a398b0a1ad0..9c61d36dc0cd3 100644 --- a/x-pack/plugins/lists/server/services/lists/find_list.ts +++ b/x-pack/plugins/lists/server/services/lists/find_list.ts @@ -6,7 +6,6 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { Filter, @@ -66,6 +65,7 @@ export const findList = async ({ const { body: totalCount } = await esClient.count({ body: { + // @ts-expect-error GetQueryFilterReturn is not compatible with QueryContainer query, }, ignore_unavailable: true, @@ -76,8 +76,9 @@ export const findList = async ({ // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const { body: response } = await esClient.search>({ + const { body: response } = await esClient.search({ body: { + // @ts-expect-error GetQueryFilterReturn is not compatible with QueryContainer query, search_after: scroll.searchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index 50e6d08dd80ff..6f18d143df00b 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -6,7 +6,6 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; import { transformElasticToList } from '../utils/transform_elastic_to_list'; @@ -25,7 +24,7 @@ export const getList = async ({ // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const { body: response } = await esClient.search>({ + const { body: response } = await esClient.search({ body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/lists/update_list.test.ts b/x-pack/plugins/lists/server/services/lists/update_list.test.ts index e2d3b09fe518a..8cc1c60ecc23d 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.test.ts @@ -34,6 +34,7 @@ describe('update_list', () => { const options = getUpdateListOptionsMock(); const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.update.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const updatedList = await updateList({ ...options, esClient }); @@ -51,6 +52,7 @@ describe('update_list', () => { const options = getUpdateListOptionsMock(); const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.update.mockReturnValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) ); const updatedList = await updateList({ ...options, esClient }); diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index aa4eb9a8d834f..f98e40b04b6d7 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CreateDocumentResponse } from 'elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; import { decodeVersion } from '../utils/decode_version'; @@ -61,7 +60,7 @@ export const updateList = async ({ updated_at: updatedAt, updated_by: user, }; - const { body: response } = await esClient.update({ + const { body: response } = await esClient.update({ ...decodeVersion(_version), body: { doc }, id, diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts index 34359a7a9c697..ae37e47861845 100644 --- a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -6,7 +6,6 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; import { Scroll } from '../lists/types'; @@ -40,9 +39,10 @@ export const getSearchAfterScroll = async ({ const query = getQueryFilter({ filter }); let newSearchAfter = searchAfter; for (let i = 0; i < hops; ++i) { - const { body: response } = await esClient.search>>({ + const { body: response } = await esClient.search>({ body: { _source: getSourceWithTieBreaker({ sortField }), + // @ts-expect-error Filter is not assignale to QueryContainer query, search_after: newSearchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts index 87749ed6fdb3b..3cd902aeeb36e 100644 --- a/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { SortFieldOrUndefined } from '../../../common/schemas'; @@ -14,7 +13,7 @@ export type TieBreaker = T & { }; interface GetSearchAfterWithTieBreakerOptions { - response: SearchResponse>; + response: estypes.SearchResponse>; sortField: SortFieldOrUndefined; } @@ -27,14 +26,18 @@ export const getSearchAfterWithTieBreaker = ({ } else { const lastEsElement = response.hits.hits[response.hits.hits.length - 1]; if (sortField == null) { + // @ts-expect-error @elastic/elasticsearch _source is optional return [lastEsElement._source.tie_breaker_id]; } else { - const [[, sortValue]] = Object.entries(lastEsElement._source).filter( - ([key]) => key === sortField - ); + const [[, sortValue]] = Object.entries( + // @ts-expect-error @elastic/elasticsearch _source is optional + lastEsElement._source + ).filter(([key]) => key === sortField); if (typeof sortValue === 'string') { + // @ts-expect-error @elastic/elasticsearch _source is optional return [sortValue, lastEsElement._source.tie_breaker_id]; } else { + // @ts-expect-error @elastic/elasticsearch _source is optional return [lastEsElement._source.tie_breaker_id]; } } diff --git a/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts index 3fd886f8f6919..97cfe3dd8e634 100644 --- a/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts +++ b/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts @@ -4,25 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; -export interface SortWithTieBreakerReturn { - tie_breaker_id: 'asc'; - [key: string]: string; -} - export const getSortWithTieBreaker = ({ sortField, sortOrder, }: { sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; -}): SortWithTieBreakerReturn[] | undefined => { - const ascOrDesc = sortOrder ?? 'asc'; +}): estypes.SortCombinations[] => { + const ascOrDesc = sortOrder ?? ('asc' as const); if (sortField != null) { - return [{ [sortField]: ascOrDesc, tie_breaker_id: 'asc' }]; + return [{ [sortField]: ascOrDesc, tie_breaker_id: 'asc' as const }]; } else { - return [{ tie_breaker_id: 'asc' }]; + return [{ tie_breaker_id: 'asc' as const }]; } }; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index 3dd0f083797f1..4f0f8fe49a9d0 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { SearchEsListItemSchema, SearchListItemArraySchema, Type } from '../../../common/schemas'; import { transformElasticHitsToListItem } from './transform_elastic_to_list_item'; export interface TransformElasticMSearchToListItemOptions { - response: SearchResponse; + response: estypes.SearchResponse; type: Type; value: unknown[]; } diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts index fa77336fb7724..4ed08f70219af 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts @@ -5,19 +5,20 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ListArraySchema, SearchEsListSchema } from '../../../common/schemas'; import { encodeHitVersion } from './encode_hit_version'; export interface TransformElasticToListOptions { - response: SearchResponse; + response: estypes.SearchResponse; } export const transformElasticToList = ({ response, }: TransformElasticToListOptions): ListArraySchema => { + // @ts-expect-error created_at is incompatible return response.hits.hits.map((hit) => { return { _version: encodeHitVersion(hit), diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 8c1949ed90cda..436987e71dd22 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; @@ -14,12 +14,12 @@ import { encodeHitVersion } from './encode_hit_version'; import { findSourceValue } from './find_source_value'; export interface TransformElasticToListItemOptions { - response: SearchResponse; + response: estypes.SearchResponse; type: Type; } export interface TransformElasticHitToListItemOptions { - hits: SearchResponse['hits']['hits']; + hits: Array>; type: Type; } @@ -35,22 +35,21 @@ export const transformElasticHitsToListItem = ({ type, }: TransformElasticHitToListItemOptions): ListItemArraySchema => { return hits.map((hit) => { + const { _id, _source } = hit; const { - _id, - _source: { - /* eslint-disable @typescript-eslint/naming-convention */ - created_at, - deserializer, - serializer, - updated_at, - updated_by, - created_by, - list_id, - tie_breaker_id, - meta, - /* eslint-enable @typescript-eslint/naming-convention */ - }, - } = hit; + /* eslint-disable @typescript-eslint/naming-convention */ + created_at, + deserializer, + serializer, + updated_at, + updated_by, + created_by, + list_id, + tie_breaker_id, + meta, + /* eslint-enable @typescript-eslint/naming-convention */ + } = _source!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + // @ts-expect-error _source is optional const value = findSourceValue(hit._source); if (value == null) { throw new ErrorWithStatusCode(`Was expected ${type} to not be null/undefined`, 400); diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 0936cdc50b4c0..c55a564951c4e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -285,11 +285,29 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); - if (!esResp.aggregations.fitToBounds.bounds) { + + if (!esResp.aggregations) { + return null; + } + + const fitToBounds = esResp.aggregations.fitToBounds as { + bounds?: { + top_left: { + lat: number; + lon: number; + }; + bottom_right: { + lat: number; + lon: number; + }; + }; + }; + + if (!fitToBounds.bounds) { // aggregations.fitToBounds is empty object when there are no matching documents return null; } - esBounds = esResp.aggregations.fitToBounds.bounds; + esBounds = fitToBounds.bounds; } catch (error) { if (error.name === 'AbortError') { throw new DataRequestAbortError(); diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 3274261cdba56..d6ebf2fb216b2 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -179,6 +179,7 @@ export async function getTile({ [KBN_TOO_MANY_FEATURES_PROPERTY]: true, }, geometry: esBboxToGeoJsonPolygon( + // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response bboxResponse.rawResponse.aggregations.data_bounds.bounds, tileToESBbox(x, y, z) ), @@ -199,6 +200,7 @@ export async function getTile({ // Todo: pass in epochMillies-fields const featureCollection = hitsToGeoJson( + // @ts-expect-error hitsToGeoJson should be refactored to accept estypes.Hit documentsResponse.rawResponse.hits.hits, (hit: Record) => { return flattenHit(geometryFieldName, hit); diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts index ed9c9e7589749..77d453b68edc5 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts @@ -5,33 +5,41 @@ * 2.0. */ -import { IndexPatternTitle } from '../kibana'; -import { RuntimeMappings } from '../fields'; -import { JobId } from './job'; +import { estypes } from '@elastic/elasticsearch'; +// import { IndexPatternTitle } from '../kibana'; +// import { RuntimeMappings } from '../fields'; +// import { JobId } from './job'; export type DatafeedId = string; -export interface Datafeed { - datafeed_id: DatafeedId; - aggregations?: Aggregation; - aggs?: Aggregation; - chunking_config?: ChunkingConfig; - frequency?: string; - indices: IndexPatternTitle[]; - indexes?: IndexPatternTitle[]; // The datafeed can contain indexes and indices - job_id: JobId; - query: object; - query_delay?: string; - script_fields?: Record; - runtime_mappings?: RuntimeMappings; - scroll_size?: number; - delayed_data_check_config?: object; - indices_options?: IndicesOptions; -} +export type Datafeed = estypes.Datafeed; +// export interface Datafeed extends estypes.DatafeedConfig { +// runtime_mappings?: RuntimeMappings; +// aggs?: Aggregation; +// } +// export interface Datafeed { +// datafeed_id: DatafeedId; +// aggregations?: Aggregation; +// aggs?: Aggregation; +// chunking_config?: ChunkingConfig; +// frequency?: string; +// indices: IndexPatternTitle[]; +// indexes?: IndexPatternTitle[]; // The datafeed can contain indexes and indices +// job_id: JobId; +// query: object; +// query_delay?: string; +// script_fields?: Record; +// runtime_mappings?: RuntimeMappings; +// scroll_size?: number; +// delayed_data_check_config?: object; +// indices_options?: IndicesOptions; +// } -export interface ChunkingConfig { - mode: 'auto' | 'manual' | 'off'; - time_span?: string; -} +export type ChunkingConfig = estypes.ChunkingConfig; + +// export interface ChunkingConfig { +// mode: 'auto' | 'manual' | 'off'; +// time_span?: string; +// } export type Aggregation = Record< string, @@ -45,9 +53,10 @@ export type Aggregation = Record< } >; -export interface IndicesOptions { - expand_wildcards?: 'all' | 'open' | 'closed' | 'hidden' | 'none'; - ignore_unavailable?: boolean; - allow_no_indices?: boolean; - ignore_throttled?: boolean; -} +export type IndicesOptions = estypes.IndicesOptions; +// export interface IndicesOptions { +// expand_wildcards?: 'all' | 'open' | 'closed' | 'hidden' | 'none'; +// ignore_unavailable?: boolean; +// allow_no_indices?: boolean; +// ignore_throttled?: boolean; +// } diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts index a4b0a5c5c6068..5e1d5e009a764 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { UrlConfig } from '../custom_urls'; import { CREATED_BY_LABEL } from '../../constants/new_job'; @@ -19,79 +20,87 @@ export interface CustomSettings { }; } -export interface Job { - job_id: JobId; - analysis_config: AnalysisConfig; - analysis_limits?: AnalysisLimits; - background_persist_interval?: string; - custom_settings?: CustomSettings; - data_description: DataDescription; - description: string; - groups: string[]; - model_plot_config?: ModelPlotConfig; - model_snapshot_retention_days?: number; - daily_model_snapshot_retention_after_days?: number; - renormalization_window_days?: number; - results_index_name?: string; - results_retention_days?: number; +export type Job = estypes.Job; +// export interface Job { +// job_id: JobId; +// analysis_config: AnalysisConfig; +// analysis_limits?: AnalysisLimits; +// background_persist_interval?: string; +// custom_settings?: CustomSettings; +// data_description: DataDescription; +// description: string; +// groups: string[]; +// model_plot_config?: ModelPlotConfig; +// model_snapshot_retention_days?: number; +// daily_model_snapshot_retention_after_days?: number; +// renormalization_window_days?: number; +// results_index_name?: string; +// results_retention_days?: number; - // optional properties added when the job has been created - create_time?: number; - finished_time?: number; - job_type?: 'anomaly_detector'; - job_version?: string; - model_snapshot_id?: string; - deleting?: boolean; -} +// // optional properties added when the job has been created +// create_time?: number; +// finished_time?: number; +// job_type?: 'anomaly_detector'; +// job_version?: string; +// model_snapshot_id?: string; +// deleting?: boolean; +// } -export interface AnalysisConfig { - bucket_span: BucketSpan; - categorization_field_name?: string; - categorization_filters?: string[]; - categorization_analyzer?: object | string; - detectors: Detector[]; - influencers: string[]; - latency?: number; - multivariate_by_fields?: boolean; - summary_count_field_name?: string; - per_partition_categorization?: PerPartitionCategorization; -} +export type AnalysisConfig = estypes.AnalysisConfig; +// export interface AnalysisConfig { +// bucket_span: BucketSpan; +// categorization_field_name?: string; +// categorization_filters?: string[]; +// categorization_analyzer?: object | string; +// detectors: Detector[]; +// influencers: string[]; +// latency?: number; +// multivariate_by_fields?: boolean; +// summary_count_field_name?: string; +// per_partition_categorization?: PerPartitionCategorization; +// } -export interface Detector { - by_field_name?: string; - detector_description?: string; - detector_index?: number; - exclude_frequent?: string; - field_name?: string; - function: string; - over_field_name?: string; - partition_field_name?: string; - use_null?: boolean; - custom_rules?: CustomRule[]; -} -export interface AnalysisLimits { - categorization_examples_limit?: number; - model_memory_limit: string; -} +export type Detector = estypes.Detector; +// export interface Detector { +// by_field_name?: string; +// detector_description?: string; +// detector_index?: number; +// exclude_frequent?: string; +// field_name?: string; +// function: string; +// over_field_name?: string; +// partition_field_name?: string; +// use_null?: boolean; +// custom_rules?: CustomRule[]; +// } -export interface DataDescription { - format?: string; - time_field: string; - time_format?: string; -} +export type AnalysisLimits = estypes.AnalysisLimits; +// export interface AnalysisLimits { +// categorization_examples_limit?: number; +// model_memory_limit: string; +// } -export interface ModelPlotConfig { - enabled?: boolean; - annotations_enabled?: boolean; - terms?: string; -} +export type DataDescription = estypes.DataDescription; +// export interface DataDescription { +// format?: string; +// time_field: string; +// time_format?: string; +// } +export type ModelPlotConfig = estypes.ModelPlotConfig; +// export interface ModelPlotConfig { +// enabled?: boolean; +// annotations_enabled?: boolean; +// terms?: string; +// } + +export type CustomRule = estypes.DetectionRule; // TODO, finish this when it's needed -export interface CustomRule { - actions: string[]; - scope?: object; - conditions: any[]; -} +// export interface CustomRule { +// actions: string[]; +// scope?: object; +// conditions: any[]; +// } export interface PerPartitionCategorization { enabled?: boolean; diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index 047852534965c..f9f7f8fc7ead6 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, @@ -120,4 +121,4 @@ export interface RuntimeField { }; } -export type RuntimeMappings = Record; +export type RuntimeMappings = estypes.RuntimeFields; diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 4b80661f13c09..10f5fb975ef5e 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -733,7 +733,7 @@ export function validateGroupNames(job: Job): ValidationResults { * @return {Duration} the parsed interval, or null if it does not represent a valid * time interval. */ -export function parseTimeIntervalForJob(value: string | undefined): Duration | null { +export function parseTimeIntervalForJob(value: string | number | undefined): Duration | null { if (value === undefined) { return null; } @@ -748,7 +748,7 @@ export function parseTimeIntervalForJob(value: string | undefined): Duration | n // Checks that the value for a field which represents a time interval, // such as a job bucket span or datafeed query delay, is valid. -function isValidTimeInterval(value: string | undefined): boolean { +function isValidTimeInterval(value: string | number | undefined): boolean { if (value === undefined) { return true; } diff --git a/x-pack/plugins/ml/common/util/parse_interval.ts b/x-pack/plugins/ml/common/util/parse_interval.ts index c3013ef447792..6ca280dc12ebd 100644 --- a/x-pack/plugins/ml/common/util/parse_interval.ts +++ b/x-pack/plugins/ml/common/util/parse_interval.ts @@ -34,7 +34,10 @@ const SUPPORT_ES_DURATION_UNITS: SupportedUnits[] = ['ms', 's', 'm', 'h', 'd']; // to work with units less than 'day'. // 3. Fractional intervals e.g. 1.5h or 4.5d are not allowed, in line with the behaviour // of the Elasticsearch date histogram aggregation. -export function parseInterval(interval: string, checkValidEsUnit = false): Duration | null { +export function parseInterval( + interval: string | number, + checkValidEsUnit = false +): Duration | null { const matches = String(interval).trim().match(INTERVAL_STRING_RE); if (!Array.isArray(matches) || matches.length < 3) { return null; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 69750b0ab1aaa..312776f0d6a07 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -110,6 +110,7 @@ export const getRuntimeFieldsMapping = ( if (isPopulatedObject(ipRuntimeMappings)) { indexPatternFields.forEach((ipField) => { if (ipRuntimeMappings.hasOwnProperty(ipField)) { + // @ts-expect-error combinedRuntimeMappings[ipField] = ipRuntimeMappings[ipField]; } }); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts index a3753c8f000ae..4788254e97d1e 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -30,6 +30,7 @@ export function chartLoaderProvider(mlResultsService: MlResultsService) { job.data_counts.earliest_record_timestamp, job.data_counts.latest_record_timestamp, intervalMs, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options job.datafeed_config.indices_options ); if (resp.error !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 2ca95a14fb812..aac36f3e4f573 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; @@ -28,7 +29,7 @@ export interface RichDetector { byField: SplitField; overField: SplitField; partitionField: SplitField; - excludeFrequent: string | null; + excludeFrequent: estypes.ExcludeFrequent | null; description: string | null; customRules: CustomRule[] | null; } @@ -56,7 +57,7 @@ export class AdvancedJobCreator extends JobCreator { byField: SplitField, overField: SplitField, partitionField: SplitField, - excludeFrequent: string | null, + excludeFrequent: estypes.ExcludeFrequent | null, description: string | null ) { // addDetector doesn't support adding new custom rules. @@ -83,7 +84,7 @@ export class AdvancedJobCreator extends JobCreator { byField: SplitField, overField: SplitField, partitionField: SplitField, - excludeFrequent: string | null, + excludeFrequent: estypes.ExcludeFrequent | null, description: string | null, index: number ) { @@ -114,7 +115,7 @@ export class AdvancedJobCreator extends JobCreator { byField: SplitField, overField: SplitField, partitionField: SplitField, - excludeFrequent: string | null, + excludeFrequent: estypes.ExcludeFrequent | null, description: string | null, customRules: CustomRule[] | null ): { detector: Detector; richDetector: RichDetector } { @@ -182,6 +183,7 @@ export class AdvancedJobCreator extends JobCreator { timeFieldName: this.timeFieldName, query: this.query, runtimeMappings: this.datafeedConfig.runtime_mappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options indicesOptions: this.datafeedConfig.indices_options, }); this.setTimeRange(start.epoch, end.epoch); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index ec5cb59964ffd..13d46faaf21cf 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -246,12 +246,20 @@ export class JobCreator { private _initModelPlotConfig() { // initialize configs to false if they are missing if (this._job_config.model_plot_config === undefined) { - this._job_config.model_plot_config = {}; + this._job_config.model_plot_config = { + enabled: false, + }; } - if (this._job_config.model_plot_config.enabled === undefined) { + if ( + this._job_config.model_plot_config !== undefined && + this._job_config.model_plot_config.enabled === undefined + ) { this._job_config.model_plot_config.enabled = false; } - if (this._job_config.model_plot_config.annotations_enabled === undefined) { + if ( + this._job_config.model_plot_config !== undefined && + this._job_config.model_plot_config.annotations_enabled === undefined + ) { this._job_config.model_plot_config.annotations_enabled = false; } } @@ -636,6 +644,7 @@ export class JobCreator { this._job_config.custom_settings !== undefined && this._job_config.custom_settings[setting] !== undefined ) { + // @ts-expect-error return this._job_config.custom_settings[setting]; } return null; @@ -710,6 +719,7 @@ export class JobCreator { this._datafeed_config.runtime_mappings = {}; } Object.entries(runtimeFieldMap).forEach(([key, val]) => { + // @ts-expect-error this._datafeed_config.runtime_mappings![key] = val; }); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 201a304fd3356..bf354b8ad984f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -11,6 +11,7 @@ import { Job, Datafeed, Detector } from '../../../../../../../common/types/anoma import { splitIndexPatternNames } from '../../../../../../../common/util/job_utils'; export function createEmptyJob(): Job { + // @ts-expect-error return { job_id: '', description: '', @@ -27,6 +28,7 @@ export function createEmptyJob(): Job { } export function createEmptyDatafeed(indexPatternTitle: IndexPatternTitle): Datafeed { + // @ts-expect-error return { datafeed_id: '', job_id: '', diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts index 43e7d4e45b6e0..c67a93c5e0626 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts @@ -9,6 +9,7 @@ import { Job, Datafeed } from '../../../../../../../common/types/anomaly_detecti import { filterRuntimeMappings } from './filter_runtime_mappings'; function getJob(): Job { + // @ts-expect-error return { job_id: 'test', description: '', @@ -53,12 +54,14 @@ function getDatafeed(): Datafeed { runtime_mappings: { responsetime_big: { type: 'double', + // @ts-expect-error @elastic/elasticsearch StoredScript.language is required script: { source: "emit(doc['responsetime'].value * 100.0)", }, }, airline_lower: { type: 'keyword', + // @ts-expect-error @elastic/elasticsearch StoredScript.language is required script: { source: "emit(doc['airline'].value.toLowerCase())", }, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts index 641eda3dbf3e8..b51cd9b99792b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts @@ -52,6 +52,7 @@ export class CategorizationExamplesLoader { this._jobCreator.end, analyzer, this._jobCreator.runtimeMappings ?? undefined, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options this._jobCreator.datafeedConfig.indices_options ); return resp; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index a01581f7526c5..6d5fa26af7024 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -257,6 +257,7 @@ export class ResultsLoader { const fieldValues = await this._chartLoader.loadFieldExampleValues( this._jobCreator.splitField, this._jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options this._jobCreator.datafeedConfig.indices_options ); if (fieldValues.length > 0) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 751f12e14f6b5..10c160f58ff77 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import React, { FC, Fragment, useState, useContext, useEffect } from 'react'; import { EuiComboBox, @@ -170,6 +171,7 @@ export const AdvancedDetectorModal: FC = ({ byField, overField, partitionField, + // @ts-expect-error excludeFrequent: excludeFrequentOption.label !== '' ? excludeFrequentOption.label : null, description: descriptionOption !== '' ? descriptionOption : null, customRules: null, @@ -343,7 +345,9 @@ function createFieldOption(field: Field | null): EuiComboBoxOptionOption { }; } -function createExcludeFrequentOption(excludeFrequent: string | null): EuiComboBoxOptionOption { +function createExcludeFrequentOption( + excludeFrequent: estypes.ExcludeFrequent | null +): EuiComboBoxOptionOption { if (excludeFrequent === null) { return emptyOption; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index 85083146c1378..113bde6fbf93d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -42,6 +42,7 @@ export function useEstimateBucketSpan() { splitField: undefined, timeField: mlContext.currentIndexPattern.timeFieldName, runtimeMappings: jobCreator.runtimeMappings ?? undefined, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options indicesOptions: jobCreator.datafeedConfig.indices_options, }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx index da9f306cf30e6..f3396a95738a6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx @@ -55,6 +55,7 @@ export const CategorizationDetectorsSummary: FC = () => { jobCreator.start, jobCreator.end, chartInterval.getInterval().asMilliseconds(), + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); setEventRateChartData(resp); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index 46eb4b88d0518..0515496469030 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -114,6 +114,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { .loadFieldExampleValues( splitField, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ) .then(setFieldValues) @@ -145,6 +146,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { fieldValues.length > 0 ? fieldValues[0] : null, cs.intervalMs, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); setLineChartsData(resp); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx index a4c344d16482b..dc76fc0178112 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx @@ -44,6 +44,7 @@ export const MultiMetricDetectorsSummary: FC = () => { const tempFieldValues = await chartLoader.loadFieldExampleValues( jobCreator.splitField, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); setFieldValues(tempFieldValues); @@ -78,6 +79,7 @@ export const MultiMetricDetectorsSummary: FC = () => { fieldValues.length > 0 ? fieldValues[0] : null, cs.intervalMs, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); setLineChartsData(resp); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index a7eaaff611183..7f5a06925c7e8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -161,6 +161,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { jobCreator.splitField, cs.intervalMs, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); @@ -184,6 +185,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { fields: await chartLoader.loadFieldExampleValues( field, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ), }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index 55a9d37d1115c..31b436944a5b0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -79,6 +79,7 @@ export const PopulationDetectorsSummary: FC = () => { jobCreator.splitField, cs.intervalMs, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); @@ -102,6 +103,7 @@ export const PopulationDetectorsSummary: FC = () => { fields: await chartLoader.loadFieldExampleValues( field, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ), }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index 0e09a81908e83..c5c5cd4d8b744 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -94,6 +94,7 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { null, cs.intervalMs, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); if (resp[DTR_IDX] !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx index ced94b2095f72..5e64f4ef18984 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx @@ -60,6 +60,7 @@ export const SingleMetricDetectorsSummary: FC = () => { null, cs.intervalMs, jobCreator.runtimeMappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); if (resp[DTR_IDX] !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index d2cf6b7a00471..b57fd45019abe 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -48,6 +48,7 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) jobCreator.start, jobCreator.end, chartInterval.getInterval().asMilliseconds(), + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); setEventRateChartData(resp); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.ts b/x-pack/plugins/ml/public/application/util/string_utils.ts index 9cd22d1d6ce76..b981bbb8fe1a6 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.ts @@ -74,7 +74,7 @@ export function detectorToString(dtr: Detector): string { txt += PARTITION_FIELD_OPTION + quoteField(dtr.partition_field_name); } - if (dtr.exclude_frequent !== undefined && dtr.exclude_frequent !== '') { + if (dtr.exclude_frequent !== undefined) { txt += EXCLUDE_FREQUENT_OPTION + dtr.exclude_frequent; } diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index ce61541896721..81529669749bc 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -398,6 +398,7 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea const response = await mlClient.anomalySearch( { + // @ts-expect-error body: requestBody, }, jobIds @@ -425,7 +426,8 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea .filter((v) => v.doc_count > 0 && v[resultsLabel.aggGroupLabel].doc_count > 0) // Map response .map(formatter) - : [formatter(result as AggResultsResponse)] + : // @ts-expect-error + [formatter(result as AggResultsResponse)] ).filter(isDefined); }; diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index 278dd19f74acc..6e76a536feb25 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -6,17 +6,10 @@ */ import { IScopedClusterClient } from 'kibana/server'; - import { JobSavedObjectService } from '../../saved_objects'; import { JobType } from '../../../common/types/saved_objects'; -import { - Job, - JobStats, - Datafeed, - DatafeedStats, -} from '../../../common/types/anomaly_detection_jobs'; -import { Calendar } from '../../../common/types/calendars'; +import { Job, Datafeed } from '../../../common/types/anomaly_detection_jobs'; import { searchProvider } from './search'; import { DataFrameAnalyticsConfig } from '../../../common/types/data_frame_analytics'; @@ -109,7 +102,7 @@ export function getMlClient( // similar to groupIdsCheck above, however we need to load the jobs first to get the groups information const ids = getADJobIdsFromRequest(p); if (ids.length) { - const { body } = await mlClient.getJobs<{ jobs: Job[] }>(...p); + const { body } = await mlClient.getJobs(...p); await groupIdsCheck(p, body.jobs, filteredJobIds); } } @@ -131,6 +124,7 @@ export function getMlClient( } } + // @ts-expect-error promise and TransportRequestPromise are incompatible. missing abort return { async closeJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); @@ -152,7 +146,7 @@ export function getMlClient( // deleted initially and could still fail. return resp; }, - async deleteDatafeed(...p: any) { + async deleteDatafeed(...p: Parameters) { await datafeedIdsCheck(p); const resp = await mlClient.deleteDatafeed(...p); const [datafeedId] = getDatafeedIdsFromRequest(p); @@ -213,7 +207,7 @@ export function getMlClient( return mlClient.getCalendarEvents(...p); }, async getCalendars(...p: Parameters) { - const { body } = await mlClient.getCalendars<{ calendars: Calendar[] }, any>(...p); + const { body } = await mlClient.getCalendars(...p); const { body: { jobs: allJobs }, } = await mlClient.getJobs<{ jobs: Job[] }>(); @@ -263,9 +257,9 @@ export function getMlClient( // this should use DataFrameAnalyticsStats, but needs a refactor to move DataFrameAnalyticsStats to common await jobIdsCheck('data-frame-analytics', p, true); try { - const { body } = await mlClient.getDataFrameAnalyticsStats<{ - data_frame_analytics: DataFrameAnalyticsConfig[]; - }>(...p); + const { body } = ((await mlClient.getDataFrameAnalyticsStats(...p)) as unknown) as { + body: { data_frame_analytics: DataFrameAnalyticsConfig[] }; + }; const jobs = await jobSavedObjectService.filterJobsForSpace( 'data-frame-analytics', body.data_frame_analytics, @@ -282,8 +276,8 @@ export function getMlClient( async getDatafeedStats(...p: Parameters) { await datafeedIdsCheck(p, true); try { - const { body } = await mlClient.getDatafeedStats<{ datafeeds: DatafeedStats[] }>(...p); - const datafeeds = await jobSavedObjectService.filterDatafeedsForSpace( + const { body } = await mlClient.getDatafeedStats(...p); + const datafeeds = await jobSavedObjectService.filterDatafeedsForSpace( 'anomaly-detector', body.datafeeds, 'datafeed_id' @@ -299,7 +293,7 @@ export function getMlClient( async getDatafeeds(...p: Parameters) { await datafeedIdsCheck(p, true); try { - const { body } = await mlClient.getDatafeeds<{ datafeeds: Datafeed[] }>(...p); + const { body } = await mlClient.getDatafeeds(...p); const datafeeds = await jobSavedObjectService.filterDatafeedsForSpace( 'anomaly-detector', body.datafeeds, @@ -322,8 +316,8 @@ export function getMlClient( }, async getJobStats(...p: Parameters) { try { - const { body } = await mlClient.getJobStats<{ jobs: JobStats[] }>(...p); - const jobs = await jobSavedObjectService.filterJobsForSpace( + const { body } = await mlClient.getJobStats(...p); + const jobs = await jobSavedObjectService.filterJobsForSpace( 'anomaly-detector', body.jobs, 'job_id' diff --git a/x-pack/plugins/ml/server/lib/ml_client/search.ts b/x-pack/plugins/ml/server/lib/ml_client/search.ts index 158de0017fbbf..3062a70d9a975 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/search.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/search.ts @@ -7,11 +7,10 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; -import { RequestParams, ApiResponse } from '@elastic/elasticsearch'; +import { estypes, ApiResponse } from '@elastic/elasticsearch'; import { JobSavedObjectService } from '../../saved_objects'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import type { SearchResponse7 } from '../../../common/types/es_client'; import type { JobType } from '../../../common/types/saved_objects'; export function searchProvider( @@ -29,12 +28,12 @@ export function searchProvider( } async function anomalySearch( - searchParams: RequestParams.Search, + searchParams: estypes.SearchRequest, jobIds: string[] - ): Promise>> { + ): Promise>> { await jobIdsCheck('anomaly-detector', jobIds); const { asInternalUser } = client; - const resp = await asInternalUser.search>({ + const resp = await asInternalUser.search({ ...searchParams, index: ML_RESULTS_INDEX_PATTERN, }); diff --git a/x-pack/plugins/ml/server/lib/node_utils.ts b/x-pack/plugins/ml/server/lib/node_utils.ts index b76245fb9796c..82e5d7f469849 100644 --- a/x-pack/plugins/ml/server/lib/node_utils.ts +++ b/x-pack/plugins/ml/server/lib/node_utils.ts @@ -17,7 +17,7 @@ export async function getMlNodeCount(client: IScopedClusterClient): Promise { if (body.nodes[k].attributes !== undefined) { - const maxOpenJobs = body.nodes[k].attributes['ml.max_open_jobs']; + const maxOpenJobs = +body.nodes[k].attributes['ml.max_open_jobs']; if (maxOpenJobs !== null && maxOpenJobs > 0) { count++; } diff --git a/x-pack/plugins/ml/server/lib/query_utils.ts b/x-pack/plugins/ml/server/lib/query_utils.ts index 265962bb8432c..dd4dc01498dbb 100644 --- a/x-pack/plugins/ml/server/lib/query_utils.ts +++ b/x-pack/plugins/ml/server/lib/query_utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; /* * Contains utility functions for building and processing queries. */ @@ -40,7 +41,10 @@ export function buildBaseFilterCriteria( // Wraps the supplied aggregations in a sampler aggregation. // A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) // of less than 1 indicates no sampling, and the aggs are returned as-is. -export function buildSamplerAggregation(aggs: object, samplerShardSize: number) { +export function buildSamplerAggregation( + aggs: any, + samplerShardSize: number +): Record { if (samplerShardSize < 1) { return aggs; } diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index f5c64cfa51efc..2d5bd1f6f6e45 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -290,11 +290,13 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) { try { const { body } = await asInternalUser.search(params); + // @ts-expect-error TODO fix search response types if (body.error !== undefined && body.message !== undefined) { // No need to translate, this will not be exposed in the UI. throw new Error(`Annotations couldn't be retrieved from Elasticsearch.`); } + // @ts-expect-error TODO fix search response types const docs: Annotations = get(body, ['hits', 'hits'], []).map((d: EsResult) => { // get the original source document and the document id, we need it // to identify the annotation when editing/deleting it. diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts index 2efc2f905d9bb..1f5bbe8ac0fd4 100644 --- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import numeral from '@elastic/numeral'; import { IScopedClusterClient } from 'kibana/server'; import { MLCATEGORY } from '../../../common/constants/field_types'; @@ -89,12 +89,15 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => { new Set() ); - const maxBucketFieldCardinalities: string[] = influencers.filter( + const normalizedInfluencers: estypes.Field[] = Array.isArray(influencers) + ? influencers + : [influencers]; + const maxBucketFieldCardinalities = normalizedInfluencers.filter( (influencerField) => !!influencerField && !excludedKeywords.has(influencerField) && !overallCardinalityFields.has(influencerField) - ) as string[]; + ); if (overallCardinalityFields.size > 0) { overallCardinality = await fieldsService.getCardinalityOfFields( @@ -116,7 +119,7 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => { timeFieldName, earliestMs, latestMs, - bucketSpan, + bucketSpan as string, // update to Time type datafeedConfig ); } diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index 982485ab737ae..96bd74b9880a6 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,14 +5,17 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { difference } from 'lodash'; -import { EventManager, CalendarEvent } from './event_manager'; +import { EventManager } from './event_manager'; import type { MlClient } from '../../lib/ml_client'; +type ScheduledEvent = estypes.ScheduledEvent; + interface BasicCalendar { job_ids: string[]; description?: string; - events: CalendarEvent[]; + events: ScheduledEvent[]; } export interface Calendar extends BasicCalendar { @@ -37,23 +40,24 @@ export class CalendarManager { calendar_id: calendarId, }); - const calendars = body.calendars; + const calendars = body.calendars as Calendar[]; const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. calendar.events = await this._eventManager.getCalendarEvents(calendarId); return calendar; } async getAllCalendars() { + // @ts-expect-error missing size argument const { body } = await this._mlClient.getCalendars({ size: 1000 }); - const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = body.calendars; + const events: ScheduledEvent[] = await this._eventManager.getAllEvents(); + const calendars: Calendar[] = body.calendars as Calendar[]; calendars.forEach((cal) => (cal.events = [])); // loop events and combine with related calendars events.forEach((event) => { const calendar = calendars.find((cal) => cal.calendar_id === event.calendar_id); - if (calendar) { + if (calendar && calendar.events) { calendar.events.push(event); } }); @@ -98,7 +102,7 @@ export class CalendarManager { ); // if an event in the original calendar cannot be found, it must have been deleted - const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( + const eventsToRemove: ScheduledEvent[] = origCalendar.events.filter( (event) => calendar.events.find((e) => this._eventManager.isEqual(e, event)) === undefined ); diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 42ad3b99184c6..c870d67524135 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -5,16 +5,11 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import type { MlClient } from '../../lib/ml_client'; -export interface CalendarEvent { - calendar_id?: string; - event_id?: string; - description: string; - start_time: number; - end_time: number; -} +type ScheduledEvent = estypes.ScheduledEvent; export class EventManager { private _mlClient: MlClient; @@ -39,7 +34,7 @@ export class EventManager { return body.events; } - async addEvents(calendarId: string, events: CalendarEvent[]) { + async addEvents(calendarId: string, events: ScheduledEvent[]) { const body = { events }; return await this._mlClient.postCalendarEvents({ @@ -55,7 +50,7 @@ export class EventManager { }); } - isEqual(ev1: CalendarEvent, ev2: CalendarEvent) { + isEqual(ev1: ScheduledEvent, ev2: ScheduledEvent) { return ( ev1.event_id === ev2.event_id && ev1.description === ev2.description && diff --git a/x-pack/plugins/ml/server/models/calendar/index.ts b/x-pack/plugins/ml/server/models/calendar/index.ts index 26fb1bbe2c235..c5177dd675ca1 100644 --- a/x-pack/plugins/ml/server/models/calendar/index.ts +++ b/x-pack/plugins/ml/server/models/calendar/index.ts @@ -6,4 +6,3 @@ */ export { CalendarManager, Calendar, FormCalendar } from './calendar_manager'; -export { CalendarEvent } from './event_manager'; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index 0cbbf67dbbfac..516823ff78758 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -79,9 +79,9 @@ export function analyticsAuditMessagesProvider({ asInternalUser }: IScopedCluste }, }); - let messages = []; - if (body.hits.total.value > 0) { - messages = body.hits.hits.map((hit: Message) => hit._source); + let messages: JobMessage[] = []; + if (typeof body.hits.total !== 'number' && body.hits.total.value > 0) { + messages = (body.hits.hits as Message[]).map((hit) => hit._source); messages.reverse(); } return messages; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 371446638814a..4c79855f39e89 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import type { estypes } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { @@ -197,7 +198,7 @@ async function getValidationCheckMessages( analyzedFields: string[], index: string | string[], analysisConfig: AnalysisConfig, - query: unknown = defaultQuery + query: estypes.QueryContainer = defaultQuery ) { const analysisType = getAnalysisType(analysisConfig); const depVar = getDependentVar(analysisConfig); @@ -241,9 +242,11 @@ async function getValidationCheckMessages( }, }); + // @ts-expect-error const totalDocs = body.hits.total.value; if (body.aggregations) { + // @ts-expect-error Object.entries(body.aggregations).forEach(([aggName, { doc_count: docCount, value }]) => { const empty = docCount / totalDocs; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 4e99330610fca..21ed258a0b764 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -288,6 +288,7 @@ export class DataRecognizer { body: searchBody, }); + // @ts-expect-error fix search response return body.hits.total.value > 0; } @@ -864,10 +865,10 @@ export class DataRecognizer { try { const duration: { start: string; end?: string } = { start: '0' }; if (start !== undefined) { - duration.start = (start as unknown) as string; + duration.start = String(start); } if (end !== undefined) { - duration.end = (end as unknown) as string; + duration.end = String(end); } const { diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 2a820e0629b75..679b7b3f12a23 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -654,6 +654,7 @@ export class DataVisualizer { }); const aggregations = body.aggregations; + // @ts-expect-error fix search response const totalCount = body.hits.total.value; const stats = { totalCount, @@ -739,6 +740,7 @@ export class DataVisualizer { size, body: searchBody, }); + // @ts-expect-error fix search response return body.hits.total.value > 0; } @@ -1213,6 +1215,7 @@ export class DataVisualizer { fieldName: field, examples: [] as any[], }; + // @ts-expect-error fix search response if (body.hits.total.value > 0) { const hits = body.hits.hits; for (let i = 0; i < hits.length; i++) { diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 8e4dbaf23212f..c2b95d9a58584 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -184,6 +184,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { } = await asCurrentUser.search({ index, body, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options ...(datafeedConfig?.indices_options ?? {}), }); @@ -192,6 +193,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { } const aggResult = fieldsToAgg.reduce((obj, field) => { + // @ts-expect-error fix search aggregation response obj[field] = (aggregations[field] || { value: 0 }).value; return obj; }, {} as { [field: string]: number }); @@ -247,10 +249,14 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { }); if (aggregations && aggregations.earliest && aggregations.latest) { + // @ts-expect-error fix search aggregation response obj.start.epoch = aggregations.earliest.value; + // @ts-expect-error fix search aggregation response obj.start.string = aggregations.earliest.value_as_string; + // @ts-expect-error fix search aggregation response obj.end.epoch = aggregations.latest.value; + // @ts-expect-error fix search aggregation response obj.end.string = aggregations.latest.value_as_string; } return obj; @@ -400,6 +406,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { } = await asCurrentUser.search({ index, body, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options ...(datafeedConfig?.indices_options ?? {}), }); @@ -408,6 +415,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { } const aggResult = fieldsToAgg.reduce((obj, field) => { + // @ts-expect-error fix search aggregation response obj[field] = (aggregations[getMaxBucketAggKey(field)] || { value: 0 }).value ?? 0; return obj; }, {} as { [field: string]: number }); diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index c0c8b53e7aac6..2a25087260795 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import type { MlClient } from '../../lib/ml_client'; -import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; +// import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; +import { Job } from '../../../common/types/anomaly_detection_jobs'; export interface Filter { filter_id: string; @@ -46,17 +48,17 @@ interface FiltersInUse { [id: string]: FilterUsage; } -interface PartialDetector { - detector_description: string; - custom_rules: DetectorRule[]; -} +// interface PartialDetector { +// detector_description: string; +// custom_rules: DetectorRule[]; +// } -interface PartialJob { - job_id: string; - analysis_config: { - detectors: PartialDetector[]; - }; -} +// interface PartialJob { +// job_id: string; +// analysis_config: { +// detectors: PartialDetector[]; +// }; +// } export class FilterManager { constructor(private _mlClient: MlClient) {} @@ -69,15 +71,23 @@ export class FilterManager { this._mlClient.getFilters({ filter_id: filterId }), ]); - if (results[FILTERS] && results[FILTERS].body.filters.length) { + if ( + results[FILTERS] && + (results[FILTERS].body as estypes.GetFiltersResponse).filters.length + ) { let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && results[JOBS].body.jobs) { - filtersInUse = this.buildFiltersInUse(results[JOBS].body.jobs); + if (results[JOBS] && (results[JOBS].body as estypes.GetJobsResponse).jobs) { + filtersInUse = this.buildFiltersInUse( + (results[JOBS].body as estypes.GetJobsResponse).jobs + ); } - const filter = results[FILTERS].body.filters[0]; - filter.used_by = filtersInUse[filter.filter_id]; - return filter; + const filter = (results[FILTERS].body as estypes.GetFiltersResponse).filters[0]; + return { + ...filter, + used_by: filtersInUse[filter.filter_id], + item_count: 0, + } as FilterStats; } else { throw Boom.notFound(`Filter with the id "${filterId}" not found`); } @@ -105,8 +115,8 @@ export class FilterManager { // Build a map of filter_ids against jobs and detectors using that filter. let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && results[JOBS].body.jobs) { - filtersInUse = this.buildFiltersInUse(results[JOBS].body.jobs); + if (results[JOBS] && (results[JOBS].body as estypes.GetJobsResponse).jobs) { + filtersInUse = this.buildFiltersInUse((results[JOBS].body as estypes.GetJobsResponse).jobs); } // For each filter, return just @@ -115,8 +125,8 @@ export class FilterManager { // item_count // jobs using the filter const filterStats: FilterStats[] = []; - if (results[FILTERS] && results[FILTERS].body.filters) { - results[FILTERS].body.filters.forEach((filter: Filter) => { + if (results[FILTERS] && (results[FILTERS].body as estypes.GetFiltersResponse).filters) { + (results[FILTERS].body as estypes.GetFiltersResponse).filters.forEach((filter: Filter) => { const stats: FilterStats = { filter_id: filter.filter_id, description: filter.description, @@ -173,7 +183,7 @@ export class FilterManager { return body; } - buildFiltersInUse(jobsList: PartialJob[]) { + buildFiltersInUse(jobsList: Job[]) { // Build a map of filter_ids against jobs and detectors using that filter. const filtersInUse: FiltersInUse = {}; jobsList.forEach((job) => { @@ -183,7 +193,7 @@ export class FilterManager { const rules = detector.custom_rules; rules.forEach((rule) => { if (rule.scope) { - const ruleScope: DetectorRuleScope = rule.scope; + const ruleScope = rule.scope; const scopeFields = Object.keys(ruleScope); scopeFields.forEach((scopeField) => { const filter = ruleScope[scopeField]; diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index cf9b2027225c9..8279571adbae2 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; import { IScopedClusterClient } from 'kibana/server'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; @@ -27,7 +28,8 @@ export interface MlDatafeedsStatsResponse { interface Results { [id: string]: { - started: boolean; + started?: estypes.StartDatafeedResponse['started']; + stopped?: estypes.StopDatafeedResponse['stopped']; error?: any; }; } @@ -105,8 +107,10 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie async function startDatafeed(datafeedId: string, start?: number, end?: number) { return mlClient.startDatafeed({ datafeed_id: datafeedId, - start: (start as unknown) as string, - end: (end as unknown) as string, + body: { + start: start !== undefined ? String(start) : undefined, + end: end !== undefined ? String(end) : undefined, + }, }); } @@ -115,18 +119,16 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie for (const datafeedId of datafeedIds) { try { - const { body } = await mlClient.stopDatafeed<{ - started: boolean; - }>({ + const { body } = await mlClient.stopDatafeed({ datafeed_id: datafeedId, }); - results[datafeedId] = body; + results[datafeedId] = { stopped: body.stopped }; } catch (error) { if (isRequestTimeout(error)) { return fillResultsWithTimeouts(results, datafeedId, datafeedIds, DATAFEED_STATE.STOPPED); } else { results[datafeedId] = { - started: false, + stopped: false, error: error.body, }; } @@ -175,9 +177,7 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie // get all the datafeeds and match it with the jobId const { body: { datafeeds }, - } = await mlClient.getDatafeeds( - excludeGenerated ? { exclude_generated: true } : {} - ); + } = await mlClient.getDatafeeds(excludeGenerated ? { exclude_generated: true } : {}); // for (const result of datafeeds) { if (result.job_id === jobId) { return result; @@ -190,7 +190,7 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie try { const { body: { datafeeds: datafeedsResults }, - } = await mlClient.getDatafeeds({ + } = await mlClient.getDatafeeds({ datafeed_id: assumedDefaultDatafeedId, ...(excludeGenerated ? { exclude_generated: true } : {}), }); @@ -220,6 +220,7 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie job.data_description.time_field, query, datafeed.runtime_mappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options datafeed.indices_options ); @@ -351,6 +352,7 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie const data = { index: datafeed.indices, body, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options ...(datafeed.indices_options ?? {}), }; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index ac3e00a918da8..d0d824a88f5a9 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -143,7 +143,10 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); } - const { body } = await mlClient.stopDatafeed({ datafeed_id: datafeedId, force: true }); + const { body } = await mlClient.stopDatafeed({ + datafeed_id: datafeedId, + body: { force: true }, + }); if (body.stopped !== true) { return { success: false }; } @@ -316,6 +319,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { (ds) => ds.datafeed_id === datafeed.datafeed_id ); if (datafeedStats) { + // @ts-expect-error datafeeds[datafeed.job_id] = { ...datafeed, ...datafeedStats }; } } @@ -384,6 +388,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { if (jobStatsResults && jobStatsResults.jobs) { const jobStats = jobStatsResults.jobs.find((js) => js.job_id === tempJob.job_id); if (jobStats !== undefined) { + // @ts-expect-error tempJob = { ...tempJob, ...jobStats }; if (jobStats.node) { tempJob.node = jobStats.node; @@ -417,13 +422,20 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { const detailed = true; const jobIds: string[] = []; try { - const { body } = await asInternalUser.tasks.list({ actions, detailed }); - Object.keys(body.nodes).forEach((nodeId) => { - const tasks = body.nodes[nodeId].tasks; - Object.keys(tasks).forEach((taskId) => { - jobIds.push(tasks[taskId].description.replace(/^delete-job-/, '')); - }); + const { body } = await asInternalUser.tasks.list({ + // @ts-expect-error @elastic-elasticsearch expects it to be a string + actions, + detailed, }); + + if (body.nodes) { + Object.keys(body.nodes).forEach((nodeId) => { + const tasks = body.nodes![nodeId].tasks; + Object.keys(tasks).forEach((taskId) => { + jobIds.push(tasks[taskId].description!.replace(/^delete-job-/, '')); + }); + }); + } } catch (e) { // if the user doesn't have permission to load the task list, // use the jobs list to get the ids of deleting jobs diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index f1f5d98b96a53..425dff89032a3 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -73,8 +73,9 @@ export function modelSnapshotProvider(client: IScopedClusterClient, mlClient: Ml if (replay && model.snapshot_id === snapshotId && snapshot.model_snapshots.length) { // create calendar before starting restarting the datafeed if (calendarEvents !== undefined && calendarEvents.length) { + const calendarId = String(Date.now()); const calendar: FormCalendar = { - calendarId: String(Date.now()), + calendarId, job_ids: [jobId], description: i18n.translate( 'xpack.ml.models.jobService.revertModelSnapshot.autoCreatedCalendar.description', @@ -83,16 +84,18 @@ export function modelSnapshotProvider(client: IScopedClusterClient, mlClient: Ml } ), events: calendarEvents.map((s) => ({ + calendar_id: calendarId, + event_id: '', description: s.description, - start_time: s.start, - end_time: s.end, + start_time: `${s.start}`, + end_time: `${s.end}`, })), }; const cm = new CalendarManager(mlClient); await cm.newCalendar(calendar); } - forceStartDatafeeds([datafeedId], snapshot.model_snapshots[0].latest_record_time_stamp, end); + forceStartDatafeeds([datafeedId], +snapshot.model_snapshots[0].latest_record_time_stamp, end); } return { success: true }; diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index 37fa675362773..b0ee20763f430 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -127,7 +127,7 @@ export function categorizationExamplesProvider({ async function loadTokens(examples: string[], analyzer: CategorizationAnalyzer) { const { body: { tokens }, - } = await asInternalUser.indices.analyze<{ tokens: Token[] }>({ + } = await asInternalUser.indices.analyze({ body: { ...getAnalyzer(analyzer), text: examples, @@ -139,19 +139,21 @@ export function categorizationExamplesProvider({ const tokensPerExample: Token[][] = examples.map((e) => []); - tokens.forEach((t, i) => { - for (let g = 0; g < sumLengths.length; g++) { - if (t.start_offset <= sumLengths[g] + g) { - const offset = g > 0 ? sumLengths[g - 1] + g : 0; - tokensPerExample[g].push({ - ...t, - start_offset: t.start_offset - offset, - end_offset: t.end_offset - offset, - }); - break; + if (tokens !== undefined) { + tokens.forEach((t, i) => { + for (let g = 0; g < sumLengths.length; g++) { + if (t.start_offset <= sumLengths[g] + g) { + const offset = g > 0 ? sumLengths[g - 1] + g : 0; + tokensPerExample[g].push({ + ...t, + start_offset: t.start_offset - offset, + end_offset: t.end_offset - offset, + }); + break; + } } - } - }); + }); + } return tokensPerExample; } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 6637faeba094d..851336056a7f5 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -81,6 +81,7 @@ export function topCategoriesProvider(mlClient: MlClient) { const catCounts: Array<{ id: CategoryId; count: number; + // @ts-expect-error }> = body.aggregations?.cat_count?.buckets.map((c: any) => ({ id: c.key, count: c.doc_count, @@ -125,6 +126,7 @@ export function topCategoriesProvider(mlClient: MlClient) { [] ); + // @ts-expect-error return body.hits.hits?.map((c: { _source: Category }) => c._source) || []; } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 7ce54cd2f9c5e..0287c2af11a7e 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -5,13 +5,14 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { cloneDeep } from 'lodash'; import { SavedObjectsClientContract } from 'kibana/server'; import { Field, FieldId, NewJobCaps, RollupFields } from '../../../../common/types/fields'; import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { combineFieldsAndAggs } from '../../../../common/util/fields_utils'; -import { rollupServiceProvider, RollupJob } from './rollup'; +import { rollupServiceProvider } from './rollup'; import { aggregations, mlOnlyAggregations } from '../../../../common/constants/aggregation_types'; const supportedTypes: string[] = [ @@ -109,7 +110,9 @@ class FieldsService { this._mlClusterClient, this._savedObjectsClient ); - const rollupConfigs: RollupJob[] | null = await rollupService.getRollupJobs(); + const rollupConfigs: + | estypes.RollupCapabilitiesJob[] + | null = await rollupService.getRollupJobs(); // if a rollup index has been specified, yet there are no // rollup configs, return with no results @@ -131,14 +134,16 @@ class FieldsService { } } -function combineAllRollupFields(rollupConfigs: RollupJob[]): RollupFields { +function combineAllRollupFields(rollupConfigs: estypes.RollupCapabilitiesJob[]): RollupFields { const rollupFields: RollupFields = {}; rollupConfigs.forEach((conf) => { Object.keys(conf.fields).forEach((fieldName) => { if (rollupFields[fieldName] === undefined) { + // @ts-expect-error fix type. our RollupFields type is better rollupFields[fieldName] = conf.fields[fieldName]; } else { const aggs = conf.fields[fieldName]; + // @ts-expect-error fix type. our RollupFields type is better aggs.forEach((agg) => { if (rollupFields[fieldName].find((f) => f.agg === agg.agg) === null) { rollupFields[fieldName].push(agg); diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 3b480bae2199e..d83f7afb4cdf6 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { SavedObject } from 'kibana/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; @@ -26,7 +27,7 @@ export async function rollupServiceProvider( const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); let jobIndexPatterns: string[] = [indexPattern]; - async function getRollupJobs(): Promise { + async function getRollupJobs(): Promise { if (rollupIndexPatternObject !== null) { const parsedTypeMetaData = JSON.parse(rollupIndexPatternObject.attributes.typeMeta); const rollUpIndex: string = parsedTypeMetaData.params.rollup_index; @@ -36,7 +37,7 @@ export async function rollupServiceProvider( const indexRollupCaps = rollupCaps[rollUpIndex]; if (indexRollupCaps && indexRollupCaps.rollup_jobs) { - jobIndexPatterns = indexRollupCaps.rollup_jobs.map((j: RollupJob) => j.index_pattern); + jobIndexPatterns = indexRollupCaps.rollup_jobs.map((j) => j.index_pattern); return indexRollupCaps.rollup_jobs; } diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 2beade7f5dbc4..94e9a8dc7bffb 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -69,6 +69,7 @@ export async function validateJob( timeField, job.datafeed_config.query, job.datafeed_config.runtime_mappings, + // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options job.datafeed_config.indices_options ); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts index 853f96ad77743..44c5e3cabb18f 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts @@ -148,8 +148,7 @@ describe('ML - validateModelMemoryLimit', () => { it('Called with no duration or split and mml above limit', () => { const job = getJobConfig(); const duration = undefined; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '31mb'; + job.analysis_limits!.model_memory_limit = '31mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -166,8 +165,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(10); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '20mb'; + job.analysis_limits!.model_memory_limit = '20mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -184,8 +182,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(2); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '30mb'; + job.analysis_limits!.model_memory_limit = '30mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -202,8 +199,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(2); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '10mb'; + job.analysis_limits!.model_memory_limit = '10mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -222,8 +218,7 @@ describe('ML - validateModelMemoryLimit', () => { const duration = { start: 0, end: 1 }; // @ts-expect-error delete mlInfoResponse.limits.max_model_memory_limit; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '10mb'; + job.analysis_limits!.model_memory_limit = '10mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -239,8 +234,7 @@ describe('ML - validateModelMemoryLimit', () => { it('Called with no duration or split and mml above limit, no max setting', () => { const job = getJobConfig(); const duration = undefined; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '31mb'; + job.analysis_limits!.model_memory_limit = '31mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -256,8 +250,7 @@ describe('ML - validateModelMemoryLimit', () => { it('Called with no duration or split and mml above limit, no max setting, above effective max mml', () => { const job = getJobConfig(); const duration = undefined; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '41mb'; + job.analysis_limits!.model_memory_limit = '41mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -274,8 +267,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '20mb'; + job.analysis_limits!.model_memory_limit = '20mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -292,8 +284,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '0mb'; + job.analysis_limits!.model_memory_limit = '0mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -310,8 +301,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '10mbananas'; + job.analysis_limits!.model_memory_limit = '10mbananas'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -328,8 +318,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '10'; + job.analysis_limits!.model_memory_limit = '10'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -346,8 +335,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = 'mb'; + job.analysis_limits!.model_memory_limit = 'mb'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -364,8 +352,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = 'asdf'; + job.analysis_limits!.model_memory_limit = 'asdf'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -382,8 +369,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '1023KB'; + job.analysis_limits!.model_memory_limit = '1023KB'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -400,8 +386,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '1024KB'; + job.analysis_limits!.model_memory_limit = '1024KB'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -418,8 +403,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '6MB'; + job.analysis_limits!.model_memory_limit = '6MB'; return validateModelMemoryLimit( getMockMlClusterClient(), @@ -436,8 +420,7 @@ describe('ML - validateModelMemoryLimit', () => { const dtrs = createDetectors(1); const job = getJobConfig(['instance'], dtrs); const duration = { start: 0, end: 1 }; - // @ts-expect-error - job.analysis_limits.model_memory_limit = '20MB'; + job.analysis_limits!.model_memory_limit = '20MB'; return validateModelMemoryLimit( getMockMlClusterClient(), diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 81e26808bd246..be1786d64f2aa 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -10,7 +10,6 @@ import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; import { PartitionFieldsType } from '../../../common/types/anomalies'; import { CriteriaField } from './results_service'; import { FieldConfig, FieldsConfig } from '../../routes/schemas/results_service_schema'; -import { Job } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; type SearchTerm = @@ -151,7 +150,7 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => throw Boom.notFound(`Job with the id "${jobId}" not found`); } - const job: Job = jobsResponse.jobs[0]; + const job = jobsResponse.jobs[0]; const isModelPlotEnabled = job?.model_plot_config?.enabled; const isAnomalousOnly = (Object.entries(fieldsConfig) as Array<[string, FieldConfig]>).some( diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 3d1db66cc24cc..1996acd2cdb06 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -183,6 +183,7 @@ export function resultsServiceProvider(mlClient: MlClient) { anomalies: [], interval: 'second', }; + // @ts-expect-error update to correct search response if (body.hits.total.value > 0) { let records: AnomalyRecordDoc[] = []; body.hits.hits.forEach((hit: any) => { @@ -401,6 +402,7 @@ export function resultsServiceProvider(mlClient: MlClient) { ); const examplesByCategoryId: { [key: string]: any } = {}; + // @ts-expect-error update to correct search response if (body.hits.total.value > 0) { body.hits.hits.forEach((hit: any) => { if (maxExamples) { @@ -437,6 +439,7 @@ export function resultsServiceProvider(mlClient: MlClient) { ); const definition = { categoryId, terms: null, regex: null, examples: [] }; + // @ts-expect-error update to correct search response if (body.hits.total.value > 0) { const source = body.hits.hits[0]._source; definition.categoryId = source.category_id; @@ -576,6 +579,7 @@ export function resultsServiceProvider(mlClient: MlClient) { ); if (fieldToBucket === JOB_ID) { finalResults = { + // @ts-expect-error update search response jobs: results.aggregations?.unique_terms?.buckets.map( (b: { key: string; doc_count: number }) => b.key ), @@ -588,6 +592,7 @@ export function resultsServiceProvider(mlClient: MlClient) { }, {} ); + // @ts-expect-error update search response results.aggregations.jobs.buckets.forEach( (bucket: { key: string | number; unique_stopped_partitions: { buckets: any[] } }) => { jobs[bucket.key] = bucket.unique_stopped_partitions.buckets.map((b) => b.key); diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index aa7adbe2db660..ed583bd9e3eb1 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; -import { RequestParams } from '@elastic/elasticsearch'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -23,8 +23,6 @@ import { updateModelSnapshotSchema, } from './schemas/anomaly_detectors_schema'; -import { Job, JobStats } from '../../common/types/anomaly_detection_jobs'; - /** * Routes for the anomaly detectors */ @@ -49,7 +47,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, response }) => { try { - const { body } = await mlClient.getJobs<{ jobs: Job[] }>(); + const { body } = await mlClient.getJobs(); return response.ok({ body, }); @@ -81,7 +79,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { try { const { jobId } = request.params; - const { body } = await mlClient.getJobs<{ jobs: Job[] }>({ job_id: jobId }); + const { body } = await mlClient.getJobs({ job_id: jobId }); return response.ok({ body, }); @@ -111,7 +109,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, response }) => { try { - const { body } = await mlClient.getJobStats<{ jobs: JobStats[] }>(); + const { body } = await mlClient.getJobStats(); return response.ok({ body, }); @@ -283,7 +281,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { try { - const options: RequestParams.MlCloseJob = { + const options: estypes.CloseJobRequest = { job_id: request.params.jobId, }; const force = request.query.force; @@ -321,7 +319,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { try { - const options: RequestParams.MlDeleteJob = { + const options: estypes.DeleteJobRequest = { job_id: request.params.jobId, wait_for_completion: false, }; @@ -395,7 +393,9 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { const duration = request.body.duration; const { body } = await mlClient.forecast({ job_id: jobId, - duration, + body: { + duration, + }, }); return response.ok({ body, @@ -513,10 +513,12 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { try { const { body } = await mlClient.getOverallBuckets({ job_id: request.params.jobId, - top_n: request.body.topN, - bucket_span: request.body.bucketSpan, - start: request.body.start, - end: request.body.end, + body: { + top_n: request.body.topN, + bucket_span: request.body.bucketSpan, + start: request.body.start !== undefined ? String(request.body.start) : undefined, + end: request.body.end !== undefined ? String(request.body.end) : undefined, + }, }); return response.ok({ body, diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 448d798845e18..520f8ce6fb0a9 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -28,7 +28,6 @@ import { AnalyticsManager } from '../models/data_frame_analytics/analytics_manag import { validateAnalyticsJob } from '../models/data_frame_analytics/validation'; import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics'; import { getAuthorizationHeader } from '../lib/request_authorization'; -import { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics'; import type { MlClient } from '../lib/ml_client'; function getIndexPatternId(context: RequestHandlerContext, patternName: string) { @@ -603,14 +602,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout for (const id of analyticsIds) { try { const { body } = allSpaces - ? await client.asInternalUser.ml.getDataFrameAnalytics<{ - data_frame_analytics: DataFrameAnalyticsConfig[]; - }>({ + ? await client.asInternalUser.ml.getDataFrameAnalytics({ id, }) - : await mlClient.getDataFrameAnalytics<{ - data_frame_analytics: DataFrameAnalyticsConfig[]; - }>({ + : await mlClient.getDataFrameAnalytics({ id, }); results[id] = body.data_frame_analytics.length > 0; diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index 90d90a0e2b1e4..2013af3ee8735 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RequestParams } from '@elastic/elasticsearch'; +import { estypes } from '@elastic/elasticsearch'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -16,8 +16,6 @@ import { } from './schemas/datafeeds_schema'; import { getAuthorizationHeader } from '../lib/request_authorization'; -import { Datafeed, DatafeedStats } from '../../common/types/anomaly_detection_jobs'; - /** * Routes for datafeed service */ @@ -39,7 +37,7 @@ export function dataFeedRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, response }) => { try { - const { body } = await mlClient.getDatafeeds<{ datafeeds: Datafeed[] }>(); + const { body } = await mlClient.getDatafeeds(); return response.ok({ body, }); @@ -99,9 +97,7 @@ export function dataFeedRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, response }) => { try { - const { body } = await mlClient.getDatafeedStats<{ - datafeeds: DatafeedStats[]; - }>(); + const { body } = await mlClient.getDatafeedStats(); return response.ok({ body, }); @@ -251,7 +247,7 @@ export function dataFeedRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { try { - const options: RequestParams.MlDeleteDatafeed = { + const options: estypes.DeleteDatafeedRequest = { datafeed_id: request.params.datafeedId, }; const force = request.query.force; @@ -298,8 +294,10 @@ export function dataFeedRoutes({ router, routeGuard }: RouteInitialization) { const { body } = await mlClient.startDatafeed({ datafeed_id: datafeedId, - start, - end, + body: { + start: start !== undefined ? String(start) : undefined, + end: end !== undefined ? String(end) : undefined, + }, }); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index bfa7137b3e6d6..dbfc2195a12e1 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -13,7 +13,6 @@ import { optionalModelIdSchema, } from './schemas/inference_schema'; import { modelsProvider } from '../models/data_frame_analytics'; -import { InferenceConfigResponse } from '../../common/types/trained_models'; export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) { /** @@ -38,7 +37,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) try { const { modelId } = request.params; const { with_pipelines: withPipelines, ...query } = request.query; - const { body } = await mlClient.getTrainedModels({ + const { body } = await mlClient.getTrainedModels({ size: 1000, ...query, ...(modelId ? { model_id: modelId } : {}), @@ -85,7 +84,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { try { const { modelId } = request.params; const { body } = await mlClient.getTrainedModelsStats({ diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts index 04d5e53a5568b..6b24ef000b695 100644 --- a/x-pack/plugins/ml/server/saved_objects/checks.ts +++ b/x-pack/plugins/ml/server/saved_objects/checks.ts @@ -10,8 +10,6 @@ import { IScopedClusterClient, KibanaRequest } from 'kibana/server'; import type { JobSavedObjectService } from './service'; import { JobType, DeleteJobCheckResponse } from '../../common/types/saved_objects'; -import { Job } from '../../common/types/anomaly_detection_jobs'; -import { Datafeed } from '../../common/types/anomaly_detection_jobs'; import { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics'; import { ResolveMlCapabilities } from '../../common/types/capabilities'; @@ -51,13 +49,13 @@ export function checksFactory( const jobObjects = await jobSavedObjectService.getAllJobObjects(undefined, false); // load all non-space jobs and datafeeds - const { body: adJobs } = await client.asInternalUser.ml.getJobs<{ jobs: Job[] }>(); - const { body: datafeeds } = await client.asInternalUser.ml.getDatafeeds<{ - datafeeds: Datafeed[]; - }>(); - const { body: dfaJobs } = await client.asInternalUser.ml.getDataFrameAnalytics<{ - data_frame_analytics: DataFrameAnalyticsConfig[]; - }>(); + const { body: adJobs } = await client.asInternalUser.ml.getJobs(); + const { body: datafeeds } = await client.asInternalUser.ml.getDatafeeds(); + const { + body: dfaJobs, + } = ((await client.asInternalUser.ml.getDataFrameAnalytics()) as unknown) as { + body: { data_frame_analytics: DataFrameAnalyticsConfig[] }; + }; const savedObjectsStatus: JobSavedObjectStatus[] = jobObjects.map( ({ attributes, namespaces }) => { diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index 021bf5b12625b..1e3dcd7de5240 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -7,7 +7,6 @@ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { RequestParams } from '@elastic/elasticsearch'; import { MlLicense } from '../../../common/license'; import { CloudSetup } from '../../../../cloud/server'; import { spacesUtilsProvider } from '../../lib/spaces_utils'; @@ -24,10 +23,7 @@ export interface MlSystemProvider { ): { mlCapabilities(): Promise; mlInfo(): Promise; - mlAnomalySearch( - searchParams: RequestParams.Search, - jobIds: string[] - ): Promise>; + mlAnomalySearch(searchParams: any, jobIds: string[]): Promise>; }; } @@ -73,10 +69,7 @@ export function getMlSystemProvider( }; }); }, - async mlAnomalySearch( - searchParams: RequestParams.Search, - jobIds: string[] - ): Promise> { + async mlAnomalySearch(searchParams: any, jobIds: string[]): Promise> { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canAccessML']) diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts index ecfb5fc50a16d..603c66d2d05f2 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -25,7 +26,7 @@ describe('fetchAvailableCcs', () => { elasticsearchClientMock.createSuccessTransportRequestPromise({ [connectedRemote]: { connected: true, - }, + } as estypes.RemoteInfo, }) ); @@ -40,7 +41,7 @@ describe('fetchAvailableCcs', () => { elasticsearchClientMock.createSuccessTransportRequestPromise({ [disconnectedRemote]: { connected: false, - }, + } as estypes.RemoteInfo, }) ); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 330be4e90ed56..fb67cd1805950 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -69,8 +69,8 @@ export async function fetchCCRReadExceptions( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -95,6 +95,7 @@ export async function fetchCCRReadExceptions( const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; + // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets const { buckets: remoteClusterBuckets = [] } = response.aggregations.remote_clusters; if (!remoteClusterBuckets.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts index d326c7f4bedda..08ecaef33085b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchClusterHealth } from './fetch_cluster_health'; @@ -29,7 +30,7 @@ describe('fetchClusterHealth', () => { }, ], }, - }) + } as estypes.SearchResponse) ); const clusters = [{ clusterUuid, clusterName: 'foo' }]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index be91aaa6ec983..ddbf4e3d4b3c1 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -25,8 +25,8 @@ export async function fetchClusterHealth( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -59,11 +59,11 @@ export async function fetchClusterHealth( }, }; - const { body: response } = await esClient.search(params); - return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + const { body: response } = await esClient.search(params); + return response.hits.hits.map((hit) => { return { - health: hit._source.cluster_state?.status, - clusterUuid: hit._source.cluster_uuid, + health: hit._source!.cluster_state?.status, + clusterUuid: hit._source!.cluster_uuid, ccs: hit._index.includes(':') ? hit._index.split(':')[0] : undefined, } as AlertClusterHealth; }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts index 54aa2e68d4ef2..75991e892d419 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -28,7 +29,7 @@ describe('fetchClusters', () => { }, ], }, - }) + } as estypes.SearchResponse) ); const index = '.monitoring-es-*'; const result = await fetchClusters(esClient, index); @@ -57,7 +58,7 @@ describe('fetchClusters', () => { }, ], }, - }) + } as estypes.SearchResponse) ); const index = '.monitoring-es-*'; const result = await fetchClusters(esClient, index); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 2ff9ae3854e4a..0fb9dd5298e9e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchCpuUsageNodeStats } from './fetch_cpu_usage_node_stats'; @@ -24,6 +25,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -77,6 +79,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch container stats', async () => { esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -143,6 +146,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch properly return ccs', async () => { esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -193,7 +197,9 @@ describe('fetchCpuUsageNodeStats', () => { let params = null; esClient.search.mockImplementation((...args) => { params = args[0]; - return elasticsearchClientMock.createSuccessTransportRequestPromise({}); + return elasticsearchClientMock.createSuccessTransportRequestPromise( + {} as estypes.SearchResponse + ); }); await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); expect(params).toStrictEqual({ diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 1dfbe381b9956..07ca3572ad6b3 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -120,14 +120,14 @@ export async function fetchCpuUsageNodeStats( usage_deriv: { derivative: { buckets_path: 'average_usage', - gap_policy: 'skip', + gap_policy: 'skip' as const, unit: NORMALIZED_DERIVATIVE_UNIT, }, }, periods_deriv: { derivative: { buckets_path: 'average_periods', - gap_policy: 'skip', + gap_policy: 'skip' as const, unit: NORMALIZED_DERIVATIVE_UNIT, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts index 7664d73f6009b..8faf79fc4b59c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts @@ -25,6 +25,7 @@ describe('fetchDiskUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index aea4ede825d67..30daee225fcb4 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -101,7 +101,8 @@ export async function fetchDiskUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; - const { buckets: clusterBuckets = [] } = response.aggregations.clusters; + // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + const { buckets: clusterBuckets = [] } = response.aggregations!.clusters; if (!clusterBuckets.length) { return stats; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts index be501ee3d5280..d105174853636 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -9,6 +9,7 @@ import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; +import { estypes } from '@elastic/elasticsearch'; describe('fetchElasticsearchVersions', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; @@ -41,7 +42,7 @@ describe('fetchElasticsearchVersions', () => { }, ], }, - }) + } as estypes.SearchResponse) ); const result = await fetchElasticsearchVersions(esClient, clusters, index, size); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index b4b7739f6731b..111ef5b0c120d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -26,8 +26,8 @@ export async function fetchElasticsearchVersions( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -60,13 +60,13 @@ export async function fetchElasticsearchVersions( }, }; - const { body: response } = await esClient.search(params); - return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { - const versions = hit._source.cluster_stats?.nodes?.versions; + const { body: response } = await esClient.search(params); + return response.hits.hits.map((hit) => { + const versions = hit._source!.cluster_stats?.nodes?.versions ?? []; return { versions, - clusterUuid: hit._source.cluster_uuid, - ccs: hit._index.includes(':') ? hit._index.split(':')[0] : null, + clusterUuid: hit._source!.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : undefined, }; }); } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index dfba0c42eef3d..f51e1cde47f8d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -88,8 +88,8 @@ export async function fetchIndexShardSize( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -116,6 +116,7 @@ export async function fetchIndexShardSize( const { body: response } = await esClient.search(params); const stats: IndexShardSizeStats[] = []; + // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets const { buckets: clusterBuckets = [] } = response.aggregations.clusters; const validIndexPatterns = memoizedIndexPatterns(shardIndexPatterns); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index 901851d766512..2b966b16f2f5c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -23,6 +23,7 @@ describe('fetchKibanaVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index a4e1e606702ec..cb2f201e2586e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -70,7 +70,7 @@ export async function fetchKibanaVersions( field: 'kibana_stats.kibana.version', size: 1, order: { - latest_report: 'desc', + latest_report: 'desc' as const, }, }, aggs: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts index 69a42812bfe88..3c12c70bf1713 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -8,6 +8,7 @@ import { fetchLicenses } from './fetch_licenses'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { estypes } from '@elastic/elasticsearch'; describe('fetchLicenses', () => { const clusterName = 'MyCluster'; @@ -32,7 +33,7 @@ describe('fetchLicenses', () => { }, ], }, - }) + } as estypes.SearchResponse) ); const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 5cd4378f0a747..5178b6c4c53a7 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; -import { ElasticsearchResponse } from '../../../common/types/es'; +import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchLicenses( esClient: ElasticsearchClient, @@ -25,8 +25,8 @@ export async function fetchLicenses( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -59,15 +59,15 @@ export async function fetchLicenses( }, }; - const { body: response } = await esClient.search(params); + const { body: response } = await esClient.search(params); return ( response?.hits?.hits.map((hit) => { - const rawLicense = hit._source.license ?? {}; + const rawLicense = hit._source!.license ?? {}; const license: AlertLicense = { status: rawLicense.status ?? '', type: rawLicense.type ?? '', expiryDateMS: rawLicense.expiry_date_in_millis ?? 0, - clusterUuid: hit._source.cluster_uuid, + clusterUuid: hit._source!.cluster_uuid, ccs: hit._index, }; return license; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index e35de6e68866d..d7d4e6531f58e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -23,6 +23,7 @@ describe('fetchLogstashVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 6090ba36d9749..6fb54857d40e4 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -70,7 +70,7 @@ export async function fetchLogstashVersions( field: 'logstash_stats.logstash.version', size: 1, order: { - latest_report: 'desc', + latest_report: 'desc' as const, }, }, aggs: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 77c17a8ebf3ef..aad4638bf8359 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -94,6 +94,7 @@ export async function fetchMemoryUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; + // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets const { buckets: clusterBuckets = [] } = response.aggregations.clusters; if (!clusterBuckets.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index 2388abf024eb9..c8d15acf8ff73 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -56,6 +56,7 @@ describe('fetchMissingMonitoringData', () => { ]; esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -114,6 +115,7 @@ describe('fetchMissingMonitoringData', () => { }, ]; esClient.search.mockReturnValue( + // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index cb274848e6c5a..a7b4a3a023207 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -99,8 +99,8 @@ export async function fetchMissingMonitoringData( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index a97594c8ca995..ff3a8d4aa7ef8 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -36,8 +36,8 @@ export async function fetchNodesFromClusterStats( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -71,8 +71,8 @@ export async function fetchNodesFromClusterStats( sort: [ { timestamp: { - order: 'desc', - unmapped_type: 'long', + order: 'desc' as const, + unmapped_type: 'long' as const, }, }, ], @@ -90,6 +90,7 @@ export async function fetchNodesFromClusterStats( const { body: response } = await esClient.search(params); const nodes = []; + // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets const clusterBuckets = response.aggregations.clusters.buckets; for (const clusterBucket of clusterBuckets) { const clusterUuid = clusterBucket.key; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 5770721195e14..b63244dab719d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -13,13 +13,13 @@ const invalidNumberValue = (value: number) => { return isNaN(value) || value === undefined || value === null; }; -const getTopHits = (threadType: string, order: string) => ({ +const getTopHits = (threadType: string, order: 'asc' | 'desc') => ({ top_hits: { sort: [ { timestamp: { order, - unmapped_type: 'long', + unmapped_type: 'long' as const, }, }, ], @@ -81,10 +81,10 @@ export async function fetchThreadPoolRejectionStats( }, aggs: { most_recent: { - ...getTopHits(threadType, 'desc'), + ...getTopHits(threadType, 'desc' as const), }, least_recent: { - ...getTopHits(threadType, 'asc'), + ...getTopHits(threadType, 'asc' as const), }, }, }, @@ -96,6 +96,7 @@ export async function fetchThreadPoolRejectionStats( const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; + // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets const { buckets: clusterBuckets = [] } = response.aggregations.clusters; if (!clusterBuckets.length) { diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index e118d17e17c3f..2676e40a4902f 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -8,7 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; -import { createOrUpdateIndex, MappingsDefinition } from './utils/create_or_update_index'; +import { createOrUpdateIndex, Mappings } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; import { unwrapEsResponse, WrappedElasticsearchClientError } from './utils/unwrap_es_response'; @@ -29,7 +29,7 @@ export const plugin = (initContext: PluginInitializerContext) => export { createOrUpdateIndex, - MappingsDefinition, + Mappings, ObservabilityPluginSetup, ScopedAnnotationsClient, unwrapEsResponse, diff --git a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts index 7abd68cb9f16c..39a594dcc86ca 100644 --- a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts +++ b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts @@ -23,27 +23,6 @@ type CreateParams = t.TypeOf; type DeleteParams = t.TypeOf; type GetByIdParams = t.TypeOf; -interface IndexDocumentResponse { - _shards: { - total: number; - failed: number; - successful: number; - }; - _index: string; - _type: string; - _id: string; - _version: number; - _seq_no: number; - _primary_term: number; - result: string; -} - -export interface GetResponse { - _id: string; - _index: string; - _source: Annotation; -} - export function createAnnotationsClient(params: { index: string; esClient: ElasticsearchClient; @@ -95,7 +74,7 @@ export function createAnnotationsClient(params: { }; const body = await unwrapEsResponse( - esClient.index({ + esClient.index({ index, body: annotation, refresh: 'wait_for', @@ -103,18 +82,18 @@ export function createAnnotationsClient(params: { ); return ( - await esClient.get({ + await esClient.get({ index, id: body._id, }) - ).body; + ).body as { _id: string; _index: string; _source: Annotation }; } ), getById: ensureGoldLicense(async (getByIdParams: GetByIdParams) => { const { id } = getByIdParams; return unwrapEsResponse( - esClient.get({ + esClient.get({ id, index, }) diff --git a/x-pack/plugins/observability/server/lib/annotations/mappings.ts b/x-pack/plugins/observability/server/lib/annotations/mappings.ts index 3313c411b5889..da72afdbecb33 100644 --- a/x-pack/plugins/observability/server/lib/annotations/mappings.ts +++ b/x-pack/plugins/observability/server/lib/annotations/mappings.ts @@ -6,7 +6,7 @@ */ export const mappings = { - dynamic: 'strict', + dynamic: 'strict' as const, properties: { annotation: { properties: { @@ -45,4 +45,4 @@ export const mappings = { }, }, }, -} as const; +}; diff --git a/x-pack/plugins/observability/server/utils/create_or_update_index.ts b/x-pack/plugins/observability/server/utils/create_or_update_index.ts index cc6504fd4d4fd..19b14ef8b2437 100644 --- a/x-pack/plugins/observability/server/utils/create_or_update_index.ts +++ b/x-pack/plugins/observability/server/utils/create_or_update_index.ts @@ -4,24 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { CreateIndexRequest, PutMappingRequest } from '@elastic/elasticsearch/api/types'; import pRetry from 'p-retry'; import { Logger, ElasticsearchClient } from 'src/core/server'; -export interface MappingsObject { - type: string; - ignore_above?: number; - scaling_factor?: number; - ignore_malformed?: boolean; - coerce?: boolean; - fields?: Record; -} - -export interface MappingsDefinition { - dynamic?: boolean | 'strict'; - properties: Record; - dynamic_templates?: any[]; -} +export type Mappings = Required['body']['mappings'] & + Required['body']; export async function createOrUpdateIndex({ index, @@ -30,7 +18,7 @@ export async function createOrUpdateIndex({ logger, }: { index: string; - mappings: MappingsDefinition; + mappings: Mappings; client: ElasticsearchClient; logger: Logger; }) { @@ -59,7 +47,8 @@ export async function createOrUpdateIndex({ }); if (!result.body.acknowledged) { - const resultError = result && result.body.error && JSON.stringify(result.body.error); + const bodyWithError: { body?: { error: any } } = result as any; + const resultError = JSON.stringify(bodyWithError?.body?.error); throw new Error(resultError); } }, @@ -82,9 +71,9 @@ function createNewIndex({ }: { index: string; client: ElasticsearchClient; - mappings: MappingsDefinition; + mappings: Required['body']['mappings']; }) { - return client.indices.create<{ acknowledged: boolean; error: any }>({ + return client.indices.create({ index, body: { // auto_expand_replicas: Allows cluster to not have replicas for this index @@ -101,9 +90,9 @@ function updateExistingIndex({ }: { index: string; client: ElasticsearchClient; - mappings: MappingsDefinition; + mappings: PutMappingRequest['body']; }) { - return client.indices.putMapping<{ acknowledged: boolean; error: any }>({ + return client.indices.putMapping({ index, body: mappings, }); diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts index 561866d5077a6..b24e4f28d89f1 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; import { Inspect, Maybe, PageInfoPaginated } from '../../common'; import { RequestOptions, RequestOptionsPaginated } from '../..'; -export type ActionEdges = SearchResponse['hits']['hits']; +export type ActionEdges = estypes.SearchResponse['hits']['hits']; -export type ActionResultEdges = SearchResponse['hits']['hits']; +export type ActionResultEdges = estypes.SearchResponse['hits']['hits']; export interface ActionsStrategyResponse extends IEsSearchResponse { edges: ActionEdges; totalCount: number; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts index add8598bb77ad..035774aaffe36 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import { estypes } from '@elastic/elasticsearch'; import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; import { Inspect, Maybe, PageInfoPaginated } from '../../common'; import { RequestOptionsPaginated } from '../..'; -export type ResultEdges = SearchResponse['hits']['hits']; +export type ResultEdges = estypes.SearchResponse['hits']['hits']; export interface ResultsStrategyResponse extends IEsSearchResponse { edges: ResultEdges; diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx index 1dd5b63eedc23..1880cec0ae8e2 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -107,7 +107,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'status') { // eslint-disable-next-line react-hooks/rules-of-hooks const linkProps = useRouterNavigate( - `/live_query/${actionId}/results/${value.fields.agent_id[0]}` + `/live_query/${actionId}/results/${value.fields?.agent_id[0]}` ); return ( @@ -122,7 +122,7 @@ const ActionResultsTableComponent: React.FC = ({ action // eslint-disable-next-line react-hooks/rules-of-hooks const { data: allResultsData } = useAllResults({ actionId, - agentId: value.fields.agent_id[0], + agentId: value.fields?.agent_id[0], activePage: pagination.pageIndex, limit: pagination.pageSize, direction: Direction.asc, @@ -133,7 +133,7 @@ const ActionResultsTableComponent: React.FC = ({ action } if (columnId === 'agent_status') { - const agentIdValue = value.fields.agent_id[0]; + const agentIdValue = value.fields?.agent_id[0]; // @ts-expect-error update types const agent = find(['_id', agentIdValue], agentsData?.agents); const online = agent?.active; @@ -143,7 +143,7 @@ const ActionResultsTableComponent: React.FC = ({ action } if (columnId === 'agent') { - const agentIdValue = value.fields.agent_id[0]; + const agentIdValue = value.fields?.agent_id[0]; // @ts-expect-error update types const agent = find(['_id', agentIdValue], agentsData?.agents); const agentName = agent?.local_metadata.host.name; @@ -156,6 +156,7 @@ const ActionResultsTableComponent: React.FC = ({ action } if (columnId === '@timestamp') { + // @ts-expect-error fields is optional return value.fields['@timestamp']; } diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx index 986b46b1a4089..ca85693849651 100644 --- a/x-pack/plugins/osquery/public/actions/actions_table.tsx +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -62,6 +62,7 @@ const ActionsTableComponent = () => { () => ({ rowIndex, columnId }) => { // eslint-disable-next-line react-hooks/rules-of-hooks const data = useContext(DataContext); + // @ts-expect-error fields is optional const value = data[rowIndex].fields[columnId]; if (columnId === 'action_id') { diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 4c2048148f745..7557828c4407c 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -68,9 +68,11 @@ const ResultsTableComponent: React.FC = ({ actionId, // eslint-disable-next-line react-hooks/rules-of-hooks const data = useContext(DataContext); + // @ts-expect-error fields is optional const value = data[rowIndex].fields[columnId]; if (columnId === 'agent.name') { + // @ts-expect-error fields is optional const agentIdValue = data[rowIndex].fields['agent.id']; // eslint-disable-next-line react-hooks/rules-of-hooks const linkProps = useRouterNavigate(`/live_query/${actionId}/results/${agentIdValue}`); diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts index bc0af9a1d0dcc..e05bd15bcc722 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts @@ -36,6 +36,7 @@ export const allActions: OsqueryFactory = { ...response, inspect, edges: response.rawResponse.hits.hits, + // @ts-expect-error doesn't handle case when total TotalHits totalCount: response.rawResponse.hits.total, pageInfo: { activePage: activePage ?? 0, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts index dbcc03006399a..50fcf938bdd79 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts @@ -37,6 +37,7 @@ export const actionResults: OsqueryFactory = { ...response, inspect, edges: response.rawResponse.hits.hits, + // @ts-expect-error doesn't handle case when total TotalHits totalCount: response.rawResponse.hits.total, pageInfo: { activePage: activePage ?? 0, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts index 1f7fbccb68682..7e0532dfec1af 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts @@ -37,7 +37,11 @@ export const allAgents: OsqueryFactory = { return { ...response, inspect, - edges: response.rawResponse.hits.hits.map((hit) => ({ _id: hit._id, ...hit._source })), + edges: response.rawResponse.hits.hits.map((hit) => ({ + _id: hit._id, + ...hit._source, + })) as Agent[], + // @ts-expect-error doesn't handle case when total TotalHits totalCount: response.rawResponse.hits.total, pageInfo: { activePage: activePage ?? 0, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts index 7b9a24e4a0653..93cba882e39ed 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts @@ -36,6 +36,7 @@ export const allResults: OsqueryFactory = { ...response, inspect, edges: response.rawResponse.hits.hits, + // @ts-expect-error doesn't handle case when total TotalHits totalCount: response.rawResponse.hits.total, pageInfo: { activePage: activePage ?? 0, diff --git a/x-pack/plugins/painless_lab/server/routes/api/execute.ts b/x-pack/plugins/painless_lab/server/routes/api/execute.ts index e77f4fd4a05b5..0316809805200 100644 --- a/x-pack/plugins/painless_lab/server/routes/api/execute.ts +++ b/x-pack/plugins/painless_lab/server/routes/api/execute.ts @@ -27,6 +27,7 @@ export function registerExecuteRoute({ router, license }: RouteDependencies) { try { const client = ctx.core.elasticsearch.client.asCurrentUser; const response = await client.scriptsPainlessExecute({ + // @ts-expect-error `ExecutePainlessScriptRequest.body` does not allow `string` body, }); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 370fc42921acf..85c5379a63b7f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -91,7 +91,7 @@ export class CsvGenerator { }; const results = ( await this.clients.data.search(searchParams, { strategy: ES_SEARCH_STRATEGY }).toPromise() - ).rawResponse; + ).rawResponse as SearchResponse; return results; } diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 1eb1103336801..f07da188f3e62 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { get } from 'lodash'; -import { ElasticsearchClient, SearchResponse } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { ReportingConfig } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { GetLicense } from './'; @@ -19,7 +19,7 @@ import { KeyCountBucket, RangeStats, ReportingUsageType, - ReportingUsageSearchResponse, + // ReportingUsageSearchResponse, StatusByAppBucket, } from './types'; @@ -100,7 +100,8 @@ type RangeStatSets = Partial & { last7Days: Partial; }; -type ESResponse = Partial>; +// & ReportingUsageSearchResponse +type ESResponse = Partial; async function handleResponse(response: ESResponse): Promise> { const buckets = get(response, 'aggregations.ranges.buckets'); diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts b/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts index 3e745458f6607..06230172d52bd 100644 --- a/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts +++ b/x-pack/plugins/saved_objects_tagging/server/usage/fetch_tag_usage_data.ts @@ -11,26 +11,22 @@ import { TaggingUsageData, ByTypeTaggingUsageData } from './types'; /** * Manual type reflection of the `tagDataAggregations` resulting payload */ -interface AggregatedTagUsageResponseBody { - aggregations: { - by_type: { - buckets: Array<{ - key: string; +interface AggregatedTagUsage { + buckets: Array<{ + key: string; + doc_count: number; + nested_ref: { + tag_references: { doc_count: number; - nested_ref: { - tag_references: { + tag_id: { + buckets: Array<{ + key: string; doc_count: number; - tag_id: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; - }; + }>; }; - }>; + }; }; - }; + }>; } export const fetchTagUsageData = async ({ @@ -40,7 +36,7 @@ export const fetchTagUsageData = async ({ esClient: ElasticsearchClient; kibanaIndex: string; }): Promise => { - const { body } = await esClient.search({ + const { body } = await esClient.search({ index: [kibanaIndex], ignore_unavailable: true, filter_path: 'aggregations', @@ -59,7 +55,7 @@ export const fetchTagUsageData = async ({ const allUsedTags = new Set(); let totalTaggedObjects = 0; - const typeBuckets = body.aggregations.by_type.buckets; + const typeBuckets = (body.aggregations!.by_type as AggregatedTagUsage).buckets; typeBuckets.forEach((bucket) => { const type = bucket.key; const taggedDocCount = bucket.doc_count; diff --git a/x-pack/plugins/searchprofiler/server/routes/profile.ts b/x-pack/plugins/searchprofiler/server/routes/profile.ts index cdc420667f9e1..cbe0b75bc9eda 100644 --- a/x-pack/plugins/searchprofiler/server/routes/profile.ts +++ b/x-pack/plugins/searchprofiler/server/routes/profile.ts @@ -33,16 +33,15 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = body: { query, index }, } = request; - const parsed = { - // Activate profiler mode for this query. - profile: true, - ...query, - }; - const body = { index, - body: JSON.stringify(parsed, null, 2), + body: { + // Activate profiler mode for this query. + profile: true, + ...query, + }, }; + try { const client = ctx.core.elasticsearch.client.asCurrentUser; const resp = await client.search(body); diff --git a/x-pack/plugins/security/common/model/authenticated_user.mock.ts b/x-pack/plugins/security/common/model/authenticated_user.mock.ts index 3715245b37e61..6dad3886401ac 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.mock.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.mock.ts @@ -7,7 +7,10 @@ import type { AuthenticatedUser } from './authenticated_user'; -export function mockAuthenticatedUser(user: Partial = {}) { +// We omit `roles` here since the original interface defines this field as `readonly string[]` that makes it hard to use +// in various mocks that expect mutable string array. +type AuthenticatedUserProps = Partial & { roles: string[] }>; +export function mockAuthenticatedUser(user: AuthenticatedUserProps = {}) { return { username: 'user', email: 'email', @@ -18,6 +21,7 @@ export function mockAuthenticatedUser(user: Partial = {}) { lookup_realm: { name: 'native1', type: 'native' }, authentication_provider: { type: 'basic', name: 'basic1' }, authentication_type: 'realm', + metadata: { _reserved: false }, ...user, }; } diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts index dda3a15903696..a1b9671ab6bd7 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts @@ -64,7 +64,14 @@ describe('API Keys', () => { it('returns true when the operation completes without error', async () => { mockLicense.isEnabled.mockReturnValue(true); mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValue( - securityMock.createApiResponse({ body: {} }) + securityMock.createApiResponse({ + body: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + error_details: [], + }, + }) ); const result = await apiKeys.areAPIKeysEnabled(); expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledTimes(1); @@ -105,7 +112,14 @@ describe('API Keys', () => { it('calls `invalidateApiKey` with proper parameters', async () => { mockLicense.isEnabled.mockReturnValue(true); mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValueOnce( - securityMock.createApiResponse({ body: {} }) + securityMock.createApiResponse({ + body: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + error_details: [], + }, + }) ); const result = await apiKeys.areAPIKeysEnabled(); @@ -133,6 +147,7 @@ describe('API Keys', () => { mockLicense.isEnabled.mockReturnValue(true); mockScopedClusterClient.asCurrentUser.security.createApiKey.mockResolvedValueOnce( + // @ts-expect-error @elastic/elsticsearch CreateApiKeyResponse.expiration: number securityMock.createApiResponse({ body: { id: '123', @@ -302,6 +317,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }, }) ); @@ -312,6 +328,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }); expect(mockScopedClusterClient.asCurrentUser.security.invalidateApiKey).toHaveBeenCalledWith({ body: { @@ -328,6 +345,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }, }) ); @@ -339,6 +357,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }); expect(mockScopedClusterClient.asCurrentUser.security.invalidateApiKey).toHaveBeenCalledWith({ body: { @@ -364,6 +383,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }, }) ); @@ -372,6 +392,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }); expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledWith({ body: { @@ -388,6 +409,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }, }) ); @@ -399,6 +421,7 @@ describe('API Keys', () => { invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], error_count: 0, + error_details: [], }); expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledWith({ body: { diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index bdf549095c3c0..7396519acf9ea 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -109,7 +109,7 @@ export interface InvalidateAPIKeyResult { error_details?: Array<{ type: string; reason: string; - caused_by: { + caused_by?: { type: string; reason: string; }; @@ -145,7 +145,11 @@ export class APIKeys { ); try { - await this.clusterClient.asInternalUser.security.invalidateApiKey({ body: { ids: [id] } }); + await this.clusterClient.asInternalUser.security.invalidateApiKey({ + body: { + ids: [id], + }, + }); return true; } catch (e) { if (this.doesErrorIndicateAPIKeysAreDisabled(e)) { @@ -171,12 +175,12 @@ export class APIKeys { this.logger.debug('Trying to create an API key'); // User needs `manage_api_key` privilege to use this API - let result; + let result: CreateAPIKeyResult; try { result = ( await this.clusterClient .asScoped(request) - .asCurrentUser.security.createApiKey({ body: params }) + .asCurrentUser.security.createApiKey({ body: params }) ).body; this.logger.debug('API key was created successfully'); } catch (e) { @@ -207,10 +211,11 @@ export class APIKeys { const params = this.getGrantParams(createParams, authorizationHeader); // User needs `manage_api_key` or `grant_api_key` privilege to use this API - let result; + let result: GrantAPIKeyResult; try { result = ( - await this.clusterClient.asInternalUser.security.grantApiKey({ + await this.clusterClient.asInternalUser.security.grantApiKey({ + // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors body: params, }) ).body; @@ -235,15 +240,15 @@ export class APIKeys { this.logger.debug(`Trying to invalidate ${params.ids.length} an API key as current user`); - let result; + let result: InvalidateAPIKeyResult; try { // User needs `manage_api_key` privilege to use this API result = ( - await this.clusterClient - .asScoped(request) - .asCurrentUser.security.invalidateApiKey({ - body: { ids: params.ids }, - }) + await this.clusterClient.asScoped(request).asCurrentUser.security.invalidateApiKey({ + body: { + ids: params.ids, + }, + }) ).body; this.logger.debug( `API keys by ids=[${params.ids.join(', ')}] was invalidated successfully as current user` @@ -271,12 +276,14 @@ export class APIKeys { this.logger.debug(`Trying to invalidate ${params.ids.length} API keys`); - let result; + let result: InvalidateAPIKeyResult; try { // Internal user needs `cluster:admin/xpack/security/api_key/invalidate` privilege to use this API result = ( - await this.clusterClient.asInternalUser.security.invalidateApiKey({ - body: { ids: params.ids }, + await this.clusterClient.asInternalUser.security.invalidateApiKey({ + body: { + ids: params.ids, + }, }) ).body; this.logger.debug(`API keys by ids=[${params.ids.join(', ')}] was invalidated successfully`); diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index 20946ff6f5e80..18d567a143fee 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -113,10 +113,11 @@ export abstract class BaseAuthenticationProvider { */ protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) { return this.authenticationInfoToAuthenticatedUser( + // @ts-expect-error @elastic/elasticsearch `AuthenticateResponse` type doesn't define `authentication_type` and `enabled`. ( await this.options.client .asScoped({ headers: { ...request.headers, ...authHeaders } }) - .asCurrentUser.security.authenticate() + .asCurrentUser.security.authenticate() ).body ); } diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index 1bcb845ca5c08..f5c02953cebd3 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -46,7 +46,7 @@ describe('KerberosAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.asCurrentUser.security.authenticate.mockResolvedValue( - securityMock.createApiResponse({ body: {} }) + securityMock.createApiResponse({ body: mockAuthenticatedUser() }) ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -122,6 +122,7 @@ describe('KerberosAuthenticationProvider', () => { }); mockOptions.client.asInternalUser.security.getToken.mockResolvedValue( + // @ts-expect-error not full interface securityMock.createApiResponse({ body: { access_token: 'some-token', @@ -156,6 +157,7 @@ describe('KerberosAuthenticationProvider', () => { }); mockOptions.client.asInternalUser.security.getToken.mockResolvedValue( + // @ts-expect-error not full interface securityMock.createApiResponse({ body: { access_token: 'some-token', diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index 0de8e8e10a630..75dc2a8f47969 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -155,9 +155,13 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { authentication: AuthenticationInfo; }; try { + // @ts-expect-error authentication.email can be optional tokens = ( - await this.options.client.asInternalUser.security.getToken({ - body: { grant_type: '_kerberos', kerberos_ticket: kerberosTicket }, + await this.options.client.asInternalUser.security.getToken({ + body: { + grant_type: '_kerberos', + kerberos_ticket: kerberosTicket, + }, }) ).body; } catch (err) { diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index f35545e5e5f3a..ebeca42682eb9 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -11,7 +11,6 @@ import Boom from '@hapi/boom'; import type { KibanaRequest } from 'src/core/server'; import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticatedUser } from '../../../common/model'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { securityMock } from '../../mocks'; import { AuthenticationResult } from '../authentication_result'; @@ -24,7 +23,7 @@ import { OIDCAuthenticationProvider, OIDCLogin } from './oidc'; describe('OIDCAuthenticationProvider', () => { let provider: OIDCAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; - let mockUser: AuthenticatedUser; + let mockUser: ReturnType; let mockScopedClusterClient: ReturnType< typeof elasticsearchServiceMock.createScopedClusterClient >; diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 50d9ab33fd96f..bd51a0f815329 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -10,7 +10,6 @@ import Boom from '@hapi/boom'; import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticatedUser } from '../../../common/model'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { securityMock } from '../../mocks'; import { AuthenticationResult } from '../authentication_result'; @@ -22,7 +21,7 @@ import { SAMLAuthenticationProvider, SAMLLogin } from './saml'; describe('SAMLAuthenticationProvider', () => { let provider: SAMLAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; - let mockUser: AuthenticatedUser; + let mockUser: ReturnType; let mockScopedClusterClient: ReturnType< typeof elasticsearchServiceMock.createScopedClusterClient >; diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 4d80250607121..84a1649540267 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -50,6 +50,7 @@ describe('TokenAuthenticationProvider', () => { const authorization = `Bearer ${tokenPair.accessToken}`; mockOptions.client.asInternalUser.security.getToken.mockResolvedValue( + // @ts-expect-error not full interface securityMock.createApiResponse({ body: { access_token: tokenPair.accessToken, diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index f202a0bd43fcf..43338a8f6400f 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -72,16 +72,21 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { refresh_token: refreshToken, authentication: authenticationInfo, } = ( - await this.options.client.asInternalUser.security.getToken<{ - access_token: string; - refresh_token: string; - authentication: AuthenticationInfo; - }>({ body: { grant_type: 'password', username, password } }) + await this.options.client.asInternalUser.security.getToken({ + body: { + grant_type: 'password', + username, + password, + }, + }) ).body; this.logger.debug('Get token API request to Elasticsearch successful'); return AuthenticationResult.succeeded( - this.authenticationInfoToAuthenticatedUser(authenticationInfo), + this.authenticationInfoToAuthenticatedUser( + // @ts-expect-error @elastic/elasticsearch GetUserAccessTokenResponse declares authentication: string, but expected AuthenticatedUser + authenticationInfo as AuthenticationInfo + ), { authHeaders: { authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(), diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index a6d52e355b145..d02b368635e6f 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -109,6 +109,9 @@ describe('Tokens', () => { access_token: tokenPair.accessToken, refresh_token: tokenPair.refreshToken, authentication: authenticationInfo, + type: 'Bearer', + expires_in: 1200, + scope: 'FULL', }, }) ); @@ -197,7 +200,14 @@ describe('Tokens', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; mockElasticsearchClient.security.invalidateToken.mockResolvedValue( - securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + securityMock.createApiResponse({ + body: { + invalidated_tokens: 1, + previously_invalidated_tokens: 0, + error_count: 0, + error_details: [], + }, + }) ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); @@ -215,7 +225,14 @@ describe('Tokens', () => { const tokenPair = { accessToken: 'foo' }; mockElasticsearchClient.security.invalidateToken.mockResolvedValue( - securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + securityMock.createApiResponse({ + body: { + invalidated_tokens: 1, + previously_invalidated_tokens: 0, + error_count: 0, + error_details: [], + }, + }) ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); @@ -230,7 +247,14 @@ describe('Tokens', () => { const tokenPair = { refreshToken: 'foo' }; mockElasticsearchClient.security.invalidateToken.mockResolvedValue( - securityMock.createApiResponse({ body: { invalidated_tokens: 1 } }) + securityMock.createApiResponse({ + body: { + invalidated_tokens: 1, + previously_invalidated_tokens: 0, + error_count: 0, + error_details: [], + }, + }) ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); @@ -274,7 +298,14 @@ describe('Tokens', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; mockElasticsearchClient.security.invalidateToken.mockResolvedValue( - securityMock.createApiResponse({ body: { invalidated_tokens: 5 } }) + securityMock.createApiResponse({ + body: { + invalidated_tokens: 5, + previously_invalidated_tokens: 0, + error_count: 0, + error_details: [], + }, + }) ); await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index e3c4644775613..8f6dd9275e59c 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -60,16 +60,22 @@ export class Tokens { refresh_token: refreshToken, authentication: authenticationInfo, } = ( - await this.options.client.security.getToken<{ - access_token: string; - refresh_token: string; - authentication: AuthenticationInfo; - }>({ body: { grant_type: 'refresh_token', refresh_token: existingRefreshToken } }) + await this.options.client.security.getToken({ + body: { + grant_type: 'refresh_token', + refresh_token: existingRefreshToken, + }, + }) ).body; this.logger.debug('Access token has been successfully refreshed.'); - return { accessToken, refreshToken, authenticationInfo }; + return { + accessToken, + refreshToken, + // @ts-expect-error @elastic/elasticsearch decalred GetUserAccessTokenResponse.authentication: string + authenticationInfo: authenticationInfo as AuthenticationInfo, + }; } catch (err) { this.logger.debug(`Failed to refresh access token: ${err.message}`); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 2c1deed0f8c30..75c8229bb37d6 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -68,8 +68,8 @@ describe('#atSpace', () => { const expectedIndexPrivilegePayload = Object.entries( options.elasticsearchPrivileges?.index ?? {} - ).map(([names, indexPrivileges]) => ({ - names, + ).map(([name, indexPrivileges]) => ({ + names: [name], privileges: indexPrivileges, })); @@ -78,7 +78,7 @@ describe('#atSpace', () => { body: { cluster: options.elasticsearchPrivileges?.cluster, index: expectedIndexPrivilegePayload, - applications: [ + application: [ { application, resources: [`space:${options.spaceId}`], @@ -914,8 +914,8 @@ describe('#atSpaces', () => { const expectedIndexPrivilegePayload = Object.entries( options.elasticsearchPrivileges?.index ?? {} - ).map(([names, indexPrivileges]) => ({ - names, + ).map(([name, indexPrivileges]) => ({ + names: [name], privileges: indexPrivileges, })); @@ -924,7 +924,7 @@ describe('#atSpaces', () => { body: { cluster: options.elasticsearchPrivileges?.cluster, index: expectedIndexPrivilegePayload, - applications: [ + application: [ { application, resources: options.spaceIds.map((spaceId) => `space:${spaceId}`), @@ -2118,8 +2118,8 @@ describe('#globally', () => { const expectedIndexPrivilegePayload = Object.entries( options.elasticsearchPrivileges?.index ?? {} - ).map(([names, indexPrivileges]) => ({ - names, + ).map(([name, indexPrivileges]) => ({ + names: [name], privileges: indexPrivileges, })); @@ -2128,7 +2128,7 @@ describe('#globally', () => { body: { cluster: options.elasticsearchPrivileges?.cluster, index: expectedIndexPrivilegePayload, - applications: [ + application: [ { application, resources: [GLOBAL_RESOURCE], diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 0fc11cddf9bbc..3a35cf164ad85 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -51,22 +51,22 @@ export function checkPrivilegesWithRequestFactory( const allApplicationPrivileges = uniq([actions.version, actions.login, ...kibanaPrivileges]); const clusterClient = await getClusterClient(); - const { body: hasPrivilegesResponse } = await clusterClient - .asScoped(request) - .asCurrentUser.security.hasPrivileges({ - body: { - cluster: privileges.elasticsearch?.cluster, - index: Object.entries(privileges.elasticsearch?.index ?? {}).map( - ([names, indexPrivileges]) => ({ - names, - privileges: indexPrivileges, - }) - ), - applications: [ - { application: applicationName, resources, privileges: allApplicationPrivileges }, - ], - }, - }); + const { body } = await clusterClient.asScoped(request).asCurrentUser.security.hasPrivileges({ + body: { + cluster: privileges.elasticsearch?.cluster, + index: Object.entries(privileges.elasticsearch?.index ?? {}).map( + ([name, indexPrivileges]) => ({ + names: [name], + privileges: indexPrivileges, + }) + ), + application: [ + { application: applicationName, resources, privileges: allApplicationPrivileges }, + ], + }, + }); + + const hasPrivilegesResponse: HasPrivilegesResponse = body; validateEsPrivilegeResponse( hasPrivilegesResponse, diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index e2539695d54ee..e3a586062ae4c 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -6,7 +6,6 @@ */ import type { RouteDefinitionParams } from '../..'; -import type { BuiltinESPrivileges } from '../../../../common/model'; export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionParams) { router.get( @@ -14,7 +13,7 @@ export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionPara async (context, request, response) => { const { body: privileges, - } = await context.core.elasticsearch.client.asCurrentUser.security.getBuiltinPrivileges(); + } = await context.core.elasticsearch.client.asCurrentUser.security.getBuiltinPrivileges(); // Exclude the `none` privilege, as it doesn't make sense as an option within the Kibana UI privileges.cluster = privileges.cluster.filter((privilege) => privilege !== 'none'); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 3bfc5e5d4dda3..01d32f7fb8233 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -10,7 +10,6 @@ import { schema } from '@kbn/config-schema'; import type { RouteDefinitionParams } from '../..'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import type { ElasticsearchRole } from './model'; import { transformElasticsearchRoleToRole } from './model'; export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { @@ -25,14 +24,15 @@ export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { try { const { body: elasticsearchRoles, - } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< - Record - >({ name: request.params.name }); + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole({ + name: request.params.name, + }); const elasticsearchRole = elasticsearchRoles[request.params.name]; if (elasticsearchRole) { return response.ok({ body: transformElasticsearchRoleToRole( + // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. elasticsearchRole, request.params.name, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 2994afd40f880..4d458be4e332f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -26,7 +26,12 @@ export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams return response.ok({ body: Object.entries(elasticsearchRoles) .map(([roleName, elasticsearchRole]) => - transformElasticsearchRoleToRole(elasticsearchRole, roleName, authz.applicationName) + transformElasticsearchRoleToRole( + // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + elasticsearchRole, + roleName, + authz.applicationName + ) ) .sort((roleA, roleB) => { if (roleA.name < roleB.name) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts index f033b66805067..74a035cdd0cb6 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts @@ -25,7 +25,7 @@ export type ElasticsearchRole = Pick, name: string, application: string ): Role { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index aefcc0c72c6db..09bcb6b8c505c 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -12,7 +12,6 @@ import type { KibanaFeature } from '../../../../../features/common'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import type { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import type { ElasticsearchRole } from './model'; import { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './model'; const roleGrantsSubFeaturePrivileges = ( @@ -65,13 +64,15 @@ export function definePutRolesRoutes({ try { const { body: rawRoles, - } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< - Record - >({ name: request.params.name }, { ignore: [404] }); + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole( + { name: request.params.name }, + { ignore: [404] } + ); const body = transformPutPayloadToElasticsearchRole( request.body, authz.applicationName, + // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications`. rawRoles[name] ? rawRoles[name].applications : [] ); @@ -79,6 +80,7 @@ export function definePutRolesRoutes({ getFeatures(), context.core.elasticsearch.client.asCurrentUser.security.putRole({ name: request.params.name, + // @ts-expect-error RoleIndexPrivilege is not compatible. grant is required in IndicesPrivileges.field_security body, }), ]); diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index 3ed7493ea1f0e..63704682e3635 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -10,20 +10,6 @@ import { schema } from '@kbn/config-schema'; import { wrapIntoCustomErrorResponse } from '../../errors'; import type { RouteDefinitionParams } from '../index'; -interface FieldMappingResponse { - [indexName: string]: { - mappings: { - [fieldName: string]: { - mapping: { - [fieldName: string]: { - type: string; - }; - }; - }; - }; - }; -} - export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { router.get( { @@ -34,14 +20,12 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { try { const { body: indexMappings, - } = await context.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping( - { - index: request.params.query, - fields: '*', - allow_no_indices: false, - include_defaults: true, - } - ); + } = await context.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping({ + index: request.params.query, + fields: '*', + allow_no_indices: false, + include_defaults: true, + }); // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): // 1. Iterate over all matched indices. @@ -52,7 +36,13 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { new Set( Object.values(indexMappings).flatMap((indexMapping) => { return Object.keys(indexMapping.mappings).filter((fieldName) => { - const mappingValues = Object.values(indexMapping.mappings[fieldName].mapping); + const mappingValues = Object.values( + // `FieldMapping` type from `TypeFieldMappings` --> `GetFieldMappingResponse` is not correct and + // doesn't have any properties. + (indexMapping.mappings[fieldName] as { + mapping: Record; + }).mapping + ); const hasMapping = mappingValues.length > 0; const isRuntimeField = hasMapping && mappingValues[0]?.type === 'runtime'; diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index d060825a989d5..67cd8975b76eb 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -12,10 +12,6 @@ import type { RoleMapping } from '../../../common/model'; import { wrapError } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -interface RoleMappingsResponse { - [roleMappingName: string]: Omit; -} - export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { const { logger, router } = params; @@ -32,7 +28,7 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { const expectSingleEntity = typeof request.params.name === 'string'; try { - const roleMappingsResponse = await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping( + const roleMappingsResponse = await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping( { name: request.params.name } ); @@ -40,7 +36,8 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { return { name, ...mapping, - role_templates: (mapping.role_templates || []).map((entry) => { + // @ts-expect-error @elastic/elasticsearch `XPackRoleMapping` type doesn't define `role_templates` property. + role_templates: (mapping.role_templates || []).map((entry: RoleTemplate) => { return { ...entry, template: tryParseRoleTemplate(entry.template as string), diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts index 28165ef32356d..81502044ef94d 100644 --- a/x-pack/plugins/security/server/routes/users/get.ts +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -24,9 +24,7 @@ export function defineGetUserRoutes({ router }: RouteDefinitionParams) { const username = request.params.username; const { body: users, - } = await context.core.elasticsearch.client.asCurrentUser.security.getUser< - Record - >({ + } = await context.core.elasticsearch.client.asCurrentUser.security.getUser({ username, }); diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index b5b4f64438902..11fb4ca27f590 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -41,7 +41,7 @@ describe('Session index', () => { name: indexTemplateName, }); expect(mockElasticsearchClient.indices.exists).toHaveBeenCalledWith({ - index: getSessionIndexTemplate(indexName).index_patterns, + index: getSessionIndexTemplate(indexName).index_patterns[0], }); } @@ -93,7 +93,7 @@ describe('Session index', () => { body: expectedIndexTemplate, }); expect(mockElasticsearchClient.indices.create).toHaveBeenCalledWith({ - index: expectedIndexTemplate.index_patterns, + index: expectedIndexTemplate.index_patterns[0], }); }); @@ -126,7 +126,7 @@ describe('Session index', () => { assertExistenceChecksPerformed(); expect(mockElasticsearchClient.indices.create).toHaveBeenCalledWith({ - index: getSessionIndexTemplate(indexName).index_patterns, + index: getSessionIndexTemplate(indexName).index_patterns[0], }); }); @@ -166,7 +166,7 @@ describe('Session index', () => { const now = 123456; beforeEach(() => { mockElasticsearchClient.deleteByQuery.mockResolvedValue( - securityMock.createApiResponse({ body: {} }) + securityMock.createApiResponse({ body: {} as any }) ); jest.spyOn(Date, 'now').mockImplementation(() => now); }); @@ -600,7 +600,10 @@ describe('Session index', () => { it('returns `null` if index is not found', async () => { mockElasticsearchClient.get.mockResolvedValue( - securityMock.createApiResponse({ statusCode: 404, body: { status: 404 } }) + securityMock.createApiResponse({ + statusCode: 404, + body: { _index: 'my-index', _type: '_doc', _id: '0', found: false }, + }) ); await expect(sessionIndex.get('some-sid')).resolves.toBeNull(); @@ -608,7 +611,10 @@ describe('Session index', () => { it('returns `null` if session index value document is not found', async () => { mockElasticsearchClient.get.mockResolvedValue( - securityMock.createApiResponse({ body: { status: 200, found: false } }) + securityMock.createApiResponse({ + statusCode: 200, + body: { _index: 'my-index', _type: '_doc', _id: '0', found: false }, + }) ); await expect(sessionIndex.get('some-sid')).resolves.toBeNull(); @@ -625,9 +631,12 @@ describe('Session index', () => { mockElasticsearchClient.get.mockResolvedValue( securityMock.createApiResponse({ + statusCode: 200, body: { found: true, - status: 200, + _index: 'my-index', + _type: '_doc', + _id: '0', _source: indexDocumentSource, _primary_term: 1, _seq_no: 456, @@ -670,7 +679,17 @@ describe('Session index', () => { it('properly stores session value in the index', async () => { mockElasticsearchClient.create.mockResolvedValue( - securityMock.createApiResponse({ body: { _primary_term: 321, _seq_no: 654 } }) + securityMock.createApiResponse({ + body: { + _shards: { total: 1, failed: 0, successful: 1, skipped: 0 }, + _index: 'my-index', + _id: 'W0tpsmIBdwcYyG50zbta', + _version: 1, + _primary_term: 321, + _seq_no: 654, + result: 'created', + }, + }) ); const sid = 'some-long-sid'; @@ -708,7 +727,7 @@ describe('Session index', () => { await expect(sessionIndex.update(sessionIndexMock.createValue())).rejects.toBe(failureReason); }); - it('refetches latest session value if update fails due to conflict', async () => { + it('re-fetches latest session value if update fails due to conflict', async () => { const latestSessionValue = { usernameHash: 'some-username-hash', provider: { type: 'basic', name: 'basic1' }, @@ -719,17 +738,31 @@ describe('Session index', () => { mockElasticsearchClient.get.mockResolvedValue( securityMock.createApiResponse({ + statusCode: 200, body: { - found: true, - status: 200, + _index: 'my-index', + _type: '_doc', + _id: '0', _source: latestSessionValue, _primary_term: 321, _seq_no: 654, + found: true, }, }) ); mockElasticsearchClient.index.mockResolvedValue( - securityMock.createApiResponse({ statusCode: 409, body: { status: 409 } }) + securityMock.createApiResponse({ + statusCode: 409, + body: { + _shards: { total: 1, failed: 0, successful: 1, skipped: 0 }, + _index: 'my-index', + _id: 'W0tpsmIBdwcYyG50zbta', + _version: 1, + _primary_term: 321, + _seq_no: 654, + result: 'updated', + }, + }) ); const sid = 'some-long-sid'; @@ -763,7 +796,18 @@ describe('Session index', () => { it('properly stores session value in the index', async () => { mockElasticsearchClient.index.mockResolvedValue( - securityMock.createApiResponse({ body: { _primary_term: 321, _seq_no: 654, status: 200 } }) + securityMock.createApiResponse({ + statusCode: 200, + body: { + _shards: { total: 1, failed: 0, successful: 1, skipped: 0 }, + _index: 'my-index', + _id: 'W0tpsmIBdwcYyG50zbta', + _version: 1, + _primary_term: 321, + _seq_no: 654, + result: 'created', + }, + }) ); const sid = 'some-long-sid'; diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 1b5c820ec4710..d7a4c3e2520bf 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -39,7 +39,7 @@ const SESSION_INDEX_TEMPLATE_VERSION = 1; */ export function getSessionIndexTemplate(indexName: string) { return Object.freeze({ - index_patterns: indexName, + index_patterns: [indexName], order: 1000, settings: { index: { @@ -52,7 +52,7 @@ export function getSessionIndexTemplate(indexName: string) { }, }, mappings: { - dynamic: 'strict', + dynamic: 'strict' as 'strict', properties: { usernameHash: { type: 'keyword' }, provider: { properties: { name: { type: 'keyword' }, type: { type: 'keyword' } } }, @@ -151,13 +151,16 @@ export class SessionIndex { */ async get(sid: string) { try { - const { body: response } = await this.options.elasticsearchClient.get( + const { + body: response, + statusCode, + } = await this.options.elasticsearchClient.get( { id: sid, index: this.indexName }, { ignore: [404] } ); const docNotFound = response.found === false; - const indexNotFound = response.status === 404; + const indexNotFound = statusCode === 404; if (docNotFound || indexNotFound) { this.options.logger.debug('Cannot find session value with the specified ID.'); return null; @@ -215,7 +218,7 @@ export class SessionIndex { async update(sessionValue: Readonly) { const { sid, metadata, ...sessionValueToStore } = sessionValue; try { - const { body: response } = await this.options.elasticsearchClient.index( + const { body: response, statusCode } = await this.options.elasticsearchClient.index( { id: sid, index: this.indexName, @@ -230,7 +233,7 @@ export class SessionIndex { // We don't want to override changes that were made after we fetched session value or // re-create it if has been deleted already. If we detect such a case we discard changes and // return latest copy of the session value instead or `null` if doesn't exist anymore. - const sessionIndexValueUpdateConflict = response.status === 409; + const sessionIndexValueUpdateConflict = statusCode === 409; if (sessionIndexValueUpdateConflict) { this.options.logger.debug( 'Cannot update session value due to conflict, session either does not exist or was already updated.' @@ -457,7 +460,7 @@ export class SessionIndex { { ignore: [409, 404] } ); - if (response.deleted > 0) { + if (response.deleted! > 0) { this.options.logger.debug( `Cleaned up ${response.deleted} invalid or expired session values.` ); diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts index 05c47605b4951..e27e9b5173fd5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchResponse } from '../../../../../../src/plugins/data/common'; export type Maybe = T | null; @@ -70,10 +70,7 @@ export interface PaginationInputPaginated { querySize: number; } -export interface DocValueFields { - field: string; - format?: string | null; -} +export type DocValueFields = estypes.DocValueField; export interface Explanation { value: number; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 319933be5c79e..2160ed6170e29 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { @@ -64,13 +64,7 @@ import { MatrixHistogramRequestOptions, MatrixHistogramStrategyResponse, } from './matrix_histogram'; -import { - DocValueFields, - TimerangeInput, - SortField, - PaginationInput, - PaginationInputPaginated, -} from '../common'; +import { TimerangeInput, SortField, PaginationInput, PaginationInputPaginated } from '../common'; export * from './hosts'; export * from './matrix_histogram'; @@ -87,7 +81,7 @@ export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; filterQuery: ESQuery | string | undefined; defaultIndex: string[]; - docValueFields?: DocValueFields[]; + docValueFields?: estypes.DocValueField[]; factoryQueryType?: FactoryQueryTypes; } diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 6daced08be282..f66b060b166bf 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -94,7 +94,7 @@ export const getDocValueFields = memoizeOne( ...accumulator, { field: field.name, - format: field.format, + format: field.format ? field.format : undefined, }, ]; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts index cc8b99875758d..a266cff73344f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts @@ -6,9 +6,8 @@ */ import _ from 'lodash'; -import { RequestHandler, SearchResponse } from 'kibana/server'; +import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { ApiResponse } from '@elastic/elasticsearch'; import { validateEntities } from '../../../../common/endpoint/schema/resolver'; import { ResolverEntityIndex, ResolverSchema } from '../../../../common/endpoint/types'; @@ -88,9 +87,7 @@ export function handleEntities(): RequestHandler - > = await context.core.elasticsearch.client.asCurrentUser.search({ + const queryResponse = await context.core.elasticsearch.client.asCurrentUser.search({ ignore_unavailable: true, index: indices, body: { @@ -102,6 +99,7 @@ export function handleEntities(): RequestHandler { const parsedFilters = EventsQuery.buildFilters(filter); - const response: ApiResponse< - SearchResponse - > = await client.asCurrentUser.search(this.buildSearch(parsedFilters)); + const response = await client.asCurrentUser.search( + this.buildSearch(parsedFilters) + ); + // @ts-expect-error @elastic/elasticsearch _source is optional return response.body.hits.hits.map((hit) => hit._source); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts index 8edfa81efbca6..bf9b3ce6aa8f3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; -import { ApiResponse } from '@elastic/elasticsearch'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common'; @@ -198,7 +197,7 @@ export class DescendantsQuery { return []; } - let response: ApiResponse>; + let response: ApiResponse>; if (this.schema.ancestry) { response = await client.asCurrentUser.search({ body: this.queryWithAncestryArray(validNodes, this.schema.ancestry, limit), @@ -219,6 +218,7 @@ export class DescendantsQuery { * * So the schema fields are flattened ('process.parent.entity_id') */ + // @ts-expect-error @elastic/elasticsearch _source is optional return response.body.hits.hits.map((hit) => hit.fields); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts index d04f0d8ee3bc9..f9780d1469756 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; -import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common'; @@ -93,7 +91,7 @@ export class LifecycleQuery { return []; } - const response: ApiResponse> = await client.asCurrentUser.search({ + const response = await client.asCurrentUser.search({ body: this.query(validNodes), index: this.indexPatterns, }); @@ -106,6 +104,7 @@ export class LifecycleQuery { * * So the schema fields are flattened ('process.parent.entity_id') */ + // @ts-expect-error @elastic/elasticsearch _source is optional return response.body.hits.hits.map((hit) => hit.fields); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index 5d9db0c148d42..24c97ad88b26a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; -import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { JsonObject } from '../../../../../../../../../src/plugins/kibana_utils/common'; import { EventStats, ResolverSchema } from '../../../../../../common/endpoint/types'; @@ -125,11 +123,12 @@ export class StatsQuery { } // leaving unknown here because we don't actually need the hits part of the body - const response: ApiResponse> = await client.asCurrentUser.search({ + const response = await client.asCurrentUser.search({ body: this.query(nodes), index: this.indexPatterns, }); + // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response return response.body.aggregations?.ids?.buckets.reduce( (cummulative: Record, bucket: CategoriesAgg) => ({ ...cummulative, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts index 98a8f8c28d30d..48bbbdcbf3a48 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts @@ -27,6 +27,7 @@ export const deleteAllIndex = async ( { ignore: [404] } ); + // @ts-expect-error status doesn't exist on response if (resp.status === 404) { return true; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts index 488ba0dab0b97..abe587ec825c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts @@ -21,6 +21,7 @@ describe('get_index_exists', () => { test('it should return a true if you have _shards', async () => { const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.search.mockReturnValue( + // @ts-expect-error not full interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) ); const indexExists = await getIndexExists(esClient, 'some-index'); @@ -30,6 +31,7 @@ describe('get_index_exists', () => { test('it should return a false if you do NOT have _shards', async () => { const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; esClient.search.mockReturnValue( + // @ts-expect-error not full interface elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) ); const indexExists = await getIndexExists(esClient, 'some-index'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts index b86b58897ee62..cc7f22064572c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts @@ -15,8 +15,10 @@ export const getIndexExists = async ( const { body: response } = await esClient.search({ index, size: 0, - terminate_after: 1, allow_no_indices: true, + body: { + terminate_after: 1, + }, }); return response._shards.total > 0; } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts index d1d43ded32101..bf091ef2508af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts @@ -73,7 +73,7 @@ export const createMigration = async ({ return { destinationIndex: migrationIndex, sourceIndex: index, - taskId: response.body.task, + taskId: String(response.body.task!), version, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_index_versions_by_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_index_versions_by_index.test.ts index 32b3ccbf17b57..8e99cb32390e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_index_versions_by_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_index_versions_by_index.test.ts @@ -43,9 +43,9 @@ describe('getIndexVersionsByIndex', () => { }); it('properly transforms the response', async () => { - // @ts-expect-error mocking only what we need esClient.indices.getMapping.mockResolvedValue({ body: { + // @ts-expect-error mocking only what we need index1: { mappings: { _meta: { version: 3 } } }, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.test.ts index 24d8fab9c50de..2cdfa2c13e7b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.test.ts @@ -16,8 +16,8 @@ describe('getSignalVersionsByIndex', () => { }); it('properly transforms the elasticsearch aggregation', async () => { - // @ts-expect-error mocking only what we need esClient.search.mockResolvedValueOnce({ + // @ts-expect-error mocking only what we need body: { aggregations: { signals_indices: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts index ea064a9397879..784164e430ff0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts @@ -50,7 +50,7 @@ export const getSignalVersionsByIndex = async ({ esClient: ElasticsearchClient; index: string[]; }): Promise => { - const { body } = await esClient.search({ + const response = await esClient.search({ index, size: 0, body: { @@ -71,6 +71,9 @@ export const getSignalVersionsByIndex = async ({ }, }, }); + + // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + const body = response.body as SignalVersionsAggResponse; const indexBuckets = body.aggregations.signals_indices.buckets; return index.reduce((agg, _index) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts index 3135bfec4e0e7..3c9132fc81279 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts @@ -42,7 +42,7 @@ export const getSignalsIndicesInRange = async ({ return []; } - const response = await esClient.search({ + const response = await esClient.search({ index, body: { aggs: { @@ -60,6 +60,7 @@ export const getSignalsIndicesInRange = async ({ '@timestamp': { gte: from, lte: 'now', + // @ts-expect-error format doesn't exist in RangeQuery format: 'strict_date_optional_time', }, }, @@ -71,5 +72,7 @@ export const getSignalsIndicesInRange = async ({ }, }); - return response.body.aggregations.indexes.buckets.map((bucket) => bucket.key); + // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + const body = response.body as IndexesResponse; + return body.aggregations.indexes.buckets.map((bucket) => bucket.key); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts index b411ac2c69ef2..398438234ed71 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { SignalSearchResponse } from '../signals/types'; +import type { SignalSearchResponse, SignalSource } from '../signals/types'; import { buildSignalsSearchQuery } from './build_signals_query'; interface GetSignalsParams { @@ -38,7 +38,7 @@ export const getSignals = async ({ size, }); - const { body: result } = await esClient.search(query); + const { body: result } = await esClient.search(query); return result; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index a40cb998eb408..799fb3814f1f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -91,6 +91,7 @@ export const rulesNotificationAlertType = ({ signalsCount, resultsLink, ruleParams, + // @ts-expect-error @elastic/elasticsearch _source is optional signals, }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts index 5c626cbe33ac1..b333ef999a6ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts @@ -5,26 +5,15 @@ * 2.0. */ -import { ApiResponse } from '@elastic/elasticsearch'; import { get } from 'lodash'; import { ElasticsearchClient } from '../../../../../../../../src/core/server'; import { readIndex } from '../../index/read_index'; -interface IndicesAliasResponse { - [index: string]: IndexAliasResponse; -} - -interface IndexAliasResponse { - aliases: { - [aliasName: string]: Record; - }; -} - export const getIndexVersion = async ( esClient: ElasticsearchClient, index: string ): Promise => { - const { body: indexAlias }: ApiResponse = await esClient.indices.getAlias({ + const { body: indexAlias } = await esClient.indices.getAlias({ index, }); const writeIndex = Object.keys(indexAlias).find( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 4106d7532f7bb..8c9b19a0929d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -6,7 +6,7 @@ */ import { set } from '@elastic/safer-lodash-set'; -import { +import type { SignalSourceHit, SignalSearchResponse, BulkResponse, @@ -261,7 +261,9 @@ export const sampleWrappedSignalHit = (): WrappedSignalHit => { export const sampleDocWithAncestors = (): SignalSearchResponse => { const sampleDoc = sampleDocNoSortId(); delete sampleDoc.sort; + // @ts-expect-error @elastic/elasticsearch _source is optional delete sampleDoc._source.source; + // @ts-expect-error @elastic/elasticsearch _source is optional sampleDoc._source.signal = { parent: { id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 362c368881b37..708aefc4d8614 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -33,6 +33,7 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.source; const fakeSignalSourceHit = buildBulkBody({ doc, @@ -143,6 +144,7 @@ describe('buildBulkBody', () => { const baseDoc = sampleDocNoSortId(); const doc: SignalSourceHit = { ...baseDoc, + // @ts-expect-error @elastic/elasticsearch _source is optional _source: { ...baseDoc._source, threshold_result: { @@ -155,6 +157,7 @@ describe('buildBulkBody', () => { }, }, }; + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.source; const fakeSignalSourceHit = buildBulkBody({ doc, @@ -271,7 +274,9 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.source; + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', module: 'system', @@ -384,7 +389,9 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with but no kind information', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.source; + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', module: 'system', @@ -495,7 +502,9 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with with only kind information', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.source; + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { kind: 'event', }; @@ -599,6 +608,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as a numeric', () => { const sampleParams = sampleRuleAlertParams(); const sampleDoc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -702,6 +712,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as an object', () => { const sampleParams = sampleRuleAlertParams(); const sampleDoc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -1053,6 +1064,7 @@ describe('buildSignalFromSequence', () => { describe('buildSignalFromEvent', () => { test('builds a basic signal from a single event', () => { const ancestor = sampleDocWithAncestors().hits.hits[0]; + // @ts-expect-error @elastic/elasticsearch _source is optional delete ancestor._source.source; const ruleSO = sampleRuleSO(); const signal = buildSignalFromEvent(ancestor, ruleSO, true); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index fb72bf9c59b4e..3a6cbf5ccd34b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -73,6 +73,7 @@ export const buildBulkBody = ({ ...buildSignal([doc], rule), ...additionalSignalFields(doc), }; + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.threshold_result; const event = buildEventTypeSignal(doc); const signalHit: SignalHit = { @@ -163,7 +164,8 @@ export const buildSignalFromEvent = ( applyOverrides: boolean ): SignalHit => { const rule = applyOverrides - ? buildRuleWithOverrides(ruleSO, event._source) + ? // @ts-expect-error @elastic/elasticsearch _source is optional + buildRuleWithOverrides(ruleSO, event._source) : buildRuleWithoutOverrides(ruleSO); const signal: Signal = { ...buildSignal([event], rule), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts index 0ae81770e83c2..185c165442921 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.test.ts @@ -16,6 +16,7 @@ describe('buildEventTypeSignal', () => { test('it returns the event appended of kind signal if it does not exist', () => { const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.event; const eventType = buildEventTypeSignal(doc); const expected: object = { kind: 'signal' }; @@ -24,6 +25,7 @@ describe('buildEventTypeSignal', () => { test('it returns the event appended of kind signal if it is an empty object', () => { const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = {}; const eventType = buildEventTypeSignal(doc); const expected: object = { kind: 'signal' }; @@ -32,6 +34,7 @@ describe('buildEventTypeSignal', () => { test('it returns the event with kind signal and other properties if they exist', () => { const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', module: 'system', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts index bc267ba59b9c3..374788f31a359 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_event_type_signal.ts @@ -4,12 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { BaseSignalHit } from './types'; export const buildEventTypeSignal = (doc: BaseSignalHit): object => { - if (doc._source.event != null && doc._source.event instanceof Object) { - return { ...doc._source.event, kind: 'signal' }; + if (doc._source?.event != null && doc._source?.event instanceof Object) { + return { ...doc._source!.event, kind: 'signal' }; } else { return { kind: 'signal' }; } @@ -25,5 +24,5 @@ export const buildEventTypeSignal = (doc: BaseSignalHit): object => { * @param doc The document which might be a signal or it might be a regular log */ export const isEventTypeSignal = (doc: BaseSignalHit): boolean => { - return doc._source.signal?.rule?.id != null && typeof doc._source.signal?.rule?.id === 'string'; + return doc._source?.signal?.rule?.id != null && typeof doc._source?.signal?.rule?.id === 'string'; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index bce9adc9f0f88..e086c862262c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -4,18 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { SortOrderOrUndefined, TimestampOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; interface BuildEventsSearchQuery { - aggregations?: unknown; + aggregations?: Record; index: string[]; from: string; to: string; - filter: unknown; + filter?: estypes.QueryContainer; size: number; sortOrder?: SortOrderOrUndefined; searchAfterSortId: string | number | undefined; @@ -48,7 +48,7 @@ export const buildEventsSearchQuery = ({ ? timestampOverride : '@timestamp'; - const rangeFilter: unknown[] = [ + const rangeFilter: estypes.QueryContainer[] = [ { range: { [sortField]: { @@ -70,7 +70,9 @@ export const buildEventsSearchQuery = ({ }, }); } - const filterWithTime = [filter, { bool: { filter: rangeFilter } }]; + // @ts-expect-error undefined in not assignable to QueryContainer + // but tests contain undefined, so I suppose it's desired behaviour + const filterWithTime: estypes.QueryContainer[] = [filter, { bool: { filter: rangeFilter } }]; const searchQuery = { allow_no_indices: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index 48e04df3704ab..757e6728f244e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -417,6 +417,7 @@ describe('buildRuleWithOverrides', () => { query: 'host.name: Braden', }, ]; + // @ts-expect-error @elastic/elasticsearch _source is optional const rule = buildRuleWithOverrides(ruleSO, sampleDocNoSortId()._source); const expected: RulesSchema = { ...expectedRule(), @@ -433,6 +434,7 @@ describe('buildRuleWithOverrides', () => { `${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:true`, ]; + // @ts-expect-error @elastic/elasticsearch _source is optional const rule = buildRuleWithOverrides(ruleSO, sampleDocNoSortId()._source); expect(rule).toEqual(expectedRule()); }); @@ -440,6 +442,7 @@ describe('buildRuleWithOverrides', () => { test('it applies rule name override in buildRule', () => { const ruleSO = sampleRuleSO(); ruleSO.attributes.params.ruleNameOverride = 'someKey'; + // @ts-expect-error @elastic/elasticsearch _source is optional const rule = buildRuleWithOverrides(ruleSO, sampleDocNoSortId()._source); const expected = { ...expectedRule(), @@ -465,7 +468,9 @@ describe('buildRuleWithOverrides', () => { }, ]; const doc = sampleDocNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.new_risk_score = newRiskScore; + // @ts-expect-error @elastic/elasticsearch _source is optional const rule = buildRuleWithOverrides(ruleSO, doc._source); const expected = { ...expectedRule(), @@ -490,6 +495,7 @@ describe('buildRuleWithOverrides', () => { }, ]; const doc = sampleDocSeverity(Number(eventSeverity)); + // @ts-expect-error @elastic/elasticsearch _source is optional const rule = buildRuleWithOverrides(ruleSO, doc._source); const expected = { ...expectedRule(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index 0681a5dddb127..7755f2af70d84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -47,18 +47,21 @@ export const buildRule = ({ throttle, }: BuildRuleParams): RulesSchema => { const { riskScore, riskScoreMeta } = buildRiskScoreFromMapping({ + // @ts-expect-error @elastic/elasticsearch _source is optional eventSource: doc._source, riskScore: ruleParams.riskScore, riskScoreMapping: ruleParams.riskScoreMapping, }); const { severity, severityMeta } = buildSeverityFromMapping({ + // @ts-expect-error @elastic/elasticsearch _source is optional eventSource: doc._source, severity: ruleParams.severity, severityMapping: ruleParams.severityMapping, }); const { ruleName, ruleNameMeta } = buildRuleNameFromMapping({ + // @ts-expect-error @elastic/elasticsearch _source is optional eventSource: doc._source, ruleName: name, ruleNameMapping: ruleParams.ruleNameOverride, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 98b0d41bb0bf8..6408b5fe9de10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -28,6 +28,7 @@ describe('buildSignal', () => { test('it builds a signal as expected without original_event if event does not exist', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + // @ts-expect-error @elastic/elasticsearch _source is optional delete doc._source.event; const rule = getRulesSchemaMock(); const signal = { @@ -104,6 +105,7 @@ describe('buildSignal', () => { test('it builds a signal as expected with original_event if is present', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', dataset: 'socket', @@ -191,6 +193,7 @@ describe('buildSignal', () => { test('it builds a ancestor correctly if the parent does not exist', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', dataset: 'socket', @@ -209,12 +212,14 @@ describe('buildSignal', () => { test('it builds a ancestor correctly if the parent does exist', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', dataset: 'socket', kind: 'event', module: 'system', }; + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.signal = { parents: [ { @@ -250,6 +255,7 @@ describe('buildSignal', () => { test('it builds a signal ancestor correctly if the parent does not exist', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', dataset: 'socket', @@ -270,12 +276,14 @@ describe('buildSignal', () => { test('it builds a signal ancestor correctly if the parent does exist', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.event = { action: 'socket_opened', dataset: 'socket', kind: 'event', module: 'system', }; + // @ts-expect-error @elastic/elasticsearch _source is optional doc._source.signal = { parents: [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index 78ff0e8e1e5dd..237536a99c0f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -17,15 +17,15 @@ import { Signal, Ancestor, BaseSignalHit, ThresholdResult } from './types'; * @param doc The parent signal or event */ export const buildParent = (doc: BaseSignalHit): Ancestor => { - if (doc._source.signal != null) { + if (doc._source?.signal != null) { return { - rule: doc._source.signal.rule.id, + rule: doc._source?.signal.rule.id, id: doc._id, type: 'signal', index: doc._index, // We first look for signal.depth and use that if it exists. If it doesn't exist, this should be a pre-7.10 signal // and should have signal.parent.depth instead. signal.parent.depth in this case is treated as equivalent to signal.depth. - depth: doc._source.signal.depth ?? doc._source.signal.parent?.depth ?? 1, + depth: doc._source?.signal.depth ?? doc._source?.signal.parent?.depth ?? 1, }; } else { return { @@ -44,7 +44,7 @@ export const buildParent = (doc: BaseSignalHit): Ancestor => { */ export const buildAncestors = (doc: BaseSignalHit): Ancestor[] => { const newAncestor = buildParent(doc); - const existingAncestors = doc._source.signal?.ancestors; + const existingAncestors = doc._source?.signal?.ancestors; if (existingAncestors != null) { return [...existingAncestors, newAncestor]; } else { @@ -59,6 +59,7 @@ export const buildAncestors = (doc: BaseSignalHit): Ancestor[] => { * @param doc The source index doc to a signal. */ export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => { + // @ts-expect-error @elastic/elasticsearch _source is optional const { signal, ...noSignal } = doc._source; if (signal == null || isEventTypeSignal(doc)) { return doc; @@ -105,16 +106,17 @@ const isThresholdResult = (thresholdResult: SearchTypes): thresholdResult is Thr * @param doc The parent signal/event of the new signal to be built. */ export const additionalSignalFields = (doc: BaseSignalHit) => { - const thresholdResult = doc._source.threshold_result; + const thresholdResult = doc._source?.threshold_result; if (thresholdResult != null && !isThresholdResult(thresholdResult)) { throw new Error(`threshold_result failed to validate: ${thresholdResult}`); } return { parent: buildParent(removeClashes(doc)), + // @ts-expect-error @elastic/elasticsearch _source is optional original_time: doc._source['@timestamp'], // This field has already been replaced with timestampOverride, if provided. - original_event: doc._source.event ?? undefined, + original_event: doc._source?.event ?? undefined, threshold_result: thresholdResult, original_signal: - doc._source.signal != null && !isEventTypeSignal(doc) ? doc._source.signal : undefined, + doc._source?.signal != null && !isEventTypeSignal(doc) ? doc._source?.signal : undefined, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index c6a8c211417b8..a5e05d07ee1e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { flow, omit } from 'lodash/fp'; import set from 'set-value'; @@ -19,7 +19,6 @@ import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; -import { SearchResponse } from '../../types'; interface BulkCreateMlSignalsParams { actions: RuleAlertAction[]; @@ -72,12 +71,18 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); }; -const transformAnomalyResultsToEcs = (results: AnomalyResults): SearchResponse => { +const transformAnomalyResultsToEcs = ( + results: AnomalyResults +): estypes.SearchResponse => { const transformedHits = results.hits.hits.map(({ _source, ...rest }) => ({ ...rest, - _source: transformAnomalyFieldsToEcs(_source), + _source: transformAnomalyFieldsToEcs( + // @ts-expect-error @elastic/elasticsearch _source is optional + _source + ), })); + // @ts-expect-error Anomaly is not assignable to EcsAnomaly return { ...results, hits: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts index 421ed91278f4c..a50d40e33a717 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { SearchResponse } from '../../../types'; +import type { estypes } from '@elastic/elasticsearch'; import { FilterEventsOptions } from './types'; /** @@ -17,7 +16,7 @@ import { FilterEventsOptions } from './types'; export const filterEvents = ({ events, fieldAndSetTuples, -}: FilterEventsOptions): SearchResponse['hits']['hits'] => { +}: FilterEventsOptions): Array> => { return events.filter((item) => { return fieldAndSetTuples .map((tuple) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index bb8fc5afb1ddf..1320122626bfd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -4,13 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { ExceptionListItemSchema, entriesList } from '../../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; import { FilterEventsAgainstListOptions } from './types'; import { filterEvents } from './filter_events'; import { createFieldAndSetTuples } from './create_field_and_set_tuples'; -import { SearchResponse } from '../../../types'; /** * Filters events against a large value based list. It does this through these @@ -39,7 +38,7 @@ export const filterEventsAgainstList = async ({ logger, eventSearchResult, buildRuleMessage, -}: FilterEventsAgainstListOptions): Promise> => { +}: FilterEventsAgainstListOptions): Promise> => { try { const atLeastOneLargeValueList = exceptionsList.some(({ entries }) => hasLargeValueList(entries) @@ -56,9 +55,9 @@ export const filterEventsAgainstList = async ({ return listItem.entries.every((entry) => entriesList.is(entry)); }); - const res = await valueListExceptionItems.reduce['hits']['hits']>>( + const res = await valueListExceptionItems.reduce>>>( async ( - filteredAccum: Promise['hits']['hits']>, + filteredAccum: Promise>>, exceptionItem: ExceptionListItemSchema ) => { const events = await filteredAccum; @@ -76,7 +75,7 @@ export const filterEventsAgainstList = async ({ ); return filteredEvents; }, - Promise.resolve['hits']['hits']>(eventSearchResult.hits.hits) + Promise.resolve>>(eventSearchResult.hits.hits) ); return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts index d3bae848ab2a7..e1618d217d0dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts @@ -4,24 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { Logger } from 'src/core/server'; import { ListClient } from '../../../../../../lists/server'; import { BuildRuleMessage } from '../rule_messages'; import { ExceptionListItemSchema, Type } from '../../../../../../lists/common/schemas'; -import { SearchResponse } from '../../../types'; export interface FilterEventsAgainstListOptions { listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; logger: Logger; - eventSearchResult: SearchResponse; + eventSearchResult: estypes.SearchResponse; buildRuleMessage: BuildRuleMessage; } export interface CreateSetToFilterAgainstOptions { - events: SearchResponse['hits']['hits']; + events: Array>; field: string; listId: string; listType: Type; @@ -31,12 +30,12 @@ export interface CreateSetToFilterAgainstOptions { } export interface FilterEventsOptions { - events: SearchResponse['hits']['hits']; + events: Array>; fieldAndSetTuples: FieldSet[]; } export interface CreateFieldAndSetTuplesOptions { - events: SearchResponse['hits']['hits']; + events: Array>; exceptionItem: ExceptionListItemSchema; listClient: ListClient; logger: Logger; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts index c21b4df1ac2c7..88ce9de15cff8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -195,6 +195,7 @@ interface TestCase { function testIt({ fieldValue, scoreDefault, scoreMapping, expected }: TestCase) { const result = buildRiskScoreFromMapping({ + // @ts-expect-error @elastic/elasticsearch _source is optional eventSource: sampleDocRiskScore(fieldValue)._source, riskScore: scoreDefault, riskScoreMapping: scoreMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts index 23e5aecc5c553..b6281b637d434 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts @@ -15,6 +15,7 @@ describe('buildRuleNameFromMapping', () => { test('rule name defaults to provided if mapping is incomplete', () => { const ruleName = buildRuleNameFromMapping({ + // @ts-expect-error @elastic/elasticsearch _source is optional eventSource: sampleDocNoSortId()._source, ruleName: 'rule-name', ruleNameMapping: 'message', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index 1264592331df0..cfd4b81ae3bc8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -148,6 +148,7 @@ interface TestCase { function testIt({ fieldName, fieldValue, severityDefault, severityMapping, expected }: TestCase) { const result = buildSeverityFromMapping({ + // @ts-expect-error @elastic/elasticsearch _source is optional eventSource: sampleDocSeverity(fieldValue, fieldName)._source, severity: severityDefault, severityMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index ccefa24e2018c..6deb45095ec36 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -68,6 +68,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -88,6 +89,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -108,6 +110,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -128,6 +131,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -198,6 +202,7 @@ describe('searchAfterAndBulkCreate', () => { ) ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -218,6 +223,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -238,6 +244,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -309,6 +316,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -532,6 +540,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -617,6 +626,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -802,6 +812,7 @@ describe('searchAfterAndBulkCreate', () => { test('if returns false when singleSearchAfter throws an exception', async () => { mockService.scopedClusterClient.asCurrentUser.search .mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -906,6 +917,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -926,6 +938,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -946,6 +959,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -1010,6 +1024,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -1030,6 +1045,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -1050,6 +1066,7 @@ describe('searchAfterAndBulkCreate', () => { ); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 1dd3a2d2173a8..cfe30a6602381 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -95,6 +95,7 @@ export const searchAfterAndBulkCreate = async ({ to: tuple.to.toISOString(), services, logger, + // @ts-expect-error please, declare a type explicitly instead of unknown filter, pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), timestampOverride: ruleParams.timestampOverride, @@ -104,6 +105,7 @@ export const searchAfterAndBulkCreate = async ({ // call this function setSortIdOrExit() const lastSortId = searchResultB?.hits?.hits[searchResultB.hits.hits.length - 1]?.sort; if (lastSortId != null && lastSortId.length !== 0) { + // @ts-expect-error @elastic/elasticsearch SortResults contains null not assignable to backupSortId backupSortId = lastSortId[0]; hasBackupSortId = true; } else { @@ -135,6 +137,7 @@ export const searchAfterAndBulkCreate = async ({ to: tuple.to.toISOString(), services, logger, + // @ts-expect-error please, declare a type explicitly instead of unknown filter, pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), timestampOverride: ruleParams.timestampOverride, @@ -155,6 +158,7 @@ export const searchAfterAndBulkCreate = async ({ const lastSortId = searchResult.hits.hits[searchResult.hits.hits.length - 1]?.sort; if (lastSortId != null && lastSortId.length !== 0) { + // @ts-expect-error @elastic/elasticsearch SortResults contains null not assignable to sortId sortId = lastSortId[0]; hasSortId = true; } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts index 22dc8847087ed..f7d21adc4bea9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts @@ -16,7 +16,8 @@ export interface SearchResultWithSource { } export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEvent[] { - const sources = filteredEvents.hits.hits.map(function ( + // @ts-expect-error @elastic/elasticsearch _source is optional + const sources: TelemetryEvent[] = filteredEvents.hits.hits.map(function ( obj: SearchResultWithSource ): TelemetryEvent { return obj._source; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index bcd04ed5e15cd..930cafe57fb0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import type { estypes } from '@elastic/elasticsearch'; import { loggingSystemMock } from 'src/core/server/mocks'; import { getResult, @@ -149,12 +150,13 @@ describe('rules_notification_alert_type', () => { }, }) ); - const value: Partial = { + const value: Partial> = { statusCode: 200, body: { indices: ['index1', 'index2', 'index3', 'index4'], fields: { '@timestamp': { + // @ts-expect-error not full interface date: { indices: ['index1', 'index2', 'index3', 'index4'], searchable: true, @@ -165,7 +167,7 @@ describe('rules_notification_alert_type', () => { }, }; alertServices.scopedClusterClient.asCurrentUser.fieldCaps.mockResolvedValue( - value as ApiResponse + value as ApiResponse ); const ruleAlert = getResult(); alertServices.savedObjectsClient.get.mockResolvedValue({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts index eecedb02b2687..b9a771ac0299e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -142,6 +142,7 @@ describe('singleBulkCreate', () => { test('create successful bulk create', async () => { const sampleParams = sampleRuleAlertParams(); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not compatible response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -179,6 +180,7 @@ describe('singleBulkCreate', () => { test('create successful bulk create with docs with no versioning', async () => { const sampleParams = sampleRuleAlertParams(); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + // @ts-expect-error not compatible response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, @@ -216,6 +218,7 @@ describe('singleBulkCreate', () => { test('create unsuccessful bulk create due to empty search results', async () => { const sampleParams = sampleRuleAlertParams(); mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise(false) ); const { success, createdItemsCount } = await singleBulkCreate({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index 6c791bc4d0ee3..8a0788f6d42e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -12,7 +12,7 @@ import { AlertInstanceState, AlertServices, } from '../../../../../alerting/server'; -import { SignalSearchResponse, BulkResponse, SignalHit, WrappedSignalHit } from './types'; +import { SignalHit, SignalSearchResponse, WrappedSignalHit } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { generateId, makeFloatString, errorAggregator } from './utils'; @@ -56,12 +56,12 @@ export const filterDuplicateRules = ( signalSearchResponse: SignalSearchResponse ) => { return signalSearchResponse.hits.hits.filter((doc) => { - if (doc._source.signal == null || !isEventTypeSignal(doc)) { + if (doc._source?.signal == null || !isEventTypeSignal(doc)) { return true; } else { return !( - doc._source.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) || - doc._source.signal.rule.id === ruleId + doc._source?.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) || + doc._source?.signal.rule.id === ruleId ); } }); @@ -158,7 +158,7 @@ export const singleBulkCreate = async ({ }), ]); const start = performance.now(); - const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ + const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ index: signalsIndex, refresh, body: bulkBody, @@ -244,7 +244,7 @@ export const bulkInsertSignals = async ( doc._source, ]); const start = performance.now(); - const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ + const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ refresh, body: bulkBody, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index a325903c66ec0..cbffac6e7b455 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { sampleDocSearchResultsNoSortId, mockLogger, @@ -12,7 +12,6 @@ import { } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; -import { ShardError } from '../../types'; import { buildRuleMessageFactory } from './rule_messages'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; @@ -69,7 +68,7 @@ describe('singleSearchAfter', () => { expect(searchErrors).toEqual([]); }); test('if singleSearchAfter will return an error array', async () => { - const errors: ShardError[] = [ + const errors: estypes.ShardFailure[] = [ { shard: 1, index: 'index-123', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index b35c68c8deacd..9dcec1861f15d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { performance } from 'perf_hooks'; import { AlertInstanceContext, @@ -12,7 +12,7 @@ import { AlertServices, } from '../../../../../alerting/server'; import { Logger } from '../../../../../../../src/core/server'; -import { SignalSearchResponse } from './types'; +import type { SignalSearchResponse, SignalSource } from './types'; import { BuildRuleMessage } from './rule_messages'; import { buildEventsSearchQuery } from './build_events_query'; import { createErrorsFromShard, makeFloatString } from './utils'; @@ -22,7 +22,7 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { - aggregations?: unknown; + aggregations?: Record; searchAfterSortId: string | undefined; index: string[]; from: string; @@ -31,7 +31,7 @@ interface SingleSearchAfterParams { logger: Logger; pageSize: number; sortOrder?: SortOrderOrUndefined; - filter: unknown; + filter?: estypes.QueryContainer; timestampOverride: TimestampOverrideOrUndefined; buildRuleMessage: BuildRuleMessage; excludeDocsWithTimestampOverride: boolean; @@ -74,9 +74,7 @@ export const singleSearchAfter = async ({ const start = performance.now(); const { body: nextSearchAfterResult, - } = await services.scopedClusterClient.asCurrentUser.search( - searchAfterQuery - ); + } = await services.scopedClusterClient.asCurrentUser.search(searchAfterQuery); const end = performance.now(); const searchErrors = createErrorsFromShard({ errors: nextSearchAfterResult._shards.failures ?? [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts index 266903f568792..e39b78b4f4a44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts @@ -4,11 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; import { Filter } from 'src/plugins/data/common'; -import { SearchResponse } from 'elasticsearch'; import { ThreatListDoc, ThreatListItem } from './types'; export const getThreatMappingMock = (): ThreatMapping => { @@ -62,7 +61,7 @@ export const getThreatMappingMock = (): ThreatMapping => { ]; }; -export const getThreatListSearchResponseMock = (): SearchResponse => ({ +export const getThreatListSearchResponseMock = (): estypes.SearchResponse => ({ took: 0, timed_out: false, _shards: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index e0be48458b049..0146572941331 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -172,6 +172,7 @@ export const createThreatSignals = async ({ language: threatLanguage, threatFilters, index: threatIndex, + // @ts-expect-error@elastic/elasticsearch SortResults might contain null searchAfter: threatList.hits.hits[threatList.hits.hits.length - 1].sort, sortField: undefined, sortOrder: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index bc2be4ecaab32..83a3ce8cb773f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -101,7 +101,7 @@ export const enrichSignalThreatMatches = async ( return { ...signalHit, _source: { - ...signalHit._source, + ...signalHit._source!, threat: { ...threat, indicator: [...existingIndicators, ...matchedIndicators[i]], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts index a2a51d3a060c1..c3d3d6c6a99e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -5,15 +5,14 @@ * 2.0. */ -import { ApiResponse } from '@elastic/elasticsearch'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; import { GetSortWithTieBreakerOptions, GetThreatListOptions, SortWithTieBreaker, ThreatListCountOptions, - ThreatListItem, + ThreatListDoc, } from './types'; /** @@ -35,7 +34,7 @@ export const getThreatList = async ({ listClient, buildRuleMessage, logger, -}: GetThreatListOptions): Promise> => { +}: GetThreatListOptions): Promise> => { const calculatedPerPage = perPage ?? MAX_PER_PAGE; if (calculatedPerPage > 10000) { throw new TypeError('perPage cannot exceed the size of 10000'); @@ -53,8 +52,9 @@ export const getThreatList = async ({ `Querying the indicator items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` ) ); - const { body: response } = await esClient.search>({ + const { body: response } = await esClient.search({ body: { + // @ts-expect-error ESBoolQuery is not assignale to QueryContainer query: queryFilter, fields: [ { @@ -123,12 +123,9 @@ export const getThreatListCount = async ({ index, exceptionItems ); - const { - body: response, - }: ApiResponse<{ - count: number; - }> = await esClient.count({ + const { body: response } = await esClient.count({ body: { + // @ts-expect-error ESBoolQuery is not assignale to QueryContainer query: queryFilter, }, ignore_unavailable: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 0c14f906742d4..65b59d4df0791 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -4,9 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { SearchResponse } from 'elasticsearch'; - +import type { estypes } from '@elastic/elasticsearch'; import { ListClient } from '../../../../../../lists/server'; import { Type, @@ -187,7 +185,7 @@ export interface ThreatListDoc { * This is an ECS document being returned, but the user could return or use non-ecs based * documents potentially. */ -export type ThreatListItem = SearchResponse['hits']['hits'][number]; +export type ThreatListItem = estypes.Hit; export interface ThreatIndicator { [key: string]: unknown; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index 43158d1e89783..8e5e31cc87b4f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -7,7 +7,6 @@ import { get } from 'lodash/fp'; import set from 'set-value'; - import { normalizeThresholdField } from '../../../../../common/detection_engine/utils'; import { ThresholdNormalized, @@ -29,10 +28,10 @@ import { getThresholdTermsHash, } from '../utils'; import { BuildRuleMessage } from '../rule_messages'; -import { +import type { MultiAggBucket, - SignalSearchResponse, SignalSource, + SignalSearchResponse, ThresholdSignalHistory, } from '../types'; @@ -141,7 +140,8 @@ const getTransformedHits = ( // Recurse through the nested buckets and collect each unique combination of terms. Collect the // cardinality and document count from the leaf buckets and return a signal for each set of terms. - return getCombinations(results.aggregations[aggParts.name].buckets, 0, aggParts.field).reduce( + // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + return getCombinations(results.aggregations![aggParts.name].buckets, 0, aggParts.field).reduce( (acc: Array>, bucket) => { const hit = bucket.topThresholdHits?.hits.hits[0]; if (hit == null) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index 7dda263dd9f0a..efcdb85e9b2c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -20,7 +20,7 @@ import { import { Logger } from '../../../../../../../../src/core/server'; import { BuildRuleMessage } from '../rule_messages'; import { singleSearchAfter } from '../single_search_after'; -import { SignalSearchResponse } from '../types'; +import type { SignalSearchResponse } from '../types'; interface FindThresholdSignalsParams { from: string; @@ -56,7 +56,7 @@ export const findThresholdSignals = async ({ sort: [ { [timestampOverride ?? '@timestamp']: { - order: 'desc', + order: 'desc' as const, }, }, ], @@ -137,6 +137,7 @@ export const findThresholdSignals = async ({ to, services, logger, + // @ts-expect-error refactor to pass type explicitly instead of unknown filter, pageSize: 1, sortOrder: 'desc', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index 73068a73a38a3..4dd21938690db 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -34,7 +34,7 @@ export const getThresholdBucketFilters = async ({ bucket.terms.forEach((term) => { if (term.field != null) { - (filter.bool.filter as ESFilter[]).push({ + (filter.bool!.filter as ESFilter[]).push({ term: { [term.field]: `${term.value}`, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 013d4a07cbeb7..559c5875c90d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { DslQuery, Filter } from 'src/plugins/data/common'; import moment, { Moment } from 'moment'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -17,7 +18,7 @@ import { AlertExecutorOptions, AlertServices, } from '../../../../../alerting/server'; -import { BaseSearchResponse, SearchHit, SearchResponse, TermAggregationBucket } from '../../types'; +import { BaseSearchResponse, SearchHit, TermAggregationBucket } from '../../types'; import { EqlSearchResponse, BaseHit, @@ -150,11 +151,10 @@ export interface GetResponse { _source: SearchTypes; } -export type EventSearchResponse = SearchResponse; -export type SignalSearchResponse = SearchResponse; -export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; +export type SignalSearchResponse = estypes.SearchResponse; +export type SignalSourceHit = estypes.Hit; export type WrappedSignalHit = BaseHit; -export type BaseSignalHit = BaseHit; +export type BaseSignalHit = estypes.Hit; export type EqlSignalSearchResponse = EqlSearchResponse; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 6d7948baa9681..249305ebcd9a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -1133,6 +1133,7 @@ describe('utils', () => { test('result with error will create success: false within the result set', () => { const searchResult = sampleDocSearchResultsNoSortIdNoHits(); searchResult._shards.failed = 1; + // @ts-expect-error not full interface searchResult._shards.failures = [{ reason: { reason: 'Not a sort failure' } }]; const { success } = createSearchAfterReturnTypeFromResponse({ searchResult, @@ -1144,6 +1145,7 @@ describe('utils', () => { test('result with error will create success: false within the result set if failed is 2 or more', () => { const searchResult = sampleDocSearchResultsNoSortIdNoHits(); searchResult._shards.failed = 2; + // @ts-expect-error not full interface searchResult._shards.failures = [{ reason: { reason: 'Not a sort failure' } }]; const { success } = createSearchAfterReturnTypeFromResponse({ searchResult, @@ -1156,7 +1158,9 @@ describe('utils', () => { const searchResult = sampleDocSearchResultsNoSortIdNoHits(); searchResult._shards.failed = 2; searchResult._shards.failures = [ + // @ts-expect-error not full interface { reason: { reason: 'Not a sort failure' } }, + // @ts-expect-error not full interface { reason: { reason: 'No mapping found for [@timestamp] in order to sort on' } }, ]; const { success } = createSearchAfterReturnTypeFromResponse({ @@ -1180,7 +1184,9 @@ describe('utils', () => { const searchResult = sampleDocSearchResultsNoSortIdNoHits(); searchResult._shards.failed = 2; searchResult._shards.failures = [ + // @ts-expect-error not full interface { reason: { reason: 'No mapping found for [event.ingested] in order to sort on' } }, + // @ts-expect-error not full interface { reason: { reason: 'No mapping found for [@timestamp] in order to sort on' } }, ]; const { success } = createSearchAfterReturnTypeFromResponse({ @@ -1192,6 +1198,7 @@ describe('utils', () => { test('It will not set an invalid date time stamp from a non-existent @timestamp when the index is not 100% ECS compliant', () => { const searchResult = sampleDocSearchResultsNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = undefined; if (searchResult.hits.hits[0].fields != null) { (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = undefined; @@ -1205,6 +1212,7 @@ describe('utils', () => { test('It will not set an invalid date time stamp from a null @timestamp when the index is not 100% ECS compliant', () => { const searchResult = sampleDocSearchResultsNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = null; if (searchResult.hits.hits[0].fields != null) { (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = null; @@ -1218,6 +1226,7 @@ describe('utils', () => { test('It will not set an invalid date time stamp from an invalid @timestamp string', () => { const searchResult = sampleDocSearchResultsNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid'; if (searchResult.hits.hits[0].fields != null) { (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = ['invalid']; @@ -1233,6 +1242,7 @@ describe('utils', () => { describe('lastValidDate', () => { test('It returns undefined if the search result contains a null timestamp', () => { const searchResult = sampleDocSearchResultsNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = null; if (searchResult.hits.hits[0].fields != null) { (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = null; @@ -1243,6 +1253,7 @@ describe('utils', () => { test('It returns undefined if the search result contains a undefined timestamp', () => { const searchResult = sampleDocSearchResultsNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = undefined; if (searchResult.hits.hits[0].fields != null) { (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = undefined; @@ -1253,6 +1264,7 @@ describe('utils', () => { test('It returns undefined if the search result contains an invalid string value', () => { const searchResult = sampleDocSearchResultsNoSortId(); + // @ts-expect-error @elastic/elasticsearch _source is optional (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid value'; if (searchResult.hits.hits[0].fields != null) { (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = ['invalid value']; @@ -1286,7 +1298,7 @@ describe('utils', () => { test('It returns timestampOverride date time if set', () => { const override = '2020-10-07T19:20:28.049Z'; const searchResult = sampleDocSearchResultsNoSortId(); - searchResult.hits.hits[0]._source.different_timestamp = new Date(override).toISOString(); + searchResult.hits.hits[0]._source!.different_timestamp = new Date(override).toISOString(); const date = lastValidDate({ searchResult, timestampOverride: 'different_timestamp' }); expect(date?.toISOString()).toEqual(override); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index fa2fa1f102bd1..28edd97de0a0e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -9,6 +9,7 @@ import { createHash } from 'crypto'; import moment from 'moment'; import uuidv5 from 'uuid/v5'; import dateMath from '@elastic/datemath'; +import type { estypes } from '@elastic/elasticsearch'; import { isEmpty, partition } from 'lodash'; import { ApiResponse, Context } from '@elastic/elasticsearch/lib/Transport'; @@ -27,7 +28,6 @@ import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../.. import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArray } from '../../../../common/detection_engine/schemas/types/lists'; import { - BulkResponse, BulkResponseErrorAggregation, SignalHit, SearchAfterAndBulkCreateReturnType, @@ -408,7 +408,7 @@ export const makeFloatString = (num: number): string => Number(num).toFixed(2); * @returns The aggregated example as shown above. */ export const errorAggregator = ( - response: BulkResponse, + response: estypes.BulkResponse, ignoreStatusCodes: number[] ): BulkResponseErrorAggregation => { return response.items.reduce((accum, item) => { @@ -568,7 +568,7 @@ export const lastValidDate = ({ searchResult, timestampOverride, }: { - searchResult: SignalSearchResponse; + searchResult: estypes.SearchResponse; timestampOverride: TimestampOverrideOrUndefined; }): Date | undefined => { if (searchResult.hits.hits.length === 0) { @@ -579,7 +579,8 @@ export const lastValidDate = ({ const timestampValue = lastRecord.fields != null && lastRecord.fields[timestamp] != null ? lastRecord.fields[timestamp][0] - : lastRecord._source[timestamp]; + : // @ts-expect-error @elastic/elasticsearch _source is optional + lastRecord._source[timestamp]; const lastTimestamp = typeof timestampValue === 'string' || typeof timestampValue === 'number' ? timestampValue @@ -599,7 +600,7 @@ export const createSearchAfterReturnTypeFromResponse = ({ searchResult, timestampOverride, }: { - searchResult: SignalSearchResponse; + searchResult: estypes.SearchResponse; timestampOverride: TimestampOverrideOrUndefined; }): SearchAfterAndBulkCreateReturnType => { return createSearchAfterReturnType({ @@ -730,6 +731,7 @@ export const mergeSearchResults = (searchResults: SignalSearchResponse[]) => { total: newShards.total + existingShards.total, successful: newShards.successful + existingShards.successful, failed: newShards.failed + existingShards.failed, + // @ts-expect-error @elastic/elaticsearch skipped is optional in ShardStatistics skipped: newShards.skipped + existingShards.skipped, failures: [ ...(existingShards.failures != null ? existingShards.failures : []), @@ -741,7 +743,7 @@ export const mergeSearchResults = (searchResults: SignalSearchResponse[]) => { total: createTotalHitsFromSearchResult({ searchResult: prev }) + createTotalHitsFromSearchResult({ searchResult: next }), - max_score: Math.max(newHits.max_score, existingHits.max_score), + max_score: Math.max(newHits.max_score!, existingHits.max_score!), hits: [...existingHits.hits, ...newHits.hits], }, }; @@ -751,7 +753,7 @@ export const mergeSearchResults = (searchResults: SignalSearchResponse[]) => { export const createTotalHitsFromSearchResult = ({ searchResult, }: { - searchResult: SignalSearchResponse; + searchResult: { hits: { total: number | { value: number } } }; }): number => { const totalHits = typeof searchResult.hits.total === 'number' diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index 6aac390a3f842..db42dc2720b2a 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -5,19 +5,18 @@ * 2.0. */ -import { RequestParams } from '@elastic/elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { buildExceptionFilter } from '../../../common/shared_imports'; import { ExceptionListItemSchema } from '../../../../lists/common'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; -import { SearchResponse } from '../types'; export { Anomaly }; -export type AnomalyResults = SearchResponse; +export type AnomalyResults = estypes.SearchResponse; type MlAnomalySearch = ( - searchParams: RequestParams.Search, + searchParams: estypes.SearchRequest, jobIds: string[] -) => Promise>; +) => Promise>; export interface AnomaliesSearchParams { jobIds: string[]; @@ -67,7 +66,7 @@ export const getAnomalies = async ( include_unmapped: true, }, ], - sort: [{ record_score: { order: 'desc' } }], + sort: [{ record_score: { order: 'desc' as const } }], }, }, params.jobIds diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts index 892801a3aed0b..5d8540f886077 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts @@ -58,7 +58,7 @@ export const buildHostsQuery = ({ sort: [ { '@timestamp': { - order: 'desc', + order: 'desc' as const, }, }, ], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts index d1925e84bc5e0..e960067713bda 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash/fp'; - +import type { estypes } from '@elastic/elasticsearch'; import { HostAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications'; import { sourceFieldsMap, hostFieldsMap } from '../../../../../../../common/ecs/ecs_fields'; @@ -33,7 +33,10 @@ export const buildQuery = ({ defaultIndex, docValueFields, }: HostAuthenticationsRequestOptions) => { - const esFields = reduceFields(authenticationsFields, { ...hostFieldsMap, ...sourceFieldsMap }); + const esFields = reduceFields(authenticationsFields, { + ...hostFieldsMap, + ...sourceFieldsMap, + }) as string[]; const filter = [ ...createQueryFilterClauses(filterQuery), @@ -69,7 +72,10 @@ export const buildQuery = ({ terms: { size: querySize, field: 'user.name', - order: [{ 'successes.doc_count': 'desc' }, { 'failures.doc_count': 'desc' }], + order: [ + { 'successes.doc_count': 'desc' as const }, + { 'failures.doc_count': 'desc' as const }, + ] as estypes.TermsAggregationOrder, }, aggs: { failures: { @@ -83,7 +89,7 @@ export const buildQuery = ({ top_hits: { size: 1, _source: esFields, - sort: [{ '@timestamp': { order: 'desc' } }], + sort: [{ '@timestamp': { order: 'desc' as const } }], }, }, }, @@ -99,7 +105,7 @@ export const buildQuery = ({ top_hits: { size: 1, _source: esFields, - sort: [{ '@timestamp': { order: 'desc' } }], + sort: [{ '@timestamp': { order: 'desc' as const } }], }, }, }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts index a5c82688e01ba..01473a4368dbc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts @@ -54,7 +54,7 @@ export const buildHostsKpiAuthenticationsQuery = ({ authentication_success_histogram: { auto_date_histogram: { field: '@timestamp', - buckets: '6', + buckets: 6, }, aggs: { count: { @@ -76,7 +76,7 @@ export const buildHostsKpiAuthenticationsQuery = ({ authentication_failure_histogram: { auto_date_histogram: { field: '@timestamp', - buckets: '6', + buckets: 6, }, aggs: { count: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts index 0e0cbd8a2649d..5ea2d1aa40780 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts @@ -41,7 +41,7 @@ export const buildHostsKpiHostsQuery = ({ hosts_histogram: { auto_date_histogram: { field: '@timestamp', - buckets: '6', + buckets: 6, }, aggs: { count: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts index a702982ab8253..0471644d11bbe 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts @@ -41,7 +41,7 @@ export const buildHostsKpiUniqueIpsQuery = ({ unique_source_ips_histogram: { auto_date_histogram: { field: '@timestamp', - buckets: '6', + buckets: 6, }, aggs: { count: { @@ -59,7 +59,7 @@ export const buildHostsKpiUniqueIpsQuery = ({ unique_destination_ips_histogram: { auto_date_histogram: { field: '@timestamp', - buckets: '6', + buckets: 6, }, aggs: { count: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts index e040a94fb3515..c58e450806849 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts @@ -24,6 +24,7 @@ export const hostOverview: SecuritySolutionFactory = { options: HostOverviewRequestOptions, response: IEsSearchResponse ): Promise => { + // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations const aggregations: OverviewHostHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewHostQuery(options))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts index 2c237ab75bcbb..897ae633076a2 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts @@ -291,7 +291,8 @@ export const buildOverviewHostQuery = ({ }, size: 0, }, - }; + } as const; + // @ts-expect-error @elastic-elasticsearch readonly [] is not assignable to mutable QueryContainer[] return dslQuery; }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts index f036d7c943663..1df0d60d54a88 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { createQueryFilterClauses } from '../../../../../../utils/build_query'; import { reduceFields } from '../../../../../../utils/build_query/reduce_fields'; import { @@ -24,8 +24,8 @@ export const buildQuery = ({ const processUserFields = reduceFields(uncommonProcessesFields, { ...processFieldsMap, ...userFieldsMap, - }); - const hostFields = reduceFields(uncommonProcessesFields, hostFieldsMap); + }) as string[]; + const hostFields = reduceFields(uncommonProcessesFields, hostFieldsMap) as string[]; const filter = [ ...createQueryFilterClauses(filterQuery), { @@ -60,21 +60,21 @@ export const buildQuery = ({ field: 'process.name', order: [ { - host_count: 'asc', + host_count: 'asc' as const, }, { - _count: 'asc', + _count: 'asc' as const, }, { - _key: 'asc', + _key: 'asc' as const, }, - ], + ] as estypes.TermsAggregationOrder, }, aggregations: { process: { top_hits: { size: 1, - sort: [{ '@timestamp': { order: 'desc' } }], + sort: [{ '@timestamp': { order: 'desc' as const } }], _source: processUserFields, }, }, @@ -120,7 +120,7 @@ export const buildQuery = ({ 'event.action': 'executed', }, }, - ], + ] as estypes.QueryContainer[], }, }, { @@ -146,7 +146,7 @@ export const buildQuery = ({ 'event.action': 'process_started', }, }, - ], + ] as estypes.QueryContainer[], }, }, { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts index dfb157e148c9e..40b22a31691b6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts @@ -63,6 +63,7 @@ export const matrixHistogram: SecuritySolutionFactory = { results: { hits: { total: { value: 1, relation: 'eq' }, - max_score: null, + max_score: undefined, hits: [ { _index: 'auditbeat-7.8.0-2020.11.23-000004', _id: 'wRCuOnYB7WTwW_GluxL8', - _score: null, + _score: undefined, _source: { host: { hostname: 'internal-ci-immutable-rm-ubuntu-2004-big2-1607296224012102773', @@ -188,12 +188,12 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { results: { hits: { total: { value: 5, relation: 'eq' }, - max_score: null, + max_score: undefined, hits: [ { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'dd4fa2d4bd-1523631609876537', - _score: null, + _score: undefined, _source: { destination: { geo: { @@ -217,12 +217,12 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { results: { hits: { total: { value: 5, relation: 'eq' }, - max_score: null, + max_score: undefined, hits: [ { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'dd4fa2d4bd-1523631609876537', - _score: null, + _score: undefined, _source: { destination: { as: { number: 15169, organization: { name: 'Google LLC' } } }, }, @@ -244,12 +244,12 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { results: { hits: { total: { value: 5, relation: 'eq' }, - max_score: null, + max_score: undefined, hits: [ { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'dd4fa2d4bd-1523631486500511', - _score: null, + _score: undefined, _source: { source: { geo: { @@ -273,12 +273,12 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { results: { hits: { total: { value: 5, relation: 'eq' }, - max_score: null, + max_score: undefined, hits: [ { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'dd4fa2d4bd-1523631486500511', - _score: null, + _score: undefined, _source: { source: { as: { number: 15169, organization: { name: 'Google LLC' } } }, }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts index d1d0c44d9b61b..e5a508663a2e0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts @@ -40,7 +40,7 @@ const getAggs = (type: string, ip: string) => { _source: [`${type}.as`], sort: [ { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, ], }, @@ -60,7 +60,7 @@ const getAggs = (type: string, ip: string) => { _source: [`${type}.geo`], sort: [ { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, ], }, @@ -87,7 +87,7 @@ const getHostAggs = (ip: string) => { _source: ['host'], sort: [ { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, ], }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts index 5ab2beaabd3cb..9a73fb30a074d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts @@ -24,6 +24,7 @@ import { getDnsEdges } from './helpers'; import { buildDnsQuery } from './query.dns_network.dsl'; export const networkDns: SecuritySolutionFactory = { + // @ts-expect-error dns_name_query_count is incompatbile. Maybe' is not assignable to type 'string | undefined buildDsl: (options: NetworkDnsRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts index 259b45f436124..6a36e113b62a7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts @@ -24,6 +24,7 @@ import { getHttpEdges } from './helpers'; import { buildHttpQuery } from './query.http_network.dsl'; export const networkHttp: SecuritySolutionFactory = { + // @ts-expect-error dns_name_query_count is not conpatible with @elastic/elasticsearch buildDsl: (options: NetworkHttpRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/dns/index.ts index 9f14b16971b5f..7ef0e6e303528 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/dns/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/dns/index.ts @@ -28,6 +28,7 @@ export const networkKpiDns: SecuritySolutionFactory = { return { ...response, inspect, + // @ts-expect-error code doesn't handle TotalHits dnsQueries: response.rawResponse.hits.total, }; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts index 2956110239e69..5327a2396cdac 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/kpi/network_events/index.ts @@ -28,6 +28,7 @@ export const networkKpiNetworkEvents: SecuritySolutionFactory = { + // @ts-expect-error auto_date_histogram.buckets is incompatible buildDsl: (options: NetworkKpiUniquePrivateIpsRequestOptions) => buildUniquePrivateIpsQuery(options), parse: async ( diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts index 7a7650b0f77d7..1f85a119f3c8e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts @@ -24,6 +24,7 @@ export const networkOverview: SecuritySolutionFactory = options: NetworkOverviewRequestOptions, response: IEsSearchResponse ): Promise => { + // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations const aggregations: OverviewNetworkHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewNetworkQuery(options))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 05058e3ee7a2d..172c864f7ee4f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -53,6 +53,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory( getDataFromSourceHits, + // @ts-expect-error @elastic/elasticsearch _source is optional _source ); const fieldsData = await getDataSafety( diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index 4236c782d6c68..211c477027eec 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -9,7 +9,6 @@ import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest, - SearchResponse, } from '../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../ml/server'; import { SIGNALS_ID, INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; @@ -167,14 +166,13 @@ export const getRulesUsage = async ( }; try { - const { body: ruleResults } = await esClient.search>( - ruleSearchOptions - ); + const { body: ruleResults } = await esClient.search(ruleSearchOptions); if (ruleResults.hits?.hits?.length > 0) { rulesUsage = ruleResults.hits.hits.reduce((usage, hit) => { - const isElastic = isElasticRule(hit._source.alert.tags); - const isEnabled = hit._source.alert.enabled; + // @ts-expect-error _source is optional + const isElastic = isElasticRule(hit._source?.alert.tags); + const isEnabled = Boolean(hit._source?.alert.enabled); return updateRulesUsage({ isElastic, isEnabled }, usage); }, initialRulesUsage); diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index c0cf71fab0558..ef001edd429fd 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -50,6 +50,7 @@ async function getSpacesUsage( let resp: SpacesAggregationResponse | undefined; try { + // @ts-expect-error `SearchResponse['hits']['total']` incorrectly expects `number` type instead of `{ value: number }`. ({ body: resp } = await esClient.search({ index: kibanaIndex, body: { diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts index a4fb54a06ace8..34851073bd8a2 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -4,18 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; +import type { ESSearchRequest } from '../../../../typings/elasticsearch'; -import { ESSearchBody, ESSearchRequest } from '../../../../typings/elasticsearch'; -import { SortOrder } from '../../../../typings/elasticsearch/aggregations'; - -type BuildSortedEventsQueryOpts = Pick & - Pick, 'index' | 'size'>; +interface BuildSortedEventsQueryOpts { + aggs?: Record; + track_total_hits: boolean | number; + index: estypes.Indices; + size: number; +} export interface BuildSortedEventsQuery extends BuildSortedEventsQueryOpts { filter: unknown; from: string; to: string; - sortOrder?: SortOrder | undefined; + sortOrder?: 'asc' | 'desc'; searchAfterSortId: string | number | undefined; timeField: string; } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index 09487f1ebe936..f89f1133d0248 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -23,6 +23,8 @@ import { EuiLink, } from '@elastic/eui'; import { DocLinksStart, HttpSetup } from 'kibana/public'; +import type { estypes } from '@elastic/elasticsearch'; + import { XJson } from '../../../../../../src/plugins/es_ui_shared/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { @@ -39,6 +41,10 @@ import { buildSortedEventsQuery } from '../../../common/build_sorted_events_quer import { EsQueryAlertParams } from './types'; import { IndexSelectPopover } from '../components/index_select_popover'; +function totalHitsToNumber(total: estypes.HitsMetadata['total']): number { + return typeof total === 'number' ? total : total.value; +} + const DEFAULT_VALUES = { THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, QUERY: `{ @@ -191,7 +197,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< setTestQueryResult( i18n.translate('xpack.stackAlerts.esQuery.ui.numQueryMatchesText', { defaultMessage: 'Query matched {count} documents in the last {window}.', - values: { count: hits.total, window }, + values: { count: totalHitsToNumber(hits.total), window }, }) ); } catch (err) { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index f0596a9fcb964..bc5d5c41c5cce 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import type { estypes } from '@elastic/elasticsearch'; import { AlertExecutorOptions, AlertInstanceContext } from '../../../../alerting/server'; import { EsQueryAlertParams } from './alert_type_params'; -import { ESSearchHit } from '../../../../../../typings/elasticsearch'; // alert type context provided to actions @@ -29,7 +29,7 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext { // threshold conditions conditions: string; // query matches - hits: ESSearchHit[]; + hits: estypes.Hit[]; } export function addMessages( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 66984e46de602..92f6e731f7114 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -135,6 +135,7 @@ describe('alertType', () => { const searchResult: ESSearchResponse = generateResults([]); alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible agregations type elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) ); @@ -194,6 +195,7 @@ describe('alertType', () => { }, ]); alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) ); @@ -247,6 +249,7 @@ describe('alertType', () => { const newestDocumentTimestamp = previousTimestamp + 1000; alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -309,6 +312,7 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -370,6 +374,7 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -413,6 +418,7 @@ describe('alertType', () => { const newestDocumentTimestamp = oldestDocumentTimestamp + 5000; alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -458,6 +464,7 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults( [ @@ -521,6 +528,7 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + // @ts-expect-error not compatible response type elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults( [ diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index d1cbeeb46fac0..990ab9c1f6002 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import type { estypes } from '@elastic/elasticsearch'; import { Logger } from 'src/core/server'; import { AlertType, AlertExecutorOptions } from '../../types'; import { ActionContext, EsQueryAlertActionContext, addMessages } from './action_context'; @@ -18,7 +19,6 @@ import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ComparatorFns, getHumanReadableComparator } from '../lib'; import { parseDuration } from '../../../../alerting/server'; import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; -import { ESSearchHit } from '../../../../../../typings/elasticsearch'; export const ES_QUERY_ID = '.es-query'; @@ -217,7 +217,7 @@ export function getAlertType( const { body: searchResult } = await esClient.search(query); if (searchResult.hits.hits.length > 0) { - const numMatches = searchResult.hits.total.value; + const numMatches = (searchResult.hits.total as estypes.TotalHits).value; logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query has ${numMatches} matches`); // apply the alert condition @@ -251,7 +251,7 @@ export function getAlertType( // update the timestamp based on the current search results const firstValidTimefieldSort = getValidTimefieldSort( - searchResult.hits.hits.find((hit: ESSearchHit) => getValidTimefieldSort(hit.sort))?.sort + searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort ); if (firstValidTimefieldSort) { timestamp = firstValidTimefieldSort; @@ -265,7 +265,7 @@ export function getAlertType( } } -function getValidTimefieldSort(sortValues: Array = []): undefined | string { +function getValidTimefieldSort(sortValues: Array = []): undefined | string { for (const sortValue of sortValues) { const sortDate = tryToParseAsDate(sortValue); if (sortDate) { @@ -273,7 +273,7 @@ function getValidTimefieldSort(sortValues: Array = []): undefin } } } -function tryToParseAsDate(sortValue?: string | number): undefined | string { +function tryToParseAsDate(sortValue?: string | number | null): undefined | string { const sortDate = typeof sortValue === 'string' ? Date.parse(sortValue) : sortValue; if (sortDate && !isNaN(sortDate)) { return new Date(sortDate).toISOString(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts index a416056217442..1e26ea09618d5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts @@ -7,8 +7,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { Logger } from 'src/core/server'; -import { ApiResponse } from '@elastic/elasticsearch'; -import { SearchResponse } from 'elasticsearch'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { Query, IIndexPattern, @@ -110,7 +109,7 @@ export async function executeEsQueryFactory( return async ( gteDateTime: Date | null, ltDateTime: Date | null - ): Promise> | undefined> => { + ): Promise | undefined> => { let esFormattedQuery; if (indexQuery) { const gteEpochDateTime = gteDateTime ? new Date(gteDateTime).getTime() : null; @@ -194,7 +193,7 @@ export async function executeEsQueryFactory( }, }; - let esResult: ApiResponse> | undefined; + let esResult: estypes.SearchResponse | undefined; try { ({ body: esResult } = await esClient.search(esQuery)); } catch (err) { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 866b25d239db7..15a6564395c16 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -7,8 +7,7 @@ import _ from 'lodash'; import { Logger } from 'src/core/server'; -import { ApiResponse } from '@elastic/elasticsearch'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder'; import { AlertServices } from '../../../../alerting/server'; import { @@ -23,7 +22,7 @@ export type LatestEntityLocation = GeoContainmentInstanceState; // Flatten agg results and get latest locations for each entity export function transformResults( - results: SearchResponse | undefined, + results: estypes.SearchResponse | undefined, dateField: string, geoField: string ): Map { @@ -164,7 +163,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ ); // Start collecting data only on the first cycle - let currentIntervalResults: ApiResponse> | undefined; + let currentIntervalResults: estypes.SearchResponse | undefined; if (!currIntervalStartTime) { log.debug(`alert ${GEO_CONTAINMENT_ID}:${alertId} alert initialized. Collecting data`); // Consider making first time window configurable? @@ -177,6 +176,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ } const currLocationMap: Map = transformResults( + // @ts-expect-error body doesn't exist on currentIntervalResults currentIntervalResults?.body, params.dateField, params.geoField diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index 46d8478a7ecfa..05b74f4340d09 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -6,78 +6,88 @@ */ import { first, take, bufferCount } from 'rxjs/operators'; -import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { - WorkloadAggregation, + TaskTypeAggregation, + WorkloadAggregationResponse, + ScheduleDensityHistogram, createWorkloadAggregator, padBuckets, estimateRecurringTaskScheduling, } from './workload_statistics'; import { ConcreteTaskInstance } from '../task'; -import { AggregationResultOf, ESSearchResponse } from '../../../../../typings/elasticsearch'; + import { times } from 'lodash'; import { taskStoreMock } from '../task_store.mock'; import { of, Subject } from 'rxjs'; import { sleep } from '../test_utils'; +import { estypes } from '@elastic/elasticsearch'; + +type ResponseWithAggs = Omit, 'aggregations'> & { + aggregations: WorkloadAggregationResponse; +}; -type MockESResult = ESSearchResponse< - ConcreteTaskInstance, - { - body: WorkloadAggregation; - } ->; +const asApiResponse = (body: ResponseWithAggs) => + elasticsearchServiceMock + .createSuccessTransportRequestPromise(body as estypes.SearchResponse) + .then((res) => res.body as ResponseWithAggs); describe('Workload Statistics Aggregator', () => { test('queries the Task Store at a fixed interval for the current workload', async () => { const taskStore = taskStoreMock.create({}); - taskStore.aggregate.mockResolvedValue({ - hits: { - hits: [], - max_score: 0, - total: { value: 0, relation: 'eq' }, - }, - took: 1, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 1, - failed: 0, - }, - aggregations: { - taskType: { - buckets: [], - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, + taskStore.aggregate.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + max_score: 0, + total: { value: 0, relation: 'eq' }, }, - schedule: { - buckets: [], - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 1, + failed: 0, }, - idleTasks: { - doc_count: 0, - overdue: { - doc_count: 0, + aggregations: { + taskType: { + buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, - scheduleDensity: { - buckets: [ - { - key: '2020-10-02T15:18:37.274Z-2020-10-02T15:19:36.274Z', - from: 1.601651917274e12, - from_as_string: '2020-10-02T15:18:37.274Z', - to: 1.601651976274e12, - to_as_string: '2020-10-02T15:19:36.274Z', - doc_count: 0, - histogram: { - buckets: [], + schedule: { + buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + // The `FiltersAggregate` doesn't cover the case of a nested `AggregationContainer`, in which `FiltersAggregate` + // would not have a `buckets` property, but rather a keyed property that's inferred from the request. + // @ts-expect-error + idleTasks: { + doc_count: 0, + overdue: { + doc_count: 0, + }, + scheduleDensity: { + buckets: [ + { + key: '2020-10-02T15:18:37.274Z-2020-10-02T15:19:36.274Z', + from: 1.601651917274e12, + from_as_string: '2020-10-02T15:18:37.274Z', + to: 1.601651976274e12, + to_as_string: '2020-10-02T15:19:36.274Z', + doc_count: 0, + histogram: { + buckets: [], + }, }, - }, - ], + ], + }, }, }, - }, - } as MockESResult); + }) + ); const workloadAggregator = createWorkloadAggregator( taskStore, @@ -146,8 +156,8 @@ describe('Workload Statistics Aggregator', () => { }); }); - const mockAggregatedResult: () => MockESResult = () => - ({ + const mockAggregatedResult = () => + asApiResponse({ hits: { hits: [], max_score: 0, @@ -228,6 +238,9 @@ describe('Workload Statistics Aggregator', () => { }, ], }, + // The `FiltersAggregate` doesn't cover the case of a nested `AggregationContainer`, in which `FiltersAggregate` + // would not have a `buckets` property, but rather a keyed property that's inferred from the request. + // @ts-expect-error idleTasks: { doc_count: 13, overdue: { @@ -240,7 +253,7 @@ describe('Workload Statistics Aggregator', () => { }, }, }, - } as MockESResult); + }); test('returns a summary of the workload by task type', async () => { const taskStore = taskStoreMock.create({}); @@ -440,16 +453,20 @@ describe('Workload Statistics Aggregator', () => { const taskStore = taskStoreMock.create({}); taskStore.aggregate .mockResolvedValueOnce( - setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { - idle: 2, - }) + mockAggregatedResult().then((res) => + setTaskTypeCount(res, 'alerting_telemetry', { + idle: 2, + }) + ) ) .mockRejectedValueOnce(new Error('Elasticsearch has gone poof')) .mockResolvedValueOnce( - setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { - idle: 1, - failed: 1, - }) + mockAggregatedResult().then((res) => + setTaskTypeCount(res, 'alerting_telemetry', { + idle: 1, + failed: 1, + }) + ) ); const logger = loggingSystemMock.create().get(); const workloadAggregator = createWorkloadAggregator(taskStore, of(true), 10, 3000, logger); @@ -502,7 +519,7 @@ describe('Workload Statistics Aggregator', () => { reject(new Error(`Elasticsearch is still poof`)); } - return setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { + return setTaskTypeCount(await mockAggregatedResult(), 'alerting_telemetry', { idle: 2, }); }); @@ -631,6 +648,7 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668048128, + // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:28.128Z', to: 1601668108128, to_as_string: '2020-10-02T19:48:28.128Z', @@ -647,6 +665,7 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668046000, + // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:26.000Z', to: 1601668076000, to_as_string: '2020-10-02T19:47:56.000Z', @@ -724,6 +743,7 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671183000, + // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:43.000Z', to: 1601671213000, to_as_string: '2020-10-02T20:40:13.000Z', @@ -753,6 +773,7 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671185793, + // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:45.793Z', to: 1601671245793, to_as_string: '2020-10-02T20:40:45.793Z', @@ -782,6 +803,7 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2021-02-02T10:08:32.161Z-2021-02-02T10:09:32.161Z', from: 1612260512161, + // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2021-02-02T10:08:32.161Z', to: 1612260572161, to_as_string: '2021-02-02T10:09:32.161Z', @@ -898,16 +920,12 @@ describe('padBuckets', () => { }); function setTaskTypeCount( - { aggregations }: MockESResult, + { aggregations, ...rest }: ResponseWithAggs, taskType: string, status: Record ) { - const taskTypes = aggregations!.taskType as AggregationResultOf< - WorkloadAggregation['aggs']['taskType'], - {} - >; const buckets = [ - ...taskTypes.buckets.filter(({ key }) => key !== taskType), + ...(aggregations.taskType as TaskTypeAggregation).buckets.filter(({ key }) => key !== taskType), { key: taskType, doc_count: Object.values(status).reduce((sum, count) => sum + count, 0), @@ -920,9 +938,14 @@ function setTaskTypeCount( }, }, ]; - return ({ + return { + ...rest, hits: { - total: { value: buckets.reduce((sum, bucket) => sum + bucket.doc_count, 0) }, + ...rest.hits, + total: { + value: buckets.reduce((sum, bucket) => sum + bucket.doc_count, 0), + relation: 'eq' as estypes.TotalHitsRelation, + }, }, aggregations: { ...aggregations, @@ -931,7 +954,7 @@ function setTaskTypeCount( buckets, }, }, - } as {}) as MockESResult; + }; } /** * @@ -951,7 +974,7 @@ function mockHistogram( to: number, interval: number, foundBuckets: Array -) { +): ScheduleDensityHistogram { const now = Date.now(); const fromDate = new Date(now + from); const toDate = new Date(now + to); diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 08850c8650519..c79b310822c3e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -10,6 +10,7 @@ import { mergeMap, map, filter, switchMap, catchError } from 'rxjs/operators'; import { Logger } from 'src/core/server'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { keyBy, mapValues } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; import { AggregationResultOf } from '../../../../../typings/elasticsearch'; @@ -85,9 +86,11 @@ export interface WorkloadAggregation { // The type of a bucket in the scheduleDensity range aggregation type ScheduleDensityResult = AggregationResultOf< + // @ts-expect-error AggregationRange reqires from: number WorkloadAggregation['aggs']['idleTasks']['aggs']['scheduleDensity'], {} >['buckets'][0]; +// @ts-expect-error cannot infer histogram type ScheduledIntervals = ScheduleDensityResult['histogram']['buckets'][0]; // Set an upper bound just in case a customer sets a really high refresh rate @@ -134,7 +137,9 @@ export function createWorkloadAggregator( field: 'task.runAt', ranges: [ { + // @ts-expect-error @elastic/elasticsearch The `AggregationRange` type only supports `double` for `from` and `to` but it can be a string too for time based ranges from: `now`, + // @ts-expect-error @elastic/elasticsearch The `AggregationRange` type only supports `double` for `from` and `to` but it can be a string too for time based ranges to: `now+${asInterval(scheduleDensityBuckets * pollInterval)}`, }, ], @@ -170,19 +175,11 @@ export function createWorkloadAggregator( map((result) => { const { aggregations, - hits: { - total: { value: count }, - }, + hits: { total }, } = result; + const count = typeof total === 'number' ? total : total.value; - if ( - !( - aggregations?.taskType && - aggregations?.schedule && - aggregations?.idleTasks?.overdue && - aggregations?.idleTasks?.scheduleDensity - ) - ) { + if (!hasAggregations(aggregations)) { throw new Error(`Invalid workload: ${JSON.stringify(result)}`); } @@ -240,7 +237,9 @@ export function padBuckets( pollInterval: number, scheduleDensity: ScheduleDensityResult ): number[] { + // @ts-expect-error cannot infer histogram if (scheduleDensity.from && scheduleDensity.to && scheduleDensity.histogram?.buckets?.length) { + // @ts-expect-error cannot infer histogram const { histogram, from, to } = scheduleDensity; const firstBucket = histogram.buckets[0].key; const lastBucket = histogram.buckets[histogram.buckets.length - 1].key; @@ -354,3 +353,84 @@ export function summarizeWorkloadStat( status: HealthStatus.OK, }; } + +function hasAggregations( + aggregations?: Record +): aggregations is WorkloadAggregationResponse { + return !!( + aggregations?.taskType && + aggregations?.schedule && + (aggregations?.idleTasks as IdleTasksAggregation)?.overdue && + (aggregations?.idleTasks as IdleTasksAggregation)?.scheduleDensity + ); +} +export interface WorkloadAggregationResponse { + taskType: TaskTypeAggregation; + schedule: ScheduleAggregation; + idleTasks: IdleTasksAggregation; + [otherAggs: string]: estypes.Aggregate; +} +export interface TaskTypeAggregation extends estypes.FiltersAggregate { + buckets: Array<{ + doc_count: number; + key: string | number; + status: { + buckets: Array<{ + doc_count: number; + key: string | number; + }>; + doc_count_error_upper_bound?: number | undefined; + sum_other_doc_count?: number | undefined; + }; + }>; + doc_count_error_upper_bound?: number | undefined; + sum_other_doc_count?: number | undefined; +} +export interface ScheduleAggregation extends estypes.FiltersAggregate { + buckets: Array<{ + doc_count: number; + key: string | number; + }>; + doc_count_error_upper_bound?: number | undefined; + sum_other_doc_count?: number | undefined; +} + +export type ScheduleDensityHistogram = DateRangeBucket & { + histogram: { + buckets: Array< + DateHistogramBucket & { + interval: { + buckets: Array<{ + doc_count: number; + key: string | number; + }>; + doc_count_error_upper_bound?: number | undefined; + sum_other_doc_count?: number | undefined; + }; + } + >; + }; +}; +export interface IdleTasksAggregation extends estypes.FiltersAggregate { + doc_count: number; + scheduleDensity: { + buckets: ScheduleDensityHistogram[]; + }; + overdue: { + doc_count: number; + }; +} + +interface DateHistogramBucket { + doc_count: number; + key: number; + key_as_string: string; +} +interface DateRangeBucket { + key: string; + to?: number; + from?: number; + to_as_string?: string; + from_as_string?: string; + doc_count: number; +} diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 57a4ab320367d..9e31ab9f0cb4e 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -6,7 +6,7 @@ */ import _ from 'lodash'; -import { asUpdateByQuery, shouldBeOneOf, mustBeAllOf } from './query_clauses'; +import { shouldBeOneOf, mustBeAllOf } from './query_clauses'; import { updateFieldsAndMarkAsFailed, @@ -41,25 +41,23 @@ describe('mark_available_tasks_as_claimed', () => { retryAt: claimOwnershipUntil, }; - expect( - asUpdateByQuery({ - query: mustBeAllOf( - // Either a task with idle status and runAt <= now or - // status running or claiming with a retryAt <= now. - shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt) - ), - update: updateFieldsAndMarkAsFailed( - fieldUpdates, - claimTasksById || [], - definitions.getAllTypes(), - [], - Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { - return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; - }, {}) - ), - sort: SortByRunAtAndRetryAt, - }) - ).toEqual({ + expect({ + query: mustBeAllOf( + // Either a task with idle status and runAt <= now or + // status running or claiming with a retryAt <= now. + shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt) + ), + script: updateFieldsAndMarkAsFailed( + fieldUpdates, + claimTasksById || [], + definitions.getAllTypes(), + [], + Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { + return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; + }, {}) + ), + sort: SortByRunAtAndRetryAt, + }).toEqual({ query: { bool: { must: [ @@ -114,7 +112,6 @@ if (doc['task.runAt'].size()!=0) { }, }, }, - seq_no_primary_term: true, script: { source: ` if (params.claimableTaskTypes.contains(ctx._source.task.taskType)) { diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 8598980a4e236..2437e893abf37 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -4,27 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { - SortClause, + ScriptBasedSortClause, ScriptClause, - ExistsFilter, - TermFilter, - RangeFilter, mustBeAllOf, MustCondition, - BoolClauseWithAnyCondition, - ShouldCondition, - FilterCondition, + MustNotCondition, } from './query_clauses'; -export const TaskWithSchedule: ExistsFilter = { - exists: { field: 'task.schedule' }, -}; -export function taskWithLessThanMaxAttempts( - type: string, - maxAttempts: number -): MustCondition { +export function taskWithLessThanMaxAttempts(type: string, maxAttempts: number): MustCondition { return { bool: { must: [ @@ -41,7 +30,7 @@ export function taskWithLessThanMaxAttempts( }; } -export function tasksOfType(taskTypes: string[]): ShouldCondition { +export function tasksOfType(taskTypes: string[]): estypes.QueryContainer { return { bool: { should: [...taskTypes].map((type) => ({ term: { 'task.taskType': type } })), @@ -51,7 +40,7 @@ export function tasksOfType(taskTypes: string[]): ShouldCondition { export function tasksClaimedByOwner( taskManagerId: string, - ...taskFilters: Array | ShouldCondition> + ...taskFilters: estypes.QueryContainer[] ) { return mustBeAllOf( { @@ -64,15 +53,13 @@ export function tasksClaimedByOwner( ); } -export const IdleTaskWithExpiredRunAt: MustCondition = { +export const IdleTaskWithExpiredRunAt: MustCondition = { bool: { must: [{ term: { 'task.status': 'idle' } }, { range: { 'task.runAt': { lte: 'now' } } }], }, }; -// TODO: Fix query clauses to support this -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const InactiveTasks: BoolClauseWithAnyCondition = { +export const InactiveTasks: MustNotCondition = { bool: { must_not: [ { @@ -85,7 +72,7 @@ export const InactiveTasks: BoolClauseWithAnyCondition = { }, }; -export const RunningOrClaimingTaskWithExpiredRetryAt: MustCondition = { +export const RunningOrClaimingTaskWithExpiredRetryAt: MustCondition = { bool: { must: [ { @@ -98,7 +85,7 @@ export const RunningOrClaimingTaskWithExpiredRetryAt: MustCondition; export const updateFieldsAndMarkAsFailed = ( fieldUpdates: { diff --git a/x-pack/plugins/task_manager/server/queries/query_clauses.test.ts b/x-pack/plugins/task_manager/server/queries/query_clauses.test.ts index f144c8921263f..5f99682718f1d 100644 --- a/x-pack/plugins/task_manager/server/queries/query_clauses.test.ts +++ b/x-pack/plugins/task_manager/server/queries/query_clauses.test.ts @@ -6,29 +6,21 @@ */ import _ from 'lodash'; -import { - MustCondition, - shouldBeOneOf, - mustBeAllOf, - ExistsFilter, - TermFilter, - RangeFilter, - matchesClauses, -} from './query_clauses'; +import { MustCondition, shouldBeOneOf, mustBeAllOf, matchesClauses } from './query_clauses'; describe('matchesClauses', () => { test('merges multiple types of Bool Clauses into one', () => { - const TaskWithSchedule: ExistsFilter = { + const TaskWithSchedule = { exists: { field: 'task.schedule' }, }; - const IdleTaskWithExpiredRunAt: MustCondition = { + const IdleTaskWithExpiredRunAt: MustCondition = { bool: { must: [{ term: { 'task.status': 'idle' } }, { range: { 'task.runAt': { lte: 'now' } } }], }, }; - const RunningTask: MustCondition = { + const RunningTask: MustCondition = { bool: { must: [{ term: { 'task.status': 'running' } }], }, @@ -37,10 +29,7 @@ describe('matchesClauses', () => { expect( matchesClauses( mustBeAllOf(TaskWithSchedule), - shouldBeOneOf( - RunningTask, - IdleTaskWithExpiredRunAt - ) + shouldBeOneOf(RunningTask, IdleTaskWithExpiredRunAt) ) ).toMatchObject({ bool: { diff --git a/x-pack/plugins/task_manager/server/queries/query_clauses.ts b/x-pack/plugins/task_manager/server/queries/query_clauses.ts index 4d59bec4e3625..97c41295eece3 100644 --- a/x-pack/plugins/task_manager/server/queries/query_clauses.ts +++ b/x-pack/plugins/task_manager/server/queries/query_clauses.ts @@ -5,167 +5,27 @@ * 2.0. */ -/** - * Terminology - * =========== - * The terms for the differenct clauses in an Elasticsearch query can be confusing, here are some - * clarifications that might help you understand the Typescript types we use here. - * - * Given the following Query: - * { - * "query": { (1) - * "bool": { (2) - * "must": - * [ - * (3) { "term" : { "tag" : "wow" } }, - * { "term" : { "tag" : "elasticsearch" } }, - * { - * "must" : { "term" : { "user" : "kimchy" } } - * } - * ] - * } - * } - * } - * - * These are referred to as: - * (1). BoolClause / BoolClauseWithAnyCondition - * (2). BoolCondition / AnyBoolCondition - * (3). BoolClauseFilter - * - */ - -export interface TermFilter { - term: { [field: string]: string | string[] }; -} -export interface RangeFilter { - range: { - [field: string]: { lte: string | number } | { lt: string | number } | { gt: string | number }; - }; -} -export interface ExistsFilter { - exists: { field: string }; -} - -type BoolClauseFilter = TermFilter | RangeFilter | ExistsFilter; -type BoolClauseFiltering = - | BoolClauseWithAnyCondition - | PinnedQuery - | T; +import { estypes } from '@elastic/elasticsearch'; -enum Conditions { - Should = 'should', - Must = 'must', - MustNot = 'must_not', - Filter = 'filter', +export interface MustCondition { + bool: Pick; } - -/** - * Describe a specific BoolClause Condition with a BoolClauseFilter on it, such as: - * ``` - * { - * must : [ - * T, ... - * ] - * } - * ``` - */ -type BoolCondition = { - [c in C]: Array>; -}; - -/** - * Describe a Bool clause with a specific Condition, such as: - * ``` - * { - * // described by BoolClause - * bool: { - * // described by BoolCondition - * must: [ - * T, ... - * ] - * } - * } - * ``` - */ -interface BoolClause { - bool: BoolCondition; +export interface MustNotCondition { + bool: Pick; } -/** - * Describe a Bool clause with mixed Conditions, such as: - * ``` - * { - * // described by BoolClause<...> - * bool: { - * // described by BoolCondition - * must : { - * ... - * }, - * // described by BoolCondition - * should : { - * ... - * } - * } - * } - * ``` - */ -type AnyBoolCondition = { - [Condition in Conditions]?: Array>; -}; - -/** - * Describe a Bool Condition with any Condition on it, so it can handle both: - * ``` - * { - * bool: { - * must : { - * ... - * } - * } - * } - * ``` - * - * and: - * - * ``` - * { - * bool: { - * must_not : { - * ... - * } - * } - * } - * ``` - */ -export interface BoolClauseWithAnyCondition { - bool: AnyBoolCondition; -} - -/** - * Describe the various Bool Clause Conditions we support, as specified in the Conditions enum - */ -export type ShouldCondition = BoolClause; -export type MustCondition = BoolClause; -export type MustNotCondition = BoolClause; -export type FilterCondition = BoolClause; - -export interface SortClause { +export interface ScriptBasedSortClause { _script: { type: string; order: string; - script: { - lang: string; - source: string; - params?: { [param: string]: string | string[] }; - }; + script: ScriptClause; }; } -export type SortOptions = string | SortClause | Array; export interface ScriptClause { source: string; lang: string; - params: { + params?: { [field: string]: | string | number @@ -175,33 +35,16 @@ export interface ScriptClause { }; } -export interface UpdateByQuery { - query: PinnedQuery | BoolClauseWithAnyCondition; - sort: SortOptions; - seq_no_primary_term: true; - script: ScriptClause; -} - -export interface PinnedQuery { - pinned: PinnedClause; -} +export type PinnedQuery = Pick; -export interface PinnedClause { - ids: string[]; - organic: BoolClauseWithAnyCondition; -} - -export function matchesClauses( - ...clauses: Array> -): BoolClauseWithAnyCondition { +type BoolClause = Pick; +export function matchesClauses(...clauses: BoolClause[]): BoolClause { return { bool: Object.assign({}, ...clauses.map((clause) => clause.bool)), }; } -export function shouldBeOneOf( - ...should: Array> -): ShouldCondition { +export function shouldBeOneOf(...should: estypes.QueryContainer[]) { return { bool: { should, @@ -209,9 +52,7 @@ export function shouldBeOneOf( }; } -export function mustBeAllOf( - ...must: Array> -): MustCondition { +export function mustBeAllOf(...must: estypes.QueryContainer[]) { return { bool: { must, @@ -219,9 +60,7 @@ export function mustBeAllOf( }; } -export function filterDownBy( - ...filter: Array> -): FilterCondition { +export function filterDownBy(...filter: estypes.QueryContainer[]) { return { bool: { filter, @@ -229,10 +68,10 @@ export function filterDownBy( }; } -export function asPinnedQuery( - ids: PinnedClause['ids'], - organic: PinnedClause['organic'] -): PinnedQuery { +export function asPinnedQuery( + ids: estypes.PinnedQuery['ids'], + organic: estypes.PinnedQuery['organic'] +): Pick { return { pinned: { ids, @@ -240,20 +79,3 @@ export function asPinnedQuery( }, }; } - -export function asUpdateByQuery({ - query, - update, - sort, -}: { - query: UpdateByQuery['query']; - update: UpdateByQuery['script']; - sort: UpdateByQuery['sort']; -}): UpdateByQuery { - return { - query, - sort, - seq_no_primary_term: true, - script: update, - }; -} diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts index bd1171d7fd2f8..8f1332ccd1f9f 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts @@ -15,7 +15,7 @@ import { SearchOpts, StoreOpts, UpdateByQueryOpts, UpdateByQuerySearchOpts } fro import { asTaskClaimEvent, ClaimTaskErr, TaskClaimErrorType, TaskEvent } from '../task_events'; import { asOk, asErr } from '../lib/result_type'; import { TaskTypeDictionary } from '../task_type_dictionary'; -import { BoolClauseWithAnyCondition, TermFilter } from '../queries/query_clauses'; +import type { MustNotCondition } from '../queries/query_clauses'; import { mockLogger } from '../test_utils'; import { TaskClaiming, OwnershipClaimingOpts, TaskClaimingOpts } from './task_claiming'; import { Observable } from 'rxjs'; @@ -177,7 +177,7 @@ describe('TaskClaiming', () => { result, args: { search: store.fetch.mock.calls[index][0] as SearchOpts & { - query: BoolClauseWithAnyCondition; + query: MustNotCondition; }, updateByQuery: store.updateByQuery.mock.calls[index] as [ UpdateByQuerySearchOpts, @@ -767,12 +767,12 @@ if (doc['task.runAt'].size()!=0) { ).map( (result, index) => (store.updateByQuery.mock.calls[index][0] as { - query: BoolClauseWithAnyCondition; + query: MustNotCondition; size: number; sort: string | string[]; script: { params: { - claimableTaskTypes: string[]; + [claimableTaskTypes: string]: string[]; }; }; }).script.params.claimableTaskTypes diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.ts index b4e11dbf81eb1..dce7824281658 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.ts @@ -27,13 +27,11 @@ import { } from '../task_events'; import { - asUpdateByQuery, shouldBeOneOf, mustBeAllOf, filterDownBy, asPinnedQuery, matchesClauses, - SortOptions, } from './query_clauses'; import { @@ -50,6 +48,7 @@ import { correctVersionConflictsForContinuation, TaskStore, UpdateByQueryResult, + SearchOpts, } from '../task_store'; import { FillPoolResult } from '../lib/fill_pool'; @@ -375,21 +374,21 @@ export class TaskClaiming { // the score seems to favor newer documents rather than older documents, so // if there are not pinned tasks being queried, we do NOT want to sort by score // at all, just by runAt/retryAt. - const sort: SortOptions = [SortByRunAtAndRetryAt]; + const sort: NonNullable = [SortByRunAtAndRetryAt]; if (claimTasksById && claimTasksById.length) { sort.unshift('_score'); } const apmTrans = apm.startTransaction(`taskManager markAvailableTasksAsClaimed`, 'taskManager'); const result = await this.taskStore.updateByQuery( - asUpdateByQuery({ + { query: matchesClauses( claimTasksById && claimTasksById.length ? mustBeAllOf(asPinnedQuery(claimTasksById, queryForScheduledTasks)) : queryForScheduledTasks, filterDownBy(InactiveTasks) ), - update: updateFieldsAndMarkAsFailed( + script: updateFieldsAndMarkAsFailed( { ownerId: this.taskStore.taskManagerId, retryAt: claimOwnershipUntil, @@ -400,7 +399,7 @@ export class TaskClaiming { pick(this.taskMaxAttempts, taskTypesToClaim) ), sort, - }), + }, { max_docs: size, } diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 25ee8cb0e2374..a44bddcdb8201 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import _ from 'lodash'; import { first } from 'rxjs/operators'; @@ -24,7 +24,6 @@ import { SavedObjectsErrorHelpers, } from 'src/core/server'; import { TaskTypeDictionary } from './task_type_dictionary'; -import { RequestEvent } from '@elastic/elasticsearch/lib/Transport'; import { mockLogger } from './test_utils'; const savedObjectsClient = savedObjectsRepositoryMock.create(); @@ -206,8 +205,8 @@ describe('TaskStore', () => { }); }); - async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { - esClient.search.mockResolvedValue(asApiResponse({ hits: { hits } })); + async function testFetch(opts?: SearchOpts, hits: Array> = []) { + esClient.search.mockResolvedValue(asApiResponse({ hits: { hits, total: hits.length } })); const result = await store.fetch(opts); @@ -564,9 +563,17 @@ describe('TaskStore', () => { }); }); -const asApiResponse = (body: T): RequestEvent => - ({ - body, - } as RequestEvent); +const asApiResponse = (body: Pick) => + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: body.hits, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: body.hits.hits.length, + total: 0, + skipped: 0, + }, + }); const randomId = () => `id-${_.random(1, 20)}`; diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 083ce1507e6e5..af0adad43baa4 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -10,7 +10,9 @@ */ import { Subject } from 'rxjs'; import { omit, defaults } from 'lodash'; -import { ReindexResponseBase, SearchResponse, UpdateDocumentByQueryResponse } from 'elasticsearch'; + +import type { estypes } from '@elastic/elasticsearch'; + import { SavedObject, SavedObjectsSerializer, @@ -31,7 +33,6 @@ import { } from './task'; import { TaskTypeDictionary } from './task_type_dictionary'; -import { ESSearchResponse, ESSearchBody } from '../../../../typings/elasticsearch'; export interface StoreOpts { esClient: ElasticsearchClient; @@ -43,18 +44,21 @@ export interface StoreOpts { } export interface SearchOpts { - sort?: string | object | object[]; - query?: object; + search_after?: Array; size?: number; + sort?: estypes.Sort; + query?: estypes.QueryContainer; seq_no_primary_term?: boolean; - search_after?: unknown[]; } -export type AggregationOpts = Pick, 'aggs'> & - Pick; +export interface AggregationOpts { + aggs: Record; + query?: estypes.QueryContainer; + size?: number; +} export interface UpdateByQuerySearchOpts extends SearchOpts { - script?: object; + script?: estypes.Script; } export interface UpdateByQueryOpts extends SearchOpts { @@ -304,7 +308,7 @@ export class TaskStore { body: { hits: { hits: tasks }, }, - } = await this.esClient.search>({ + } = await this.esClient.search({ index: this.index, ignore_unavailable: true, body: { @@ -315,7 +319,9 @@ export class TaskStore { return { docs: tasks + // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` .filter((doc) => this.serializer.isRawSavedObject(doc)) + // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` .map((doc) => this.serializer.rawToSavedObject(doc)) .map((doc) => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), @@ -330,10 +336,8 @@ export class TaskStore { aggs, query, size = 0, - }: TSearchRequest): Promise> { - const { body } = await this.esClient.search< - ESSearchResponse - >({ + }: TSearchRequest): Promise> { + const { body } = await this.esClient.search({ index: this.index, ignore_unavailable: true, body: ensureAggregationOnlyReturnsTaskObjects({ @@ -355,14 +359,14 @@ export class TaskStore { const { // eslint-disable-next-line @typescript-eslint/naming-convention body: { total, updated, version_conflicts }, - } = await this.esClient.updateByQuery({ + } = await this.esClient.updateByQuery({ index: this.index, ignore_unavailable: true, refresh: true, - max_docs, conflicts: 'proceed', body: { ...opts, + max_docs, query, }, }); @@ -374,7 +378,9 @@ export class TaskStore { ); return { + // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional total, + // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional updated, version_conflicts: conflictsCorrectedForContinuation, }; @@ -393,11 +399,13 @@ export class TaskStore { * `max_docs`, but we bias in favour of over zealous `version_conflicts` as that's the best indicator we * have for an unhealthy cluster distribution of Task Manager polling intervals */ + export function correctVersionConflictsForContinuation( - updated: ReindexResponseBase['updated'], - versionConflicts: ReindexResponseBase['version_conflicts'], + updated: estypes.ReindexResponse['updated'], + versionConflicts: estypes.ReindexResponse['version_conflicts'], maxDocs?: number -) { +): number { + // @ts-expect-error estypes.ReindexResponse['updated'] and estypes.ReindexResponse['version_conflicts'] can be undefined return maxDocs && versionConflicts + updated > maxDocs ? maxDocs - updated : versionConflicts; } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts index 64d9aee7b0ac7..3db9f06bd8bf6 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts @@ -4,30 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from 'src/core/server'; -// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html -export interface ESLicense { - status: string; - uid: string; - hkey: string; - type: string; - issue_date: string; - issue_date_in_millis: number; - expiry_date: string; - expiry_date_in_millis: number; - max_nodes: number; - issued_to: string; - issuer: string; - start_date_in_millis: number; - max_resource_units: number; -} +export type ESLicense = estypes.LicenseInformation; let cachedLicense: ESLicense | undefined; async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { - const { body } = await esClient.license.get<{ license: ESLicense }>({ + const { body } = await esClient.license.get({ local, // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. accept_enterprise: true, diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index 12f2f24502ce0..6ddb10e825684 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { coreMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { getStatsWithXpack } from './get_stats_with_xpack'; @@ -81,8 +82,8 @@ function mockEsClient() { body: { cluster_uuid: 'test', cluster_name: 'test', - version: { number: '8.0.0' }, - }, + version: { number: '8.0.0' } as estypes.ElasticsearchVersionInfo, + } as estypes.RootNodeInfoResponse, } ); diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index bfe2f47078569..bb2a1b278a5c2 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -41,6 +41,7 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen query, fields, samplerShardSize, + // @ts-expect-error script is not compatible with StoredScript from @elastic/elasticsearch: string is not supported runtimeMappings ); diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts index d54a62b27f262..f1900079ec9c4 100644 --- a/x-pack/plugins/transform/server/routes/api/privileges.ts +++ b/x-pack/plugins/transform/server/routes/api/privileges.ts @@ -32,7 +32,6 @@ export function registerPrivilegesRoute({ router, license }: RouteDependencies) const { body: { has_all_requested: hasAllPrivileges, cluster }, } = await ctx.core.elasticsearch.client.asCurrentUser.security.hasPrivileges({ - method: 'POST', body: { cluster: APP_CLUSTER_PRIVILEGES, }, @@ -45,9 +44,7 @@ export function registerPrivilegesRoute({ router, license }: RouteDependencies) // Get all index privileges the user has const { body: { indices }, - } = await ctx.core.elasticsearch.client.asCurrentUser.security.getUserPrivileges({ - method: 'GET', - }); + } = await ctx.core.elasticsearch.client.asCurrentUser.security.getUserPrivileges(); // Check if they have all the required index privileges for at least one index const oneIndexWithAllPrivileges = indices.find(({ privileges }: { privileges: string[] }) => { diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 93f5caf7cf5b0..1dd136cb484fa 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -206,6 +206,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { await ctx.core.elasticsearch.client.asCurrentUser.transform .putTransform({ + // @ts-expect-error @elastic/elasticsearch max_page_search_size is required in TransformPivot body: req.body, transform_id: transformId, }) @@ -250,6 +251,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { const { body, } = await ctx.core.elasticsearch.client.asCurrentUser.transform.updateTransform({ + // @ts-expect-error query doesn't satisfy QueryContainer from @elastic/elasticsearch body: req.body, transform_id: transformId, }); @@ -452,9 +454,12 @@ async function deleteTransforms( transform_id: transformId, }); const transformConfig = body.transforms[0]; + // @ts-expect-error @elastic/elasticsearch doesn't provide typings for Transform destinationIndex = Array.isArray(transformConfig.dest.index) - ? transformConfig.dest.index[0] - : transformConfig.dest.index; + ? // @ts-expect-error @elastic/elasticsearch doesn't provide typings for Transform + transformConfig.dest.index[0] + : // @ts-expect-error @elastic/elasticsearch doesn't provide typings for Transform + transformConfig.dest.index; } catch (getTransformConfigError) { transformDeleted.error = getTransformConfigError.meta.body.error; results[transformId] = { @@ -539,6 +544,7 @@ const previewTransformHandler: RequestHandler< try { const reqBody = req.body; const { body } = await ctx.core.elasticsearch.client.asCurrentUser.transform.previewTransform({ + // @ts-expect-error max_page_search_size is required in TransformPivot body: reqBody, }); if (isLatestTransform(reqBody)) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index 4a6b2c674e429..b762f20b55b48 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -6,7 +6,7 @@ */ import { transformIdParamSchema, TransformIdParamSchema } from '../../../common/api_schemas/common'; -import { AuditMessage, TransformMessage } from '../../../common/types/messages'; +import { AuditMessage } from '../../../common/types/messages'; import { RouteDependencies } from '../../types'; @@ -78,19 +78,28 @@ export function registerTransformsAuditMessagesRoutes({ router, license }: Route } try { - const { body: resp } = await ctx.core.elasticsearch.client.asCurrentUser.search({ + const { + body: resp, + } = await ctx.core.elasticsearch.client.asCurrentUser.search({ index: ML_DF_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, size: SIZE, body: { - sort: [{ timestamp: { order: 'desc' } }, { transform_id: { order: 'asc' } }], + sort: [ + { timestamp: { order: 'desc' as const } }, + { transform_id: { order: 'asc' as const } }, + ], query, }, }); - let messages: TransformMessage[] = []; - if (resp.hits.total.value > 0) { - messages = resp.hits.hits.map((hit: AuditMessage) => hit._source); + let messages: AuditMessage[] = []; + // TODO: remove typeof checks when appropriate overloading is added for the `search` API + if ( + (typeof resp.hits.total === 'number' && resp.hits.total > 0) || + (typeof resp.hits.total === 'object' && resp.hits.total.value > 0) + ) { + messages = resp.hits.hits.map((hit) => hit._source!); messages.reverse(); } return res.ok({ body: messages }); diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index 78462d9969929..98212f1dc6aaf 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { Logger, ElasticsearchClient } from 'kibana/server'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; @@ -127,14 +127,14 @@ export async function timeSeriesQuery( const logPrefix = 'indexThreshold timeSeriesQuery: callCluster'; logger.debug(`${logPrefix} call: ${JSON.stringify(esQuery)}`); - let esResult: SearchResponse; + let esResult: estypes.SearchResponse; // note there are some commented out console.log()'s below, which are left // in, as they are VERY useful when debugging these queries; debug logging // isn't as nice since it's a single long JSON line. // console.log('time_series_query.ts request\n', JSON.stringify(esQuery, null, 4)); try { - esResult = (await esClient.search>(esQuery, { ignore: [404] })).body; + esResult = (await esClient.search(esQuery, { ignore: [404] })).body; } catch (err) { // console.log('time_series_query.ts error\n', JSON.stringify(err, null, 4)); logger.warn(`${logPrefix} error: ${err.message}`); @@ -149,7 +149,7 @@ export async function timeSeriesQuery( function getResultFromEs( isCountAgg: boolean, isGroupAgg: boolean, - esResult: SearchResponse + esResult: estypes.SearchResponse ): TimeSeriesResult { const aggregations = esResult?.aggregations || {}; @@ -164,6 +164,7 @@ function getResultFromEs( delete aggregations.dateAgg; } + // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets const groupBuckets = aggregations.groupAgg?.buckets || []; const result: TimeSeriesResult = { results: [], diff --git a/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts index c029f5b8bdaed..3245a9b8a983f 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts @@ -105,7 +105,9 @@ async function getIndicesFromPattern( return []; } - return (response.aggregations as IndiciesAggregation).indices.buckets.map((bucket) => bucket.key); + return ((response.aggregations as unknown) as IndiciesAggregation).indices.buckets.map( + (bucket) => bucket.key + ); } async function getAliasesFromPattern( @@ -118,14 +120,15 @@ async function getAliasesFromPattern( }; const result: string[] = []; - const { body: response } = await esClient.indices.getAlias(params); + const response = await esClient.indices.getAlias(params); + const responseBody = response.body; - if (response.status === 404) { + if (response.statusCode === 404) { return result; } - for (const index of Object.keys(response)) { - const aliasRecord = response[index]; + for (const index of Object.keys(responseBody)) { + const aliasRecord = responseBody[index]; if (aliasRecord.aliases) { const aliases = Object.keys(aliasRecord.aliases); result.push(...aliases); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts index d352ab77b56f3..d2595916496a5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts @@ -15,7 +15,7 @@ export const esIndicesStateCheck = async ( asCurrentUser: ElasticsearchClient, indices: string[] ): Promise => { - const { body: response } = await asCurrentUser.indices.resolveIndex({ + const { body: response } = await asCurrentUser.indices.resolveIndex({ name: '*', expand_wildcards: 'all', }); @@ -23,7 +23,7 @@ export const esIndicesStateCheck = async ( const result: StatusCheckResult = {}; indices.forEach((index) => { - result[index] = getIndexState(index, response); + result[index] = getIndexState(index, response as ResolveIndexResponseFromES); }); return result; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 9ab8d0aa7cffb..2620fe31d6277 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -31,6 +31,7 @@ describe('getUpgradeAssistantStatus', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient(); esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + // @ts-expect-error not full interface asApiResponse(deprecationsResponse) ); @@ -48,6 +49,7 @@ describe('getUpgradeAssistantStatus', () => { it('returns readyForUpgrade === false when critical issues found', async () => { esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + // @ts-expect-error not full interface asApiResponse({ cluster_settings: [{ level: 'critical', message: 'Do count me', url: 'https://...' }], node_settings: [], @@ -64,6 +66,7 @@ describe('getUpgradeAssistantStatus', () => { it('returns readyForUpgrade === true when no critical issues found', async () => { esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + // @ts-expect-error not full interface asApiResponse({ cluster_settings: [{ level: 'warning', message: 'Do not count me', url: 'https://...' }], node_settings: [], @@ -80,6 +83,7 @@ describe('getUpgradeAssistantStatus', () => { it('filters out security realm deprecation on Cloud', async () => { esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + // @ts-expect-error not full interface asApiResponse({ cluster_settings: [ { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index 3486603341674..e775190d426df 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -19,9 +19,7 @@ export async function getUpgradeAssistantStatus( dataClient: IScopedClusterClient, isCloudEnabled: boolean ): Promise { - const { - body: deprecations, - } = await dataClient.asCurrentUser.migration.deprecations(); + const { body: deprecations } = await dataClient.asCurrentUser.migration.deprecations(); const cluster = getClusterDeprecations(deprecations, isCloudEnabled); const indices = getCombinedIndexInfos(deprecations); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 592c2d15b9c0c..cffd49e5bd38a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -283,6 +283,7 @@ describe('ReindexActions', () => { it('returns flat settings', async () => { clusterClient.asCurrentUser.indices.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ myIndex: { settings: { 'index.mySetting': '1' }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index a91cf8ddeada9..ae0a6f97e29d5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -105,6 +105,7 @@ describe('reindexService', () => { it('calls security API with basic requirements', async () => { clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ has_all_requested: true }) ); @@ -130,6 +131,7 @@ describe('reindexService', () => { it('includes manage_ml for ML indices', async () => { clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ has_all_requested: true }) ); @@ -154,6 +156,7 @@ describe('reindexService', () => { it('includes checking for permissions on the baseName which could be an alias', async () => { clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ has_all_requested: true }) ); @@ -183,6 +186,7 @@ describe('reindexService', () => { it('includes manage_watcher for watcher indices', async () => { clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ has_all_requested: true, }) @@ -440,6 +444,7 @@ describe('reindexService', () => { }, } as any); + // @ts-expect-error not full interface clusterClient.asCurrentUser.tasks.cancel.mockResolvedValueOnce(asApiResponse(true)); await service.cancelReindexing('myIndex'); @@ -564,6 +569,7 @@ describe('reindexService', () => { f() ); clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) ); clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( @@ -596,6 +602,7 @@ describe('reindexService', () => { ); clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) ); clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( @@ -646,6 +653,7 @@ describe('reindexService', () => { f() ); clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ nodes: { nodeX: { version: '6.7.0' } } }) ); clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( @@ -670,6 +678,7 @@ describe('reindexService', () => { f() ); clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ nodes: { nodeX: { version: '6.6.0' } } }) ); @@ -784,7 +793,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(clusterClient.asCurrentUser.indices.putSettings).toHaveBeenCalledWith({ index: 'myIndex', - body: { 'index.blocks.write': true }, + body: { index: { blocks: { write: true } } }, }); }); @@ -823,6 +832,7 @@ describe('reindexService', () => { it('creates new index with settings and mappings and updates lastCompletedStep', async () => { actions.getFlatSettings.mockResolvedValueOnce(settingsMappings); clusterClient.asCurrentUser.indices.create.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ acknowledged: true }) ); const updatedOp = await service.processNextStep(reindexOp); @@ -839,10 +849,12 @@ describe('reindexService', () => { it('fails if create index is not acknowledged', async () => { clusterClient.asCurrentUser.indices.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ myIndex: settingsMappings }) ); clusterClient.asCurrentUser.indices.create.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ acknowledged: false }) ); const updatedOp = await service.processNextStep(reindexOp); @@ -854,6 +866,7 @@ describe('reindexService', () => { it('fails if create index fails', async () => { clusterClient.asCurrentUser.indices.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ myIndex: settingsMappings }) ); @@ -872,7 +885,7 @@ describe('reindexService', () => { // Original index should have been set back to allow reads. expect(clusterClient.asCurrentUser.indices.putSettings).toHaveBeenCalledWith({ index: 'myIndex', - body: { 'index.blocks.write': false }, + body: { index: { blocks: { write: false } } }, }); }); }); @@ -932,6 +945,7 @@ describe('reindexService', () => { describe('reindex task is not complete', () => { it('updates reindexTaskPercComplete', async () => { clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ completed: false, task: { status: { created: 10, total: 100 } }, @@ -947,6 +961,7 @@ describe('reindexService', () => { describe('reindex task is complete', () => { it('deletes task, updates reindexTaskPercComplete, updates lastCompletedStep', async () => { clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ completed: true, task: { status: { created: 100, total: 100 } }, @@ -954,12 +969,14 @@ describe('reindexService', () => { ); clusterClient.asCurrentUser.count.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ count: 100, }) ); clusterClient.asCurrentUser.delete.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ result: 'deleted', }) @@ -976,6 +993,7 @@ describe('reindexService', () => { it('fails if docs created is less than count in source index', async () => { clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ completed: true, task: { status: { created: 95, total: 95 } }, @@ -983,6 +1001,7 @@ describe('reindexService', () => { ); clusterClient.asCurrentUser.count.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ count: 100, }) @@ -999,6 +1018,7 @@ describe('reindexService', () => { describe('reindex task is cancelled', () => { it('deletes task, updates status to cancelled', async () => { clusterClient.asCurrentUser.tasks.get.mockResolvedValueOnce( + // @ts-expect-error not full interface asApiResponse({ completed: true, task: { status: { created: 100, total: 100, canceled: 'by user request' } }, @@ -1006,6 +1026,7 @@ describe('reindexService', () => { ); clusterClient.asCurrentUser.delete.mockResolvedValue( + // @ts-expect-error not full interface asApiResponse({ result: 'deleted' }) ); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 1b5f91e0c53b8..b9d5b42a65250 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -226,7 +226,7 @@ export const reindexServiceFactory = ( if (reindexOp.attributes.lastCompletedStep >= ReindexStep.readonly) { await esClient.indices.putSettings({ index: reindexOp.attributes.indexName, - body: { 'index.blocks.write': false }, + body: { index: { blocks: { write: false } } }, }); } @@ -290,7 +290,7 @@ export const reindexServiceFactory = ( const { indexName } = reindexOp.attributes; const { body: putReadonly } = await esClient.indices.putSettings({ index: indexName, - body: { 'index.blocks.write': true }, + body: { index: { blocks: { write: true } } }, }); if (!putReadonly.acknowledged) { @@ -363,7 +363,10 @@ export const reindexServiceFactory = ( return actions.updateReindexOp(reindexOp, { lastCompletedStep: ReindexStep.reindexStarted, - reindexTaskId: startReindexResponse.task, + reindexTaskId: + startReindexResponse.task === undefined + ? startReindexResponse.task + : String(startReindexResponse.task), reindexTaskPercComplete: 0, reindexOptions: { ...(reindexOptions ?? {}), @@ -389,11 +392,11 @@ export const reindexServiceFactory = ( if (!taskResponse.completed) { // Updated the percent complete - const perc = taskResponse.task.status.created / taskResponse.task.status.total; + const perc = taskResponse.task.status!.created / taskResponse.task.status!.total; return actions.updateReindexOp(reindexOp, { reindexTaskPercComplete: perc, }); - } else if (taskResponse.task.status.canceled === 'by user request') { + } else if (taskResponse.task.status?.canceled === 'by user request') { // Set the status to cancelled reindexOp = await actions.updateReindexOp(reindexOp, { status: ReindexStatus.cancelled, @@ -403,9 +406,11 @@ export const reindexServiceFactory = ( reindexOp = await cleanupChanges(reindexOp); } else { // Check that it reindexed all documents - const { body: count } = await esClient.count({ index: reindexOp.attributes.indexName }); + const { + body: { count }, + } = await esClient.count({ index: reindexOp.attributes.indexName }); - if (taskResponse.task.status.created < count) { + if (taskResponse.task.status!.created < count) { // Include the entire task result in the error message. This should be guaranteed // to be JSON-serializable since it just came back from Elasticsearch. throw error.reindexTaskFailed(`Reindexing failed: ${JSON.stringify(taskResponse)}`); diff --git a/x-pack/plugins/uptime/common/utils/as_mutable_array.ts b/x-pack/plugins/uptime/common/utils/as_mutable_array.ts new file mode 100644 index 0000000000000..ce1d7e607ec4c --- /dev/null +++ b/x-pack/plugins/uptime/common/utils/as_mutable_array.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Sometimes we use `as const` to have a more specific type, +// because TypeScript by default will widen the value type of an +// array literal. Consider the following example: +// +// const filter = [ +// { term: { 'agent.name': 'nodejs' } }, +// { range: { '@timestamp': { gte: 'now-15m ' }} +// ]; + +// The result value type will be: + +// const filter: ({ +// term: { +// 'agent.name'?: string +// }; +// range?: undefined +// } | { +// term?: undefined; +// range: { +// '@timestamp': { +// gte: string +// } +// } +// })[]; + +// This can sometimes leads to issues. In those cases, we can +// use `as const`. However, the Readonly type is not compatible +// with Array. This function returns a mutable version of a type. + +export function asMutableArray>( + arr: T +): T extends Readonly<[...infer U]> ? U : unknown[] { + return arr as any; +} diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 1a7cef504b019..12e58b94cfa50 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -16,7 +16,7 @@ import { UMBackendFrameworkAdapter } from './adapters'; import { UMLicenseCheck } from './domains'; import { UptimeRequests } from './requests'; import { savedObjectsAdapter } from './saved_objects'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchRequest, ESSearchResponse } from '../../../../../typings/elasticsearch'; export interface UMDomainLibs { requests: UptimeRequests; @@ -54,7 +54,9 @@ export function createUptimeESClient({ return { baseESClient: esClient, - async search(params: TParams): Promise<{ body: ESSearchResponse }> { + async search( + params: TParams + ): Promise<{ body: ESSearchResponse }> { let res: any; let esError: any; const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index 2824fa9af35ad..1b20ed9085fef 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -48,7 +48,7 @@ export const getCerts: UMElasticsearchQueryFn = asyn { multi_match: { query: escape(search), - type: 'phrase_prefix', + type: 'phrase_prefix' as const, fields: [ 'monitor.id.text', 'monitor.name.text', @@ -98,7 +98,7 @@ export const getCerts: UMElasticsearchQueryFn = asyn field: 'monitor.id', }, name: 'monitors', - sort: [{ 'monitor.id': 'asc' }], + sort: [{ 'monitor.id': 'asc' as const }], }, }, aggs: { @@ -154,7 +154,7 @@ export const getCerts: UMElasticsearchQueryFn = asyn const sha1 = server?.hash?.sha1; const sha256 = server?.hash?.sha256; - const monitors = hit.inner_hits.monitors.hits.hits.map((monitor: any) => ({ + const monitors = hit.inner_hits!.monitors.hits.hits.map((monitor: any) => ({ name: monitor._source?.monitor.name, id: monitor._source?.monitor.id, url: monitor._source?.url?.full, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts index de37688b155f5..560653950c6f8 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; @@ -30,7 +31,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< 'synthetics.type': 'journey/start', }, }, - ], + ] as QueryContainer[], }, }, size: 1, @@ -55,7 +56,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< 'synthetics.type': 'journey/start', }, }, - ], + ] as QueryContainer[], }, }, _source: ['@timestamp', 'monitor.check_group'], @@ -78,7 +79,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< ], }, }, - sort: [{ '@timestamp': { order: 'desc' } }], + sort: [{ '@timestamp': { order: 'desc' as const } }], }; const nextParams = { @@ -97,7 +98,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< ], }, }, - sort: [{ '@timestamp': { order: 'asc' } }], + sort: [{ '@timestamp': { order: 'asc' as const } }], }; const { body: previousJourneyResult } = await uptimeEsClient.search({ body: previousParams }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts index 9865bd95fe961..5c4e263468947 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts @@ -5,6 +5,9 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { SearchHit } from '../../../../../../typings/elasticsearch'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; @@ -35,10 +38,13 @@ export const getJourneyFailedSteps: UMElasticsearchQueryFn { + return ((result.hits.hits as Array>).map((h) => { const source = h._source as Ping & { '@timestamp': string }; return { ...source, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index faa260eb9abd4..6a533d558c721 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types/ping'; @@ -33,7 +34,7 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn< 'synthetics.type': 'step/screenshot', }, }, - ], + ] as QueryContainer[], }, }, aggs: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index 43d17cb938159..bfb8f4d881c3d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -5,6 +5,9 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { SearchHit } from 'typings/elasticsearch/search'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; @@ -42,10 +45,13 @@ export const getJourneySteps: UMElasticsearchQueryFn (h?._source as Ping).synthetics?.type === 'step/screenshot') - .map((h) => (h?._source as Ping).synthetics?.step?.index as number); + const screenshotIndexes: number[] = (result.hits.hits as Array>) + .filter((h) => h._source?.synthetics?.type === 'step/screenshot') + .map((h) => h._source?.synthetics?.step?.index as number); - return (result.hits.hits - .filter((h) => (h?._source as Ping).synthetics?.type !== 'step/screenshot') + return ((result.hits.hits as Array>) + .filter((h) => h._source?.synthetics?.type !== 'step/screenshot') .map((h) => { const source = h._source as Ping & { '@timestamp': string }; return { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts b/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts index 82958167341c0..6f88e7e37e55e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types/ping'; @@ -18,7 +19,7 @@ export const getStepLastSuccessfulStep: UMElasticsearchQueryFn< GetStepScreenshotParams, any > = async ({ uptimeEsClient, monitorId, stepIndex, timestamp }) => { - const lastSuccessCheckParams = { + const lastSuccessCheckParams: estypes.SearchRequest['body'] = { size: 1, sort: [ { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index 14190413ac7e7..ad8b345d7b276 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { UMElasticsearchQueryFn } from '../adapters'; import { Ping } from '../../../common/runtime_types'; @@ -44,20 +45,20 @@ export const getLatestMonitor: UMElasticsearchQueryFn = async ({ uptimeEsClient, monitorId, dateStart, dateEnd }) => { - const sortOptions: SortOptions = [ + const sortOptions = [ { '@timestamp': { - order: 'desc', + order: 'desc' as const, }, }, ]; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index ef90794e634b7..16aca6f8238fb 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -152,7 +152,7 @@ export const getHistogramForMonitors = async ( }; const { body: result } = await queryContext.search({ body: params }); - const histoBuckets: any[] = result.aggregations?.histogram.buckets ?? []; + const histoBuckets: any[] = (result.aggregations as any)?.histogram.buckets ?? []; const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => { const byId: { [key: string]: number } = {}; histoBucket.by_id.buckets.forEach((idBucket: any) => { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index b588127b06483..8b46070f701f2 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -6,6 +6,8 @@ */ import { JsonObject } from 'src/plugins/kibana_utils/public'; +import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters'; import { Ping } from '../../../common/runtime_types/ping'; @@ -68,7 +70,7 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, // append user filters, if defined ...(filters?.bool ? [filters] : []), - ], + ] as QueryContainer[], }, }, size: 0, @@ -81,7 +83,7 @@ export const getMonitorStatus: UMElasticsearchQueryFn< * to tell Elasticsearch where it should start on subsequent queries. */ ...(afterKey ? { after: afterKey } : {}), - sources: [ + sources: asMutableArray([ { monitorId: { terms: { @@ -104,7 +106,7 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, }, }, - ], + ] as const), }, aggs: { fields: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index 970af80576cad..246b2001a9381 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { NetworkEvent } from '../../../common/runtime_types'; @@ -29,7 +30,7 @@ export const getNetworkEvents: UMElasticsearchQueryFn< { term: { 'synthetics.type': 'journey/network_info' } }, { term: { 'monitor.check_group': checkGroup } }, { term: { 'synthetics.step.index': Number(stepIndex) } }, - ], + ] as QueryContainer[], }, }, // NOTE: This limit may need tweaking in the future. Users can technically perform multiple diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index d59da8029f1b9..0362ac30ac713 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -53,7 +53,7 @@ export const getPingHistogram: UMElasticsearchQueryFn< { multi_match: { query: escape(query), - type: 'phrase_prefix', + type: 'phrase_prefix' as const, fields: ['monitor.id.text', 'monitor.name.text', 'url.full.text'], }, }, @@ -68,7 +68,7 @@ export const getPingHistogram: UMElasticsearchQueryFn< date_histogram: { field: '@timestamp', fixed_interval: bucketSize || minInterval + 'ms', - missing: 0, + missing: '0', }, aggs: { down: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index 453663ba737fb..cf7a85e60f753 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { GetPingsParams, @@ -73,13 +74,13 @@ export const getPings: UMElasticsearchQueryFn = a { range: { '@timestamp': { gte: from, lte: to } } }, ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), ...(status ? [{ term: { 'monitor.status': status } }] : []), - ], + ] as QueryContainer[], ...REMOVE_NON_SUMMARY_BROWSER_CHECKS, }, }, sort: [{ '@timestamp': { order: (sort ?? 'desc') as 'asc' | 'desc' } }], ...((locations ?? []).length > 0 - ? { post_filter: { terms: { 'observer.geo.name': locations } } } + ? { post_filter: { terms: { 'observer.geo.name': (locations as unknown) as string[] } } } : {}), }; diff --git a/x-pack/plugins/uptime/server/lib/requests/helper.ts b/x-pack/plugins/uptime/server/lib/requests/helper.ts index e3969f84c8485..c637c05094667 100644 --- a/x-pack/plugins/uptime/server/lib/requests/helper.ts +++ b/x-pack/plugins/uptime/server/lib/requests/helper.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SearchResponse } from '@elastic/elasticsearch/api/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ElasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { @@ -49,7 +50,7 @@ export const setupMockEsCompositeQuery = ( }, }; esMock.search.mockResolvedValueOnce({ - body: mockResponse, + body: (mockResponse as unknown) as SearchResponse, statusCode: 200, headers: {}, warnings: [], diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 639a24a2bdffa..179e9e809e59b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -22,7 +22,7 @@ export const findPotentialMatches = async ( const { body: queryResult } = await query(queryContext, searchAfter, size); const monitorIds: string[] = []; - (queryResult.aggregations?.monitors.buckets ?? []).forEach((b) => { + (queryResult.aggregations?.monitors.buckets ?? []).forEach((b: any) => { const monitorId = b.key.monitor_id; monitorIds.push(monitorId as string); }); @@ -40,7 +40,8 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number) body, }; - return await queryContext.search(params); + const response = await queryContext.search(params); + return response; }; const queryBody = async (queryContext: QueryContext, searchAfter: any, size: number) => { @@ -77,7 +78,9 @@ const queryBody = async (queryContext: QueryContext, searchAfter: any, size: num size, sources: [ { - monitor_id: { terms: { field: 'monitor.id', order: queryContext.cursorOrder() } }, + monitor_id: { + terms: { field: 'monitor.id' as const, order: queryContext.cursorOrder() }, + }, }, ], }, diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index d6ba222e50eb4..0305de740dc99 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -55,6 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // @ts-expect-error @elastic/elasticsearch DeleteSnapshotLifecycleRequest.policy_id is required await esClient.ilm.deleteLifecycle({ policy: TEST_POLICY_NAME }); }); diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 164c7032d9dd3..41f8d5e56f8e6 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -114,10 +114,11 @@ export default function ({ getService }: FtrProviderContext) { description: 'Test calendar', }); await ml.api.createCalendarEvents(calendarId, [ + // @ts-expect-error not full interface { description: eventDescription, - start_time: 1513641600000, - end_time: 1513728000000, + start_time: '1513641600000', + end_time: '1513728000000', }, ]); diff --git a/x-pack/test/api_integration/apis/es/has_privileges.ts b/x-pack/test/api_integration/apis/es/has_privileges.ts index 7403d27c4dd4b..4cd1f70d4fffc 100644 --- a/x-pack/test/api_integration/apis/es/has_privileges.ts +++ b/x-pack/test/api_integration/apis/es/has_privileges.ts @@ -32,6 +32,7 @@ export default function ({ getService }: FtrProviderContext) { name: 'hp_read_user', body: { cluster: [], + // @ts-expect-error unknown property index: [], applications: [ { diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 56b44234f065c..32b6a446a57b2 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -7,7 +7,6 @@ import moment from 'moment'; import expect from '@kbn/expect'; -import { Client } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -20,7 +19,7 @@ const COMMON_HEADERS = { export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); - const es: Client = getService('es'); + const es = getService('es'); async function assertExpectedSavedObjects(num: number) { // Make sure that new/deleted docs are available to search @@ -42,6 +41,7 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:lens-ui-telemetry', wait_for_completion: true, refresh: true, @@ -52,6 +52,7 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:lens-ui-telemetry', wait_for_completion: true, refresh: true, diff --git a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts index d1527db6b66ea..21c1c1efbf9e8 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/create_annotations.ts @@ -25,6 +25,7 @@ export default ({ getService }: FtrProviderContext) => { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.setKibanaTimeZoneToUTC(); + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(testJobConfig); }); diff --git a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts index 422d00a21ce15..f6c4c98e1f7ea 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/delete_annotations.ts @@ -25,6 +25,7 @@ export default ({ getService }: FtrProviderContext) => { for (let i = 0; i < testSetupJobConfigs.length; i++) { const job = testSetupJobConfigs[i]; const annotationToIndex = testSetupAnnotations[i]; + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(job); await ml.api.indexAnnotation(annotationToIndex); } diff --git a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts index ac8438170c882..c0e81955ad595 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/get_annotations.ts @@ -26,6 +26,7 @@ export default ({ getService }: FtrProviderContext) => { for (let i = 0; i < testSetupJobConfigs.length; i++) { const job = testSetupJobConfigs[i]; const annotationToIndex = testSetupAnnotations[i]; + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(job); await ml.api.indexAnnotation(annotationToIndex); } diff --git a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts index 41406cb344388..346f4562b77bb 100644 --- a/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts +++ b/x-pack/test/api_integration/apis/ml/annotations/update_annotations.ts @@ -38,6 +38,7 @@ export default ({ getService }: FtrProviderContext) => { for (let i = 0; i < testSetupJobConfigs.length; i++) { const job = testSetupJobConfigs[i]; const annotationToIndex = testSetupAnnotations[i]; + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(job); await ml.api.indexAnnotation(annotationToIndex); } @@ -54,7 +55,7 @@ export default ({ getService }: FtrProviderContext) => { const originalAnnotation = annotationsForJob[0]; const annotationUpdateRequestBody = { ...commonAnnotationUpdateRequestBody, - job_id: originalAnnotation._source.job_id, + job_id: originalAnnotation._source?.job_id, _id: originalAnnotation._id, }; @@ -85,7 +86,7 @@ export default ({ getService }: FtrProviderContext) => { const originalAnnotation = annotationsForJob[0]; const annotationUpdateRequestBody = { ...commonAnnotationUpdateRequestBody, - job_id: originalAnnotation._source.job_id, + job_id: originalAnnotation._source?.job_id, _id: originalAnnotation._id, }; @@ -116,7 +117,7 @@ export default ({ getService }: FtrProviderContext) => { const annotationUpdateRequestBody = { ...commonAnnotationUpdateRequestBody, - job_id: originalAnnotation._source.job_id, + job_id: originalAnnotation._source?.job_id, _id: originalAnnotation._id, }; @@ -143,7 +144,7 @@ export default ({ getService }: FtrProviderContext) => { timestamp: Date.now(), end_timestamp: Date.now(), annotation: 'Updated annotation', - job_id: originalAnnotation._source.job_id, + job_id: originalAnnotation._source?.job_id, type: ANNOTATION_TYPE.ANNOTATION, event: 'model_change', detector_index: 2, diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts index acbfcbe7acd47..03ee82eed27d0 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts @@ -52,6 +52,7 @@ export default ({ getService }: FtrProviderContext) => { ]; for (const jobConfig of mockJobConfigs) { + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(jobConfig); } } diff --git a/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts index fac62237aa74e..01b87e6059a29 100644 --- a/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts +++ b/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts @@ -52,7 +52,11 @@ export default ({ getService }: FtrProviderContext) => { expect(createdCalendar.description).to.eql(requestBody.description); expect(createdCalendar.job_ids).to.eql(requestBody.job_ids); - await ml.api.waitForEventsToExistInCalendar(calendarId, requestBody.events); + await ml.api.waitForEventsToExistInCalendar( + calendarId, + // @ts-expect-error not full interface + requestBody.events + ); }); it('should not create new calendar for user without required permission', async () => { diff --git a/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts index a2e1709731aa7..dfbffad9dafdd 100644 --- a/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts +++ b/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts @@ -34,6 +34,7 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { await ml.api.createCalendar(calendarId, testCalendar); + // @ts-expect-error not full interface await ml.api.createCalendarEvents(calendarId, testEvents); }); diff --git a/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts index 243a40abe97a4..175c678317e6c 100644 --- a/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts +++ b/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts @@ -35,6 +35,7 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { for (const testCalendar of testCalendars) { await ml.api.createCalendar(testCalendar.calendar_id, testCalendar); + // @ts-expect-error not full interface await ml.api.createCalendarEvents(testCalendar.calendar_id, testEvents); } }); @@ -54,6 +55,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.have.length(testCalendars.length); expect(body[0].events).to.have.length(testEvents.length); + // @ts-expect-error not full interface ml.api.assertAllEventsExistInCalendar(testEvents, body[0]); }); @@ -66,6 +68,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.have.length(testCalendars.length); expect(body[0].events).to.have.length(testEvents.length); + // @ts-expect-error not full interface ml.api.assertAllEventsExistInCalendar(testEvents, body[0]); }); @@ -89,6 +92,7 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { await ml.api.createCalendar(calendarId, testCalendar); + // @ts-expect-error not full interface await ml.api.createCalendarEvents(calendarId, testEvents); }); @@ -106,6 +110,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.job_ids).to.eql(testCalendar.job_ids); expect(body.description).to.eql(testCalendar.description); expect(body.events).to.have.length(testEvents.length); + // @ts-expect-error not full interface ml.api.assertAllEventsExistInCalendar(testEvents, body); }); @@ -119,6 +124,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.job_ids).to.eql(testCalendar.job_ids); expect(body.description).to.eql(testCalendar.description); expect(body.events).to.have.length(testEvents.length); + // @ts-expect-error not full interface ml.api.assertAllEventsExistInCalendar(testEvents, body); }); diff --git a/x-pack/test/api_integration/apis/ml/calendars/helpers.ts b/x-pack/test/api_integration/apis/ml/calendars/helpers.ts index 7ddfc4b41679d..f80c985c2676e 100644 --- a/x-pack/test/api_integration/apis/ml/calendars/helpers.ts +++ b/x-pack/test/api_integration/apis/ml/calendars/helpers.ts @@ -5,13 +5,16 @@ * 2.0. */ -import { Calendar, CalendarEvent } from '../../../../../plugins/ml/server/models/calendar'; +import { estypes } from '@elastic/elasticsearch'; +import { Calendar } from '../../../../../plugins/ml/server/models/calendar'; + +type ScheduledEvent = estypes.ScheduledEvent; export const assertAllEventsExistInCalendar = ( - eventsToCheck: CalendarEvent[], + eventsToCheck: ScheduledEvent[], calendar: Calendar ): boolean => { - const updatedCalendarEvents = calendar.events as CalendarEvent[]; + const updatedCalendarEvents = calendar.events; let allEventsAreUpdated = true; for (const eventToCheck of eventsToCheck) { // if at least one of the events that we need to check is not in the updated events diff --git a/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts index 1ca9a66319943..4798a3407be11 100644 --- a/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts +++ b/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts @@ -42,6 +42,7 @@ export default ({ getService }: FtrProviderContext) => { beforeEach(async () => { await ml.api.createCalendar(calendarId, originalCalendar); + // @ts-expect-error not full interface await ml.api.createCalendarEvents(calendarId, originalEvents); }); @@ -70,6 +71,7 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedEvents).to.have.length(updateCalendarRequestBody.events.length); await ml.api.waitForEventsToExistInCalendar( updatedCalendar.calendar_id, + // @ts-expect-error not full interface updateCalendarRequestBody.events ); }); diff --git a/x-pack/test/api_integration/apis/ml/jobs/common_jobs.ts b/x-pack/test/api_integration/apis/ml/jobs/common_jobs.ts index 5039ea550d47e..2136e16e3c153 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/common_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/common_jobs.ts @@ -7,6 +7,7 @@ import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +// @ts-expect-error not full interface export const SINGLE_METRIC_JOB_CONFIG: Job = { job_id: `jobs_summary_fq_single_${Date.now()}`, description: 'mean(responsetime) on farequote dataset with 15m bucket span', @@ -26,6 +27,7 @@ export const SINGLE_METRIC_JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error not full interface export const MULTI_METRIC_JOB_CONFIG: Job = { job_id: `jobs_summary_fq_multi_${Date.now()}`, description: 'mean(responsetime) partition=airline on farequote dataset with 1h bucket span', @@ -40,6 +42,7 @@ export const MULTI_METRIC_JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error not full interface export const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'REPLACE', indices: ['ft_farequote'], diff --git a/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts b/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts index fbca2f2abcce3..1505e6fbe09be 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts @@ -16,6 +16,7 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); + // @ts-expect-error not full interface const JOB_CONFIG: Job = { job_id: `fq_multi_1_ae`, description: @@ -35,6 +36,7 @@ export default ({ getService }: FtrProviderContext) => { model_plot_config: { enabled: true }, }; + // @ts-expect-error not full interface const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_multi_1_se', indices: ['ft_farequote'], diff --git a/x-pack/test/api_integration/apis/ml/results/get_categorizer_stats.ts b/x-pack/test/api_integration/apis/ml/results/get_categorizer_stats.ts index 9952dca651ba0..ef677969d006f 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_categorizer_stats.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_categorizer_stats.ts @@ -43,6 +43,7 @@ export default ({ getService }: FtrProviderContext) => { daily_model_snapshot_retention_after_days: 1, allow_lazy_open: false, }; + // @ts-expect-error not full interface const testDatafeedConfig: Datafeed = { datafeed_id: `datafeed-${jobId}`, indices: ['ft_module_sample_logs'], @@ -54,6 +55,7 @@ export default ({ getService }: FtrProviderContext) => { before(async () => { await esArchiver.loadIfNeeded('ml/module_sample_logs'); await ml.testResources.setKibanaTimeZoneToUTC(); + // @ts-expect-error not full interface await ml.api.createAndRunAnomalyDetectionLookbackJob(testJobConfig, testDatafeedConfig); }); diff --git a/x-pack/test/api_integration/apis/ml/results/get_stopped_partitions.ts b/x-pack/test/api_integration/apis/ml/results/get_stopped_partitions.ts index 7b03e0e3091de..d00999b06b588 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_stopped_partitions.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_stopped_partitions.ts @@ -40,6 +40,7 @@ export default ({ getService }: FtrProviderContext) => { daily_model_snapshot_retention_after_days: 1, allow_lazy_open: false, }; + // @ts-expect-error not full interface const datafeedConfig: Datafeed = { datafeed_id: `datafeed-${jobId}`, indices: ['ft_module_sample_logs'], @@ -50,6 +51,7 @@ export default ({ getService }: FtrProviderContext) => { return { testDescription: `stop_on_warn is ${stopOnWarn}`, jobId, + // @ts-expect-error not full interface jobConfig: { job_id: jobId, ...commonJobConfig, diff --git a/x-pack/test/api_integration/apis/security/roles.ts b/x-pack/test/api_integration/apis/security/roles.ts index 440bb4ca32f18..09b2d2eef9fbe 100644 --- a/x-pack/test/api_integration/apis/security/roles.ts +++ b/x-pack/test/api_integration/apis/security/roles.ts @@ -312,6 +312,7 @@ export default function ({ getService }: FtrProviderContext) { metadata: { foo: 'test-metadata', }, + // @ts-expect-error @elastic/elasticsearch PutRoleRequest.body doesn't declare `transient_metadata` property transient_metadata: { enabled: true, }, diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts b/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts index 7055c60b6cd34..a85e8ef82fc8c 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts @@ -37,7 +37,7 @@ export default function ({ getService }: FtrProviderContext) { let stats: Record; before('disable monitoring and pull local stats', async () => { - await es.cluster.put_settings({ body: disableCollection }); + await es.cluster.putSettings({ body: disableCollection }); await new Promise((r) => setTimeout(r, 1000)); const { body } = await supertest diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts index 749bea2961348..9e9211c4b5893 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -25,15 +25,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/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index 950fde37e3078..c202111f0e5e4 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -438,10 +438,10 @@ export default ({ getService }: FtrProviderContext): void => { }); // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -471,10 +471,10 @@ export default ({ getService }: FtrProviderContext): void => { }); // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -500,10 +500,10 @@ export default ({ getService }: FtrProviderContext): void => { }); // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.closed ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses['in-progress'] ); }); @@ -573,10 +573,10 @@ export default ({ getService }: FtrProviderContext): void => { let signals = await getSignals(); // There should be no change in their status since syncing is disabled expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source.signal.status + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status ).to.be(CaseStatuses.open); expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source.signal.status + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status ).to.be(CaseStatuses.open); const updatedIndWithStatus: CasesResponse = (await setStatus({ @@ -597,10 +597,10 @@ export default ({ getService }: FtrProviderContext): void => { // There should still be no change in their status since syncing is disabled expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source.signal.status + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status ).to.be(CaseStatuses.open); expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source.signal.status + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status ).to.be(CaseStatuses.open); // turn on the sync settings @@ -623,15 +623,15 @@ export default ({ getService }: FtrProviderContext): void => { // alerts should be updated now that the expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source.signal.status + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status ).to.be(CaseStatuses.closed); expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source.signal.status + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status ).to.be(CaseStatuses.closed); // the duplicate signal id in the other index should not be affect (so its status should be open) expect( - signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source.signal.status + signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source?.signal.status ).to.be(CaseStatuses.open); }); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index d647bb09f804a..e5cc2489a12e9 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -96,7 +96,7 @@ export default function ({ getService }: FtrProviderContext) { let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -116,7 +116,7 @@ export default function ({ getService }: FtrProviderContext) { signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses['in-progress'] ); }); @@ -156,10 +156,10 @@ export default function ({ getService }: FtrProviderContext) { ids: [signalID, signalID2], }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -183,10 +183,10 @@ export default function ({ getService }: FtrProviderContext) { ids: [signalID, signalID2], }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses['in-progress'] ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses['in-progress'] ); }); @@ -244,10 +244,10 @@ export default function ({ getService }: FtrProviderContext) { }); // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -272,10 +272,10 @@ export default function ({ getService }: FtrProviderContext) { }); // There still should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -302,10 +302,10 @@ export default function ({ getService }: FtrProviderContext) { ids: [signalID, signalID2], }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.closed ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses['in-progress'] ); }); @@ -366,10 +366,10 @@ export default function ({ getService }: FtrProviderContext) { }); // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -408,10 +408,10 @@ export default function ({ getService }: FtrProviderContext) { }); // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses.open ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.open ); @@ -453,10 +453,10 @@ export default function ({ getService }: FtrProviderContext) { }); // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( CaseStatuses['in-progress'] ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( CaseStatuses.closed ); }); diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 96806af37e2c1..6fb108f69ad22 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -6,10 +6,11 @@ */ import expect from '@kbn/expect'; -import { Client } from '@elastic/elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; + import * as st from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; -import { Explanation, SearchResponse } from 'elasticsearch'; import { CASES_URL, SUB_CASES_PATCH_DEL_URL } from '../../../../plugins/cases/common/constants'; import { CasesConfigureRequest, @@ -28,19 +29,11 @@ import { getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; -interface Hit { - _index: string; - _type: string; - _id: string; - _score: number; - _source: T; - _version?: number; - _explanation?: Explanation; - fields?: any; - highlight?: any; - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; +function toArray(input: T | T[]): T[] { + if (Array.isArray(input)) { + return input; + } + return [input]; } /** @@ -51,11 +44,11 @@ export const getSignalsWithES = async ({ indices, ids, }: { - es: Client; + es: KibanaClient; indices: string | string[]; ids: string | string[]; -}): Promise>>> => { - const signals = await es.search>({ +}): Promise>>> => { + const signals = await es.search({ index: indices, body: { size: 10000, @@ -64,7 +57,7 @@ export const getSignalsWithES = async ({ filter: [ { ids: { - values: ids, + values: toArray(ids), }, }, ], @@ -76,13 +69,13 @@ export const getSignalsWithES = async ({ return signals.body.hits.hits.reduce((acc, hit) => { let indexMap = acc.get(hit._index); if (indexMap === undefined) { - indexMap = new Map>([[hit._id, hit]]); + indexMap = new Map>([[hit._id, hit]]); } else { indexMap.set(hit._id, hit); } acc.set(hit._index, indexMap); return acc; - }, new Map>>()); + }, new Map>>()); }; interface SetStatusCasesParams { @@ -346,7 +339,7 @@ export const removeServerGeneratedPropertiesFromConfigure = ( return rest; }; -export const deleteAllCaseItems = async (es: Client) => { +export const deleteAllCaseItems = async (es: KibanaClient) => { await Promise.all([ deleteCases(es), deleteSubCases(es), @@ -356,9 +349,10 @@ export const deleteAllCaseItems = async (es: Client) => { ]); }; -export const deleteCasesUserActions = async (es: Client): Promise => { +export const deleteCasesUserActions = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:cases-user-actions', wait_for_completion: true, refresh: true, @@ -366,9 +360,10 @@ export const deleteCasesUserActions = async (es: Client): Promise => { }); }; -export const deleteCases = async (es: Client): Promise => { +export const deleteCases = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:cases', wait_for_completion: true, refresh: true, @@ -380,9 +375,10 @@ export const deleteCases = async (es: Client): Promise => { * Deletes all sub cases in the .kibana index. This uses ES to perform the delete and does * not go through the case API. */ -export const deleteSubCases = async (es: Client): Promise => { +export const deleteSubCases = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:cases-sub-case', wait_for_completion: true, refresh: true, @@ -390,9 +386,10 @@ export const deleteSubCases = async (es: Client): Promise => { }); }; -export const deleteComments = async (es: Client): Promise => { +export const deleteComments = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:cases-comments', wait_for_completion: true, refresh: true, @@ -400,9 +397,10 @@ export const deleteComments = async (es: Client): Promise => { }); }; -export const deleteConfiguration = async (es: Client): Promise => { +export const deleteConfiguration = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:cases-configure', wait_for_completion: true, refresh: true, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts index dd0052b03382a..8f7d2a0c01771 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts @@ -99,6 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: migrationResults } = await es.search({ index: newIndex }); expect(migrationResults.hits.hits).length(1); + // @ts-expect-error _source has unknown type const migratedSignal = migrationResults.hits.hits[0]._source.signal; expect(migratedSignal._meta.version).to.equal(SIGNALS_TEMPLATE_VERSION); }); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 683d57081a267..a9c128ee87703 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -6,7 +6,8 @@ */ import { KbnClient } from '@kbn/test'; -import { ApiResponse, Client } from '@elastic/elasticsearch'; +import type { ApiResponse } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; @@ -383,7 +384,7 @@ export const deleteAllAlerts = async ( ); }; -export const downgradeImmutableRule = async (es: Client, ruleId: string): Promise => { +export const downgradeImmutableRule = async (es: KibanaClient, ruleId: string): Promise => { return countDownES(async () => { return es.updateByQuery({ index: '.kibana', @@ -408,9 +409,10 @@ export const downgradeImmutableRule = async (es: Client, ruleId: string): Promis * Remove all timelines from the .kibana index * @param es The ElasticSearch handle */ -export const deleteAllTimelines = async (es: Client): Promise => { +export const deleteAllTimelines = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:siem-ui-timeline', wait_for_completion: true, refresh: true, @@ -423,10 +425,11 @@ export const deleteAllTimelines = async (es: Client): Promise => { * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllRulesStatuses = async (es: Client): Promise => { +export const deleteAllRulesStatuses = async (es: KibanaClient): Promise => { return countDownES(async () => { return es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:siem-detection-engine-rule-status', wait_for_completion: true, refresh: true, @@ -1176,7 +1179,7 @@ export const getIndexNameFromLoad = (loadResponse: Record): str * @param esClient elasticsearch {@link Client} * @param index name of the index to query */ -export const waitForIndexToPopulate = async (es: Client, index: string): Promise => { +export const waitForIndexToPopulate = async (es: KibanaClient, index: string): Promise => { await waitFor(async () => { const response = await es.count<{ count: number }>({ index }); return response.body.count > 0; diff --git a/x-pack/test/fleet_api_integration/apis/agents/acks.ts b/x-pack/test/fleet_api_integration/apis/agents/acks.ts index 6b68d8b28a43e..427c9c2394d9d 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/acks.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/acks.ts @@ -35,6 +35,7 @@ export default function (providerContext: FtrProviderContext) { index: '.fleet-agents', id: 'agent1', }); + // @ts-expect-error has unknown type agentDoc.access_api_key_id = apiKey.id; await esClient.update({ index: '.fleet-agents', @@ -239,11 +240,11 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - const res = await esClient.get({ + const res = await esClient.get<{ upgraded_at: unknown }>({ index: '.fleet-agents', id: 'agent1', }); - expect(res.body._source.upgraded_at).to.be.ok(); + expect(res.body._source?.upgraded_at).to.be.ok(); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/checkin.ts b/x-pack/test/fleet_api_integration/apis/agents/checkin.ts index 778063dff72c7..9150b81abf366 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/checkin.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/checkin.ts @@ -38,6 +38,7 @@ export default function (providerContext: FtrProviderContext) { index: '.fleet-agents', id: 'agent1', }); + // @ts-expect-error agentDoc has unknown type agentDoc.access_api_key_id = apiKey.id; await esClient.update({ index: '.fleet-agents', diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index 8924c090da089..09a0d3c927e4c 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -45,9 +45,11 @@ export default function (providerContext: FtrProviderContext) { index: '.fleet-agents', id: 'agent1', }); - // @ts-ignore + // @ts-expect-error agentDoc has unknown type agentDoc.access_api_key_id = accessAPIKeyId; + // @ts-expect-error agentDoc has unknown type agentDoc.default_api_key_id = outputAPIKeyBody.id; + // @ts-expect-error agentDoc has unknown type agentDoc.default_api_key = Buffer.from( `${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}` ).toString('base64'); diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index 237b9617dafb9..c9709475d182d 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -80,6 +80,7 @@ export default function (providerContext: FtrProviderContext) { applications: [], run_as: [], metadata: {}, + // @ts-expect-error @elastic/elasticsearch PutRoleRequest.body doesn't declare transient_metadata property transient_metadata: { enabled: true }, }, }); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts index e63aff3a08186..980060fea1c47 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts @@ -16,6 +16,7 @@ export default function ({ getService }: FtrProviderContext) { const supportedTestSuites = [ { suiteTitle: 'supported job with aggregation field', + // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_supported_aggs_${ts}`, job_type: 'anomaly_detector', @@ -102,6 +103,7 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteTitle: 'supported job with scripted field', + // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_supported_script_${ts}`, job_type: 'anomaly_detector', @@ -176,6 +178,7 @@ export default function ({ getService }: FtrProviderContext) { const unsupportedTestSuites = [ { suiteTitle: 'unsupported job with bucket_script aggregation field', + // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_unsupported_aggs_${ts}`, job_type: 'anomaly_detector', @@ -280,6 +283,7 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteTitle: 'unsupported job with partition by of a scripted field', + // @ts-expect-error not convertable to Job type jobConfig: { job_id: `fq_unsupported_script_${ts}`, job_type: 'anomaly_detector', diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts index 81d05ee94e6c4..97cc6be95ed93 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +// @ts-expect-error doesn't implement the full interface const JOB_CONFIG: Job = { job_id: `fq_single_1_smv`, description: 'mean(responsetime) on farequote dataset with 15m bucket span', @@ -27,6 +28,7 @@ const JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error doesn't implement the full interface const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_single_1_smv', indices: ['ft_farequote'], diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index ff38544fa8c03..a08a5cfca2afd 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +// @ts-expect-error not full interface const JOB_CONFIG: Job = { job_id: `fq_multi_1_ae`, description: @@ -28,6 +29,7 @@ const JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error not full interface const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_multi_1_se', indices: ['ft_farequote'], diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts index ec25c997e2770..950ad2a702b06 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +// @ts-expect-error not full interface const JOB_CONFIG: Job = { job_id: `fq_single_1_smv`, description: 'mean(responsetime) on farequote dataset with 15m bucket span', @@ -27,6 +28,7 @@ const JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error not full interface const DATAFEED_CONFIG: Datafeed = { datafeed_id: 'datafeed-fq_single_1_smv', indices: ['ft_farequote'], @@ -91,6 +93,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('with entity fields', function () { + // @ts-expect-error not full interface const jobConfig: Job = { job_id: `ecom_01`, description: @@ -121,6 +124,7 @@ export default function ({ getService }: FtrProviderContext) { model_plot_config: { enabled: true }, }; + // @ts-expect-error not full interface const datafeedConfig: Datafeed = { datafeed_id: 'datafeed-ecom_01', indices: ['ft_ecommerce'], diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 8d29b611c0bf5..ac8ff055209c9 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -136,10 +136,11 @@ export default function ({ getService }: FtrProviderContext) { description: 'Test calendar', }); await ml.api.createCalendarEvents(calendarId, [ + // @ts-expect-error not full interface { description: eventDescription, - start_time: 1513641600000, - end_time: 1513728000000, + start_time: '1513641600000', + end_time: '1513728000000', }, ]); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 71ac9c00032dc..95d0d20916429 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -137,10 +137,11 @@ export default function ({ getService }: FtrProviderContext) { description: 'Test calendar', }); await ml.api.createCalendarEvents(calendarId, [ + // @ts-expect-error not full interface { description: eventDescription, - start_time: 1513641600000, - end_time: 1513728000000, + start_time: '1513641600000', + end_time: '1513728000000', }, ]); diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts index 968eafab21ab4..2beae2ad007b5 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts +++ b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts @@ -21,6 +21,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); await asyncForEach(jobConfigs, async (jobConfig) => { + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(jobConfig); }); await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts index 9242c2e1b594c..1237cd8d71b8c 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts +++ b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); await asyncForEach(jobConfigs, async (jobConfig) => { + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(jobConfig); }); @@ -34,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { job_ids: jobConfigs.map((c) => c.job_id), description: 'Test calendar', }); + // @ts-expect-error not full interface await ml.api.createCalendarEvents(calendarId, testEvents); await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index c9cfe1fee4ef9..7d09deff6f6b7 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,10 +5,10 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; -import { IndexDocumentParams } from 'elasticsearch'; -import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; +import { Calendar } from '../../../../plugins/ml/server/models/calendar/index'; import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -24,18 +24,8 @@ import { } from '../../../../plugins/ml/common/constants/index_patterns'; import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; -interface EsIndexResult { - _index: string; - _id: string; - _version: number; - result: string; - _shards: any; - _seq_no: number; - _primary_term: number; -} - export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { - const es = getService('legacyEs'); + const es = getService('es'); const log = getService('log'); const retry = getService('retry'); const esSupertest = getService('esSupertest'); @@ -44,7 +34,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return { async hasJobResults(jobId: string): Promise { - const response = await es.search({ + const { body } = await es.search({ index: '.ml-anomalies-*', body: { size: 1, @@ -56,7 +46,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, }); - return response.hits.hits.length > 0; + return body.hits.hits.length > 0; }, async assertJobResultsExist(jobId: string) { @@ -84,7 +74,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, async hasDetectorResults(jobId: string, detectorIndex: number): Promise { - const response = await es.search({ + const { body } = await es.search({ index: '.ml-anomalies-*', body: { size: 1, @@ -112,7 +102,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, }); - return response.hits.hits.length > 0; + return body.hits.hits.length > 0; }, async assertDetectorResultsExist(jobId: string, detectorIndex: number) { @@ -133,13 +123,13 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async createIndices(indices: string) { log.debug(`Creating indices: '${indices}'...`); - if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) { + if ((await es.indices.exists({ index: indices, allow_no_indices: false })).body === true) { log.debug(`Indices '${indices}' already exist. Nothing to create.`); return; } - const createResponse = await es.indices.create({ index: indices }); - expect(createResponse) + const { body } = await es.indices.create({ index: indices }); + expect(body) .to.have.property('acknowledged') .eql(true, 'Response for create request indices should be acknowledged.'); @@ -149,15 +139,15 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async deleteIndices(indices: string) { log.debug(`Deleting indices: '${indices}'...`); - if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { + if ((await es.indices.exists({ index: indices, allow_no_indices: false })).body === false) { log.debug(`Indices '${indices}' don't exist. Nothing to delete.`); return; } - const deleteResponse = await es.indices.delete({ + const { body } = await es.indices.delete({ index: indices, }); - expect(deleteResponse) + expect(body) .to.have.property('acknowledged') .eql(true, 'Response for delete request should be acknowledged.'); @@ -316,7 +306,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async assertIndicesExist(indices: string) { await retry.tryForTime(30 * 1000, async () => { - if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) { + if ((await es.indices.exists({ index: indices, allow_no_indices: false })).body === true) { return true; } else { throw new Error(`indices '${indices}' should exist`); @@ -326,7 +316,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async assertIndicesNotToExist(indices: string) { await retry.tryForTime(30 * 1000, async () => { - if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { + if ((await es.indices.exists({ index: indices, allow_no_indices: false })).body === false) { return true; } else { throw new Error(`indices '${indices}' should not exist`); @@ -336,14 +326,14 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async assertIndicesNotEmpty(indices: string) { await retry.tryForTime(30 * 1000, async () => { - const response = await es.search({ + const { body } = await es.search({ index: indices, body: { size: 1, }, }); - if (response.hits.hits.length > 0) { + if (body.hits.hits.length > 0) { return true; } else { throw new Error(`indices '${indices}' should not be empty`); @@ -393,7 +383,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, - async createCalendarEvents(calendarId: string, events: CalendarEvent[]) { + async createCalendarEvents(calendarId: string, events: estypes.ScheduledEvent[]) { log.debug(`Creating events for calendar with id '${calendarId}'...`); await esSupertest.post(`/_ml/calendars/${calendarId}/events`).send({ events }).expect(200); await this.waitForEventsToExistInCalendar(calendarId, events); @@ -405,10 +395,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, assertAllEventsExistInCalendar: ( - eventsToCheck: CalendarEvent[], + eventsToCheck: estypes.ScheduledEvent[], calendar: Calendar ): boolean => { - const updatedCalendarEvents = calendar.events as CalendarEvent[]; + const updatedCalendarEvents = calendar.events; let allEventsAreUpdated = true; for (const eventToCheck of eventsToCheck) { // if at least one of the events that we need to check is not in the updated events @@ -417,8 +407,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { updatedCalendarEvents.findIndex( (updatedEvent) => updatedEvent.description === eventToCheck.description && - updatedEvent.start_time === eventToCheck.start_time && - updatedEvent.end_time === eventToCheck.end_time + // updatedEvent are fetched with suptertest which converts start_time and end_time to number + // sometimes eventToCheck declared manually with types incompatible with estypes.ScheduledEvent + String(updatedEvent.start_time) === String(eventToCheck.start_time) && + String(updatedEvent.end_time) === String(eventToCheck.end_time) ) < 0 ) { allEventsAreUpdated = false; @@ -436,7 +428,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async waitForEventsToExistInCalendar( calendarId: string, - eventsToCheck: CalendarEvent[], + eventsToCheck: estypes.ScheduledEvent[], errorMsg?: string ) { await retry.waitForWithTimeout(`'${calendarId}' events to exist`, 5 * 1000, async () => { @@ -805,7 +797,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async getAnnotations(jobId: string) { log.debug(`Fetching annotations for job '${jobId}'...`); - const results = await es.search({ + const { body } = await es.search({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, body: { query: { @@ -815,16 +807,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, }, }); - expect(results).to.not.be(undefined); - expect(results).to.have.property('hits'); + expect(body).to.not.be(undefined); + expect(body).to.have.property('hits'); log.debug('> Annotations fetched.'); - return results.hits.hits; + return body.hits.hits; }, async getAnnotationById(annotationId: string): Promise { log.debug(`Fetching annotation '${annotationId}'...`); - const result = await es.search({ + const { body } = await es.search({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, body: { size: 1, @@ -836,9 +828,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, }); log.debug('> Annotation fetched.'); - // @ts-ignore due to outdated type for hits.total - if (result.hits.total.value === 1) { - return result?.hits?.hits[0]?._source as Annotation; + + // @ts-expect-error doesn't handle total as number + if (body.hits.total.value === 1) { + return body?.hits?.hits[0]?._source as Annotation; } return undefined; }, @@ -846,15 +839,15 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async indexAnnotation(annotationRequestBody: Partial) { log.debug(`Indexing annotation '${JSON.stringify(annotationRequestBody)}'...`); // @ts-ignore due to outdated type for IndexDocumentParams.type - const params: IndexDocumentParams> = { + const params = { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, body: annotationRequestBody, refresh: 'wait_for', - }; - const results: EsIndexResult = await es.index(params); - await this.waitForAnnotationToExist(results._id); + } as const; + const { body } = await es.index(params); + await this.waitForAnnotationToExist(body._id); log.debug('> Annotation indexed.'); - return results; + return body; }, async waitForAnnotationToExist(annotationId: string, errorMsg?: string) { diff --git a/x-pack/test/functional/services/ml/common_config.ts b/x-pack/test/functional/services/ml/common_config.ts index a16c0b88de900..af3d393a16754 100644 --- a/x-pack/test/functional/services/ml/common_config.ts +++ b/x-pack/test/functional/services/ml/common_config.ts @@ -12,6 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; +// @ts-expect-error not full interface const FQ_SM_JOB_CONFIG: Job = { job_id: ``, description: 'mean(responsetime) on farequote dataset with 15m bucket span', @@ -31,6 +32,7 @@ const FQ_SM_JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error not full interface const FQ_MM_JOB_CONFIG: Job = { job_id: `fq_multi_1_ae`, description: @@ -50,6 +52,7 @@ const FQ_MM_JOB_CONFIG: Job = { model_plot_config: { enabled: true }, }; +// @ts-expect-error not full interface const FQ_DATAFEED_CONFIG: Datafeed = { datafeed_id: '', indices: ['ft_farequote'], diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts index c3859e1044b4f..8fcf8be9fa493 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts @@ -80,8 +80,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { testJobId = job.job_id; // Set up jobs + // @ts-expect-error not full interface await ml.api.createAnomalyDetectionJob(job); await ml.api.openAnomalyDetectionJob(job.job_id); + // @ts-expect-error not full interface await ml.api.createDatafeed(datafeed); await ml.api.startDatafeed(datafeed.datafeed_id); await ml.api.waitForDatafeedState(datafeed.datafeed_id, DATAFEED_STATE.STARTED); diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 7a0b1b7f7934a..0512cede0a84f 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -7,7 +7,7 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; -import { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { getImportListItemAsBuffer } from '../../plugins/lists/common/schemas/request/import_list_item_schema.mock'; import { @@ -157,10 +157,11 @@ export const binaryToString = (res: any, callback: any): void => { * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllExceptions = async (es: Client): Promise => { +export const deleteAllExceptions = async (es: KibanaClient): Promise => { return countDownES(async () => { return es.deleteByQuery({ index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter q: 'type:exception-list or type:exception-list-agnostic', wait_for_completion: true, refresh: true, diff --git a/x-pack/test/observability_api_integration/trial/tests/annotations.ts b/x-pack/test/observability_api_integration/trial/tests/annotations.ts index 928d160a5df9e..1ea3460060bc9 100644 --- a/x-pack/test/observability_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/trial/tests/annotations.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { Annotation } from '../../../../plugins/observability/common/annotations'; -import { ESSearchHit } from '../../../../../typings/elasticsearch'; import { FtrProviderContext } from '../../common/ftr_provider_context'; const DEFAULT_INDEX_NAME = 'observability-annotations'; @@ -153,6 +152,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { track_total_hits: true, }); + // @ts-expect-error doesn't handle number expect(search.body.hits.total.value).to.be(1); expect(search.body.hits.hits[0]._source).to.eql(response.body._source); @@ -236,16 +236,15 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { }, }); - const initialSearch = await es.search({ + const initialSearch = await es.search({ index: DEFAULT_INDEX_NAME, track_total_hits: true, }); + // @ts-expect-error doesn't handler number expect(initialSearch.body.hits.total.value).to.be(2); - const [id1, id2] = initialSearch.body.hits.hits.map( - (hit: ESSearchHit) => hit._id - ); + const [id1, id2] = initialSearch.body.hits.hits.map((hit) => hit._id); expect( ( @@ -261,6 +260,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { track_total_hits: true, }); + // @ts-expect-error doesn't handler number expect(searchAfterFirstDelete.body.hits.total.value).to.be(1); expect(searchAfterFirstDelete.body.hits.hits[0]._id).to.be(id2); @@ -279,6 +279,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { track_total_hits: true, }); + // @ts-expect-error doesn't handle number expect(searchAfterSecondDelete.body.hits.total.value).to.be(0); }); }); diff --git a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts index 38e566a730506..2ba5ce3f7d5b1 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; -import { Client } from '@elastic/elasticsearch'; +import type { SuperTest } from 'supertest'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { AUTHENTICATION } from './authentication'; -export const createUsersAndRoles = async (es: Client, supertest: SuperTest) => { +export const createUsersAndRoles = async (es: KibanaClient, supertest: SuperTest) => { await supertest .put('/api/security/role/kibana_legacy_user') .send({ diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index 84f84e8752122..bb46beef41449 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -43,8 +43,10 @@ export default function ({ getService }: FtrProviderContext) { } async function getNumberOfSessionDocuments() { - return (await es.search({ index: '.kibana_security_session*' })).body.hits.total - .value as number; + return ( + // @ts-expect-error doesn't handle total as number + (await es.search({ index: '.kibana_security_session*' })).body.hits.total.value as number + ); } async function loginWithSAML(providerName: string) { diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index 3d24efc9b8e74..0b17f037dfbd9 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -38,8 +38,10 @@ export default function ({ getService }: FtrProviderContext) { } async function getNumberOfSessionDocuments() { - return (await es.search({ index: '.kibana_security_session*' })).body.hits.total - .value as number; + return ( + // @ts-expect-error doesn't handle total as number + (await es.search({ index: '.kibana_security_session*' })).body.hits.total.value as number + ); } async function loginWithSAML(providerName: string) { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts index 5d93b820f3016..d46b7723fcafe 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { SearchResponse } from 'elasticsearch'; import { ResolverPaginatedEvents, SafeEndpointEvent, @@ -49,7 +48,7 @@ export default function ({ getService }: FtrProviderContext) { const generator = new EndpointDocGenerator('data'); const searchForID = async (id: string) => { - return es.search>({ + return es.search({ index: eventsIndexPattern, body: { query: { @@ -57,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) { filter: [ { ids: { - values: id, + values: [id], }, }, ], @@ -89,7 +88,7 @@ export default function ({ getService }: FtrProviderContext) { ); // ensure that the event was inserted into ES - expect(eventWithBothIPs.body.hits.hits[0]._source.event?.id).to.be( + expect(eventWithBothIPs.body.hits.hits[0]._source?.event?.id).to.be( eventWithoutNetworkObject.event?.id ); }); @@ -175,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) { it('sets the event.ingested field', async () => { const resp = await searchForID(genData.eventsInfo[0]._id); - expect(resp.body.hits.hits[0]._source.event.ingested).to.not.be(undefined); + expect(resp.body.hits.hits[0]._source?.event.ingested).to.not.be(undefined); }); }); @@ -221,11 +220,11 @@ export default function ({ getService }: FtrProviderContext) { networkIndexData.eventsInfo[0]._id ); // Should be 'United States' - expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + expect(eventWithBothIPs.body.hits.hits[0]._source?.source.geo?.country_name).to.not.be( undefined ); // should be 'Iceland' - expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo?.country_name).to.not.be( + expect(eventWithBothIPs.body.hits.hits[0]._source?.destination.geo?.country_name).to.not.be( undefined ); @@ -233,18 +232,18 @@ export default function ({ getService }: FtrProviderContext) { networkIndexData.eventsInfo[1]._id ); // Should be 'United States' - expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + expect(eventWithBothIPs.body.hits.hits[0]._source?.source.geo?.country_name).to.not.be( undefined ); - expect(eventWithSourceOnly.body.hits.hits[0]._source.destination?.geo).to.be(undefined); + expect(eventWithSourceOnly.body.hits.hits[0]._source?.destination?.geo).to.be(undefined); }); it('does not set geoip fields for events in indices other than the network index', async () => { const eventWithBothIPs = await searchForID( processIndexData.eventsInfo[0]._id ); - expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo).to.be(undefined); - expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo).to.be(undefined); + expect(eventWithBothIPs.body.hits.hits[0]._source?.source.geo).to.be(undefined); + expect(eventWithBothIPs.body.hits.hits[0]._source?.destination.geo).to.be(undefined); }); }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index add6539470cc1..156043bd3c918 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -49,14 +49,6 @@ interface BulkCreateHeader { }; } -interface BulkResponse { - items: Array<{ - create: { - _id: string; - }; - }>; -} - export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); @@ -69,12 +61,13 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { array.push({ create: { _index: eventsIndex } }, doc); return array; }, []); - const bulkResp = await client.bulk({ body, refresh: true }); + const bulkResp = await client.bulk({ body, refresh: true }); const eventsInfo = events.map((event: Event, i: number) => { - return { event, _id: bulkResp.body.items[i].create._id }; + return { event, _id: bulkResp.body.items[i].create?._id }; }); + // @ts-expect-error @elastic/elasticsearch expected BulkResponseItemBase._id: string return { eventsInfo, indices: [eventsIndex] }; }, async createTrees( diff --git a/yarn.lock b/yarn.lock index 74bca3901dfe1..0d7afc4293e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1378,16 +1378,16 @@ version "0.0.0" uid "" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.3": - version "8.0.0-canary.3" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.3.tgz#b06b95b1370417ac700f30277814fbe7ad532760" - integrity sha512-D8kiFxip0IATzXS+5MAA3+4jnTPiJrpkW+FVNc9e3eq8iCIW/BIv9kPqEH55N/RlJxFwGTz5W4jmmFqBeamNFA== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.4": + version "8.0.0-canary.4" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.4.tgz#6f1a592974941baae347eb8c66a2006848349717" + integrity sha512-UexFloloyvGOhvMc1ePRHCy89sjQL6rPTTZkXAB/GcC8rCvA3mgSnIY2+Ylvctdv9o4l+M4Bkw8azICulyhwMg== dependencies: - debug "^4.1.1" + debug "^4.3.1" hpagent "^0.1.1" - ms "^2.1.1" + ms "^2.1.3" pump "^3.0.0" - secure-json-parse "^2.1.0" + secure-json-parse "^2.3.1" "@elastic/ems-client@7.12.0": version "7.12.0" @@ -11177,7 +11177,7 @@ debug@3.X, debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, dependencies: ms "^2.1.1" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -20069,6 +20069,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + msgpackr-extract@^0.3.5, msgpackr-extract@^0.3.6: version "0.3.6" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.6.tgz#f20c0a278e44377471b1fa2a3a75a32c87693755" @@ -24953,10 +24958,10 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" -secure-json-parse@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20" - integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA== +secure-json-parse@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.3.1.tgz#908aa5e806e223ff8d179d37ad95c2433f5f147d" + integrity sha512-5uGhQLHSC9tVa7RGPkSwxbZVsJCZvIODOadAimCXkU1aCa1fWdszj2DktcutK8A7dD58PoRdxTYiy0jFl6qjnw== seed-random@~2.2.0: version "2.2.0" From 1e87cef3e0659d1764d5a0eddb5bb0189535547a Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Thu, 25 Mar 2021 12:34:38 +0100 Subject: [PATCH 53/88] handling references for kibana_context and get_index_pattern expression functions (#95224) --- ...-public.executioncontext.getsavedobject.md | 13 -- ...ins-expressions-public.executioncontext.md | 1 - ...-server.executioncontext.getsavedobject.md | 13 -- ...ins-expressions-server.executioncontext.md | 1 - .../expressions/load_index_pattern.ts | 26 +++ .../search/expressions/kibana_context.ts | 167 +++++++++++------- .../search/expressions/kibana_context.ts | 39 ++++ .../data/public/search/search_service.ts | 8 +- .../search/expressions/kibana_context.ts | 46 +++++ .../data/server/search/search_service.ts | 4 +- .../expressions/common/execution/types.ts | 15 -- .../expressions/common/executor/executor.ts | 12 +- src/plugins/expressions/public/plugin.ts | 13 +- src/plugins/expressions/public/public.api.md | 3 - src/plugins/expressions/server/server.api.md | 3 - 15 files changed, 234 insertions(+), 130 deletions(-) delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md create mode 100644 src/plugins/data/public/search/expressions/kibana_context.ts create mode 100644 src/plugins/data/server/search/expressions/kibana_context.ts 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/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/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/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/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/server/search/expressions/kibana_context.ts b/src/plugins/data/server/search/expressions/kibana_context.ts new file mode 100644 index 0000000000000..c8fdbf4764b0e --- /dev/null +++ b/src/plugins/data/server/search/expressions/kibana_context.ts @@ -0,0 +1,46 @@ +/* + * Copyright 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/server'; +import { getKibanaContextFn } from '../../../common/search/expressions'; +import { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +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 (getKibanaRequest) => { + const request = getKibanaRequest && getKibanaRequest(); + if (!request) { + throw new Error('KIBANA_CONTEXT_KIBANA_REQUEST_MISSING'); + } + + const [{ savedObjects }] = await getStartServices(); + return { + savedObjectsClient: (savedObjects.getScopedClient( + request + ) as any) as SavedObjectsClientCommon, + }; + }); +} diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 4dcab4eda34d1..fdf0b66197b34 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -53,7 +53,6 @@ import { ISearchOptions, kibana, kibanaContext, - kibanaContextFunction, kibanaTimerangeFunction, kibanaFilterFunction, kqlFunction, @@ -75,6 +74,7 @@ import { ConfigSchema } from '../../config'; import { ISearchSessionService, SearchSessionService } from './session'; import { KbnServerError } from '../../../kibana_utils/server'; import { registerBsearchRoute } from './routes/bsearch'; +import { getKibanaContext } from './expressions/kibana_context'; type StrategyMap = Record>; @@ -154,7 +154,7 @@ export class SearchService implements Plugin { expressions.registerFunction(luceneFunction); expressions.registerFunction(kqlFunction); expressions.registerFunction(kibanaTimerangeFunction); - expressions.registerFunction(kibanaContextFunction); + expressions.registerFunction(getKibanaContext({ getStartServices: core.getStartServices })); expressions.registerFunction(fieldFunction); expressions.registerFunction(rangeFunction); expressions.registerFunction(kibanaFilterFunction); diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index a897ef5222bfa..d9c8682567b30 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -11,7 +11,6 @@ import type { KibanaRequest } from 'src/core/server'; import { ExpressionType, SerializableState } from '../expression_types'; import { Adapters, RequestAdapter } from '../../../inspector/common'; -import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; import { TablesAdapter } from '../util/tables_adapter'; /** @@ -59,20 +58,6 @@ export interface ExecutionContext< */ getKibanaRequest?: () => KibanaRequest; - /** - * 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. - */ - getSavedObject?: ( - type: string, - id: string - ) => Promise>; - /** * Returns the state (true|false) of the sync colors across panels switch. */ diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 7962fe723d19f..255de31f7239b 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -215,17 +215,25 @@ export class Executor = Record { - link.arguments = fn.inject(link.arguments, references); + link.arguments = fn.inject( + link.arguments, + references + .filter((r) => r.name.includes(`l${linkId}_`)) + .map((r) => ({ ...r, name: r.name.replace(`l${linkId}_`, '') })) + ); + linkId++; }); } public extract(ast: ExpressionAstExpression) { + let linkId = 0; const allReferences: SavedObjectReference[] = []; const newAst = this.walkAst(cloneDeep(ast), (fn, link) => { const { state, references } = fn.extract(link.arguments); link.arguments = state; - allReferences.push(...references); + allReferences.push(...references.map((r) => ({ ...r, name: `l${linkId++}_${r.name}` }))); }); return { state: newAst, references: allReferences }; } diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 2bff5e09352e4..2410ad8741312 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -7,12 +7,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { - ExpressionsService, - ExpressionsServiceSetup, - ExecutionContext, - ExpressionsServiceStart, -} from '../common'; +import { ExpressionsService, ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; import { setRenderersRegistry, setNotifications, setExpressionsService } from './services'; import { ReactExpressionRenderer } from './react_expression_renderer'; import { ExpressionLoader, IExpressionLoader, loader } from './loader'; @@ -42,14 +37,8 @@ export class ExpressionsPublicPlugin implements Plugin { - const [start] = await core.getStartServices(); - return start.savedObjects.client.get(type, id); - }; - executor.extendContext({ environment: 'client', - getSavedObject, }); } diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 029d727e82e74..b3e7803f97c38 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -137,9 +137,6 @@ export type ExecutionContainer = StateContainer { abortSignal: AbortSignal; getKibanaRequest?: () => KibanaRequest; - // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts - getSavedObject?: (type: string, id: string) => Promise>; getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; inspectorAdapters: InspectorAdapters; diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index de9797843a4ab..2d873fa518306 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -135,9 +135,6 @@ export type ExecutionContainer = StateContainer { abortSignal: AbortSignal; getKibanaRequest?: () => KibanaRequest; - // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts - getSavedObject?: (type: string, id: string) => Promise>; getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; inspectorAdapters: InspectorAdapters; From 9d47330ccffc0694e226299addfbbcafb8891b83 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 25 Mar 2021 08:30:46 -0400 Subject: [PATCH 54/88] [alerting] add user facing doc on event log ILM policy (#92736) resolves https://github.com/elastic/kibana/issues/82435 Just provided a brief description, name of the policy, mentioned we create it but never modify it, provided the default values, and mentioned it could be updated by customers for their environment. Not sure we want to provide more info than that. --- .../alerting-production-considerations.asciidoc | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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]. From 80e53d5fe68e0918b7a79518883e9b0b7dce2617 Mon Sep 17 00:00:00 2001 From: igoristic Date: Thu, 25 Mar 2021 09:02:51 -0400 Subject: [PATCH 55/88] [Monitoring] Remove license check for alerting (#94874) * Removed license check for alerting * Fixed tests and CR feedback * Fixed test --- .../components/cluster/listing/listing.js | 13 +- .../__fixtures__/create_stubs.js | 34 --- .../cluster_alerts/alerts_cluster_search.js | 227 ----------------- .../alerts_cluster_search.test.js | 194 --------------- .../alerts_clusters_aggregation.js | 127 ---------- .../alerts_clusters_aggregation.test.js | 235 ------------------ .../server/cluster_alerts/check_license.js | 111 --------- .../cluster_alerts/check_license.test.js | 149 ----------- .../verify_monitoring_license.js | 48 ---- .../verify_monitoring_license.test.js | 88 ------- .../monitoring/server/deprecations.test.js | 17 -- .../plugins/monitoring/server/deprecations.ts | 4 +- .../lib/cluster/get_clusters_from_request.js | 49 +--- .../translations/translations/ja-JP.json | 9 - .../translations/translations/zh-CN.json | 9 - .../cluster/fixtures/multicluster.json | 6 +- .../standalone_cluster/fixtures/clusters.json | 12 +- .../apps/monitoring/cluster/list.js | 2 +- 18 files changed, 11 insertions(+), 1323 deletions(-) delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/__fixtures__/create_stubs.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/check_license.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.js delete mode 100644 x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index d4b8ea4a76e43..12cfc4f132863 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -41,17 +41,14 @@ const IsClusterSupported = ({ isSupported, children }) => { * completely */ const IsAlertsSupported = (props) => { - const { alertsMeta = { enabled: true }, clusterMeta = { enabled: true } } = props.cluster.alerts; - if (alertsMeta.enabled && clusterMeta.enabled) { + const { alertsMeta = { enabled: true } } = props.cluster.alerts; + if (alertsMeta.enabled) { return {props.children}; } - const message = - alertsMeta.message || - clusterMeta.message || - i18n.translate('xpack.monitoring.cluster.listing.unknownHealthMessage', { - defaultMessage: 'Unknown', - }); + const message = i18n.translate('xpack.monitoring.cluster.listing.unknownHealthMessage', { + defaultMessage: 'Unknown', + }); return ( diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/__fixtures__/create_stubs.js b/x-pack/plugins/monitoring/server/cluster_alerts/__fixtures__/create_stubs.js deleted file mode 100644 index cf8aba8ca7008..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/__fixtures__/create_stubs.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import sinon from 'sinon'; - -export function createStubs(mockQueryResult, featureStub) { - const callWithRequestStub = sinon.stub().returns(Promise.resolve(mockQueryResult)); - const getClusterStub = sinon.stub().returns({ callWithRequest: callWithRequestStub }); - const configStub = sinon.stub().returns({ - get: sinon.stub().withArgs('xpack.monitoring.cluster_alerts.enabled').returns(true), - }); - return { - callWithRequestStub, - mockReq: { - server: { - config: configStub, - plugins: { - monitoring: { - info: { - feature: featureStub, - }, - }, - elasticsearch: { - getCluster: getClusterStub, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.js b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.js deleted file mode 100644 index 05f0524c12521..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.js +++ /dev/null @@ -1,227 +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 { get } from 'lodash'; -import moment from 'moment'; -import { verifyMonitoringLicense } from './verify_monitoring_license'; -import { i18n } from '@kbn/i18n'; - -/** - * Retrieve any statically defined cluster alerts (not indexed) for the {@code cluster}. - * - * In the future, if we add other static cluster alerts, then we should probably just return an array. - * It may also make sense to put this into its own file in the future. - * - * @param {Object} cluster The cluster object containing the cluster's license. - * @return {Object} The alert to use for the cluster. {@code null} if none. - */ -export function staticAlertForCluster(cluster) { - const clusterNeedsTLSEnabled = get(cluster, 'license.cluster_needs_tls', false); - - if (clusterNeedsTLSEnabled) { - const versionParts = get(cluster, 'version', '').split('.'); - const version = versionParts.length > 1 ? `${versionParts[0]}.${versionParts[1]}` : 'current'; - - return { - metadata: { - severity: 0, - cluster_uuid: cluster.cluster_uuid, - link: `https://www.elastic.co/guide/en/x-pack/${version}/ssl-tls.html`, - }, - update_timestamp: cluster.timestamp, - timestamp: get(cluster, 'license.issue_date', cluster.timestamp), - prefix: i18n.translate('xpack.monitoring.clusterAlerts.clusterNeedsTSLEnabledDescription', { - defaultMessage: - 'Configuring TLS will be required to apply a Gold or Platinum license when security is enabled.', - }), - message: i18n.translate('xpack.monitoring.clusterAlerts.seeDocumentationDescription', { - defaultMessage: 'See documentation for details.', - }), - }; - } - - return null; -} - -/** - * Append the static alert(s) for this {@code cluster}, limiting the response to {@code size} {@code alerts}. - * - * @param {Object} cluster The cluster object containing the cluster's license. - * @param {Array} alerts The existing cluster alerts. - * @param {Number} size The maximum size. - * @return {Array} The alerts array (modified or not). - */ -export function appendStaticAlerts(cluster, alerts, size) { - const staticAlert = staticAlertForCluster(cluster); - - if (staticAlert) { - // we can put it over any resolved alert, or anything with a lower severity (which is currently none) - // the alerts array is pre-sorted from highest severity to lowest; unresolved alerts are at the bottom - const alertIndex = alerts.findIndex( - (alert) => alert.resolved_timestamp || alert.metadata.severity < staticAlert.metadata.severity - ); - - if (alertIndex !== -1) { - // we can put it in the place of this alert - alerts.splice(alertIndex, 0, staticAlert); - } else { - alerts.push(staticAlert); - } - - // chop off the last item if necessary (when size is < alerts.length) - return alerts.slice(0, size); - } - - return alerts; -} - -/** - * Create a filter that should be used when no time range is supplied and thus only un-resolved cluster alerts should - * be returned. - * - * @return {Object} Query to restrict to un-resolved cluster alerts. - */ -export function createFilterForUnresolvedAlerts() { - return { - bool: { - must_not: { - exists: { - field: 'resolved_timestamp', - }, - }, - }, - }; -} - -/** - * Create a filter that should be used when {@code options} has start or end times. - * - * This enables us to search for cluster alerts that have been resolved within the given time frame, while also - * grabbing any un-resolved cluster alerts. - * - * @param {Object} options The options for the cluster search. - * @return {Object} Query to restrict to un-resolved cluster alerts or cluster alerts resolved within the time range. - */ -export function createFilterForTime(options) { - const timeFilter = {}; - - if (options.start) { - timeFilter.gte = moment.utc(options.start).valueOf(); - } - - if (options.end) { - timeFilter.lte = moment.utc(options.end).valueOf(); - } - - return { - bool: { - should: [ - { - range: { - resolved_timestamp: { - format: 'epoch_millis', - ...timeFilter, - }, - }, - }, - { - bool: { - must_not: { - exists: { - field: 'resolved_timestamp', - }, - }, - }, - }, - ], - }, - }; -} - -/** - * @param {Object} req Request object from the API route - * @param {String} cluster The cluster being checked - */ -export async function alertsClusterSearch(req, alertsIndex, cluster, checkLicense, options = {}) { - const verification = await verifyMonitoringLicense(req.server); - - if (!verification.enabled) { - return Promise.resolve({ message: verification.message }); - } - - const license = get(cluster, 'license', {}); - const prodLicenseInfo = checkLicense(license.type, license.status === 'active', 'production'); - - if (prodLicenseInfo.clusterAlerts.enabled) { - const config = req.server.config(); - const size = options.size || config.get('monitoring.ui.max_bucket_size'); - - const params = { - index: alertsIndex, - ignoreUnavailable: true, - filterPath: 'hits.hits._source', - body: { - size, - query: { - bool: { - must: [ - { - // This will cause anything un-resolved to be sorted above anything that is resolved - // From there, those items are sorted by their severity, then by their timestamp (age) - function_score: { - boost_mode: 'max', - functions: [ - { - filter: { - bool: { - must_not: [ - { - exists: { - field: 'resolved_timestamp', - }, - }, - ], - }, - }, - weight: 2, - }, - ], - }, - }, - ], - filter: [ - { - term: { 'metadata.cluster_uuid': cluster.cluster_uuid }, - }, - ], - }, - }, - sort: [ - '_score', - { 'metadata.severity': { order: 'desc' } }, - { timestamp: { order: 'asc' } }, - ], - }, - }; - - if (options.start || options.end) { - params.body.query.bool.filter.push(createFilterForTime(options)); - } else { - params.body.query.bool.filter.push(createFilterForUnresolvedAlerts()); - } - - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - return callWithRequest(req, 'search', params).then((result) => { - const hits = get(result, 'hits.hits', []); - const alerts = hits.map((alert) => alert._source); - - return appendStaticAlerts(cluster, alerts, size); - }); - } - - return Promise.resolve({ message: prodLicenseInfo.message }); -} diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js deleted file mode 100644 index 8b655e23cb430..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js +++ /dev/null @@ -1,194 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { createStubs } from './__fixtures__/create_stubs'; -import { alertsClusterSearch } from './alerts_cluster_search'; - -const mockAlerts = [ - { - metadata: { - severity: 1, - }, - }, - { - metadata: { - severity: -1, - }, - }, - { - metadata: { - severity: 2000, - }, - resolved_timestamp: 'now', - }, -]; - -const mockQueryResult = { - hits: { - hits: [ - { - _source: mockAlerts[0], - }, - { - _source: mockAlerts[1], - }, - { - _source: mockAlerts[2], - }, - ], - }, -}; - -// TODO: tests were not running and are not up to date. -describe.skip('Alerts Cluster Search', () => { - describe('License checks pass', () => { - const featureStub = sinon.stub().returns({ - getLicenseCheckResults: () => ({ clusterAlerts: { enabled: true } }), - }); - const checkLicense = () => ({ clusterAlerts: { enabled: true } }); - - it('max hit count option', () => { - const { mockReq, callWithRequestStub } = createStubs(mockQueryResult, featureStub); - return alertsClusterSearch( - mockReq, - '.monitoring-alerts', - { cluster_uuid: 'cluster-1234' }, - checkLicense - ).then((alerts) => { - expect(alerts).to.eql(mockAlerts); - expect(callWithRequestStub.getCall(0).args[2].body.size).to.be.undefined; - }); - }); - - it('set hit count option', () => { - const { mockReq, callWithRequestStub } = createStubs(mockQueryResult, featureStub); - return alertsClusterSearch( - mockReq, - '.monitoring-alerts', - { cluster_uuid: 'cluster-1234' }, - checkLicense, - { size: 3 } - ).then((alerts) => { - expect(alerts).to.eql(mockAlerts); - expect(callWithRequestStub.getCall(0).args[2].body.size).to.be(3); - }); - }); - - it('should report static info-level alert in the right location', () => { - const { mockReq, callWithRequestStub } = createStubs(mockQueryResult, featureStub); - const cluster = { - cluster_uuid: 'cluster-1234', - timestamp: 'fake-timestamp', - version: '6.1.0-throwmeaway2', - license: { - cluster_needs_tls: true, - issue_date: 'fake-issue_date', - }, - }; - return alertsClusterSearch(mockReq, '.monitoring-alerts', cluster, checkLicense, { - size: 3, - }).then((alerts) => { - expect(alerts).to.have.length(3); - expect(alerts[0]).to.eql(mockAlerts[0]); - expect(alerts[1]).to.eql({ - metadata: { - severity: 0, - - cluster_uuid: cluster.cluster_uuid, - link: 'https://www.elastic.co/guide/en/x-pack/6.1/ssl-tls.html', - }, - update_timestamp: cluster.timestamp, - timestamp: cluster.license.issue_date, - prefix: - 'Configuring TLS will be required to apply a Gold or Platinum license when security is enabled.', - message: 'See documentation for details.', - }); - expect(alerts[2]).to.eql(mockAlerts[1]); - expect(callWithRequestStub.getCall(0).args[2].body.size).to.be(3); - }); - }); - - it('should report static info-level alert at the end if necessary', () => { - const { mockReq, callWithRequestStub } = createStubs({ hits: { hits: [] } }, featureStub); - const cluster = { - cluster_uuid: 'cluster-1234', - timestamp: 'fake-timestamp', - version: '6.1.0-throwmeaway2', - license: { - cluster_needs_tls: true, - issue_date: 'fake-issue_date', - }, - }; - return alertsClusterSearch(mockReq, '.monitoring-alerts', cluster, checkLicense, { - size: 3, - }).then((alerts) => { - expect(alerts).to.have.length(1); - expect(alerts[0]).to.eql({ - metadata: { - severity: 0, - cluster_uuid: cluster.cluster_uuid, - link: 'https://www.elastic.co/guide/en/x-pack/6.1/ssl-tls.html', - }, - update_timestamp: cluster.timestamp, - timestamp: cluster.license.issue_date, - prefix: - 'Configuring TLS will be required to apply a Gold or Platinum license when security is enabled.', - message: 'See documentation for details.', - }); - expect(callWithRequestStub.getCall(0).args[2].body.size).to.be(3); - }); - }); - }); - - describe('License checks fail', () => { - it('monitoring cluster license checks fail', () => { - const featureStub = sinon.stub().returns({ - getLicenseCheckResults: () => ({ - message: 'monitoring cluster license check fail', - clusterAlerts: { enabled: false }, - }), - }); - const checkLicense = sinon.stub(); - const { mockReq, callWithRequestStub } = createStubs({}, featureStub); - return alertsClusterSearch( - mockReq, - '.monitoring-alerts', - { cluster_uuid: 'cluster-1234' }, - checkLicense - ).then((alerts) => { - const result = { message: 'monitoring cluster license check fail' }; - expect(alerts).to.eql(result); - expect(checkLicense.called).to.be(false); - expect(callWithRequestStub.called).to.be(false); - }); - }); - - it('production cluster license checks fail', () => { - // monitoring cluster passes - const featureStub = sinon.stub().returns({ - getLicenseCheckResults: () => ({ clusterAlerts: { enabled: true } }), - }); - const checkLicense = sinon - .stub() - .returns({ clusterAlerts: { enabled: false }, message: 'prod goes boom' }); - const { mockReq, callWithRequestStub } = createStubs({}, featureStub); - return alertsClusterSearch( - mockReq, - '.monitoring-alerts', - { cluster_uuid: 'cluster-1234' }, - checkLicense - ).then((alerts) => { - const result = { message: 'prod goes boom' }; - expect(alerts).to.eql(result); - expect(checkLicense.calledOnce).to.be(true); - expect(callWithRequestStub.called).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.js b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.js deleted file mode 100644 index 5c4194d063612..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.js +++ /dev/null @@ -1,127 +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 { get, find } from 'lodash'; -import { verifyMonitoringLicense } from './verify_monitoring_license'; -import { i18n } from '@kbn/i18n'; - -export async function alertsClustersAggregation(req, alertsIndex, clusters, checkLicense) { - const verification = await verifyMonitoringLicense(req.server); - - if (!verification.enabled) { - // return metadata detailing that alerts is disabled because of the monitoring cluster license - return Promise.resolve({ alertsMeta: verification }); - } - - const params = { - index: alertsIndex, - ignoreUnavailable: true, - filterPath: 'aggregations', - body: { - size: 0, - query: { - bool: { - must_not: [ - { - exists: { field: 'resolved_timestamp' }, - }, - ], - }, - }, - aggs: { - group_by_cluster: { - terms: { - field: 'metadata.cluster_uuid', - size: 10, - }, - aggs: { - group_by_severity: { - range: { - field: 'metadata.severity', - ranges: [ - { - key: 'low', - to: 1000, - }, - { - key: 'medium', - from: 1000, - to: 2000, - }, - { - key: 'high', - from: 2000, - }, - ], - }, - }, - }, - }, - }, - }, - }; - - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - return callWithRequest(req, 'search', params).then((result) => { - const buckets = get(result.aggregations, 'group_by_cluster.buckets'); - const meta = { alertsMeta: { enabled: true } }; - - return clusters.reduce((reClusters, cluster) => { - let alerts; - - const license = cluster.license || {}; - // check the license type of the production cluster for alerts feature support - const prodLicenseInfo = checkLicense(license.type, license.status === 'active', 'production'); - if (prodLicenseInfo.clusterAlerts.enabled) { - const clusterNeedsTLS = get(license, 'cluster_needs_tls', false); - const staticAlertCount = clusterNeedsTLS ? 1 : 0; - const bucket = find(buckets, { key: cluster.cluster_uuid }); - const bucketDocCount = get(bucket, 'doc_count', 0); - let severities = {}; - - if (bucket || staticAlertCount > 0) { - if (bucketDocCount > 0 || staticAlertCount > 0) { - const groupBySeverityBuckets = get(bucket, 'group_by_severity.buckets', []); - const lowGroup = find(groupBySeverityBuckets, { key: 'low' }) || {}; - const mediumGroup = find(groupBySeverityBuckets, { key: 'medium' }) || {}; - const highGroup = find(groupBySeverityBuckets, { key: 'high' }) || {}; - severities = { - low: (lowGroup.doc_count || 0) + staticAlertCount, - medium: mediumGroup.doc_count || 0, - high: highGroup.doc_count || 0, - }; - } - - alerts = { - count: bucketDocCount + staticAlertCount, - ...severities, - }; - } - } else { - // add metadata to the cluster's alerts object detailing that alerts are disabled because of the prod cluster license - alerts = { - clusterMeta: { - enabled: false, - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', - { - defaultMessage: - 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', - values: { - clusterName: cluster.cluster_name, - licenseType: `${license.type}`, - }, - } - ), - }, - }; - } - - return Object.assign(reClusters, { [cluster.cluster_uuid]: alerts }); - }, meta); - }); -} diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js deleted file mode 100644 index fcf840ebf6636..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js +++ /dev/null @@ -1,235 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { merge } from 'lodash'; -import { createStubs } from './__fixtures__/create_stubs'; -import { alertsClustersAggregation } from './alerts_clusters_aggregation'; - -const clusters = [ - { - cluster_uuid: 'cluster-abc0', - cluster_name: 'cluster-abc0-name', - license: { type: 'test_license' }, - }, - { - cluster_uuid: 'cluster-abc1', - cluster_name: 'cluster-abc1-name', - license: { type: 'test_license' }, - }, - { - cluster_uuid: 'cluster-abc2', - cluster_name: 'cluster-abc2-name', - license: { type: 'test_license' }, - }, - { - cluster_uuid: 'cluster-abc3', - cluster_name: 'cluster-abc3-name', - license: { type: 'test_license' }, - }, - { cluster_uuid: 'cluster-no-license', cluster_name: 'cluster-no-license-name' }, - { cluster_uuid: 'cluster-invalid', cluster_name: 'cluster-invalid-name', license: {} }, -]; -const mockQueryResult = { - aggregations: { - group_by_cluster: { - buckets: [ - { - key: 'cluster-abc1', - doc_count: 1, - group_by_severity: { - buckets: [{ key: 'low', doc_count: 1 }], - }, - }, - { - key: 'cluster-abc2', - doc_count: 2, - group_by_severity: { - buckets: [{ key: 'medium', doc_count: 2 }], - }, - }, - { - key: 'cluster-abc3', - doc_count: 3, - group_by_severity: { - buckets: [{ key: 'high', doc_count: 3 }], - }, - }, - ], - }, - }, -}; - -// TODO: tests were not running and are not up to date. -describe.skip('Alerts Clusters Aggregation', () => { - describe('with alerts enabled', () => { - const featureStub = sinon.stub().returns({ - getLicenseCheckResults: () => ({ clusterAlerts: { enabled: true } }), - }); - const checkLicense = () => ({ clusterAlerts: { enabled: true } }); - - it('aggregates alert count summary by cluster', () => { - const { mockReq } = createStubs(mockQueryResult, featureStub); - return alertsClustersAggregation(mockReq, '.monitoring-alerts', clusters, checkLicense).then( - (result) => { - expect(result).to.eql({ - alertsMeta: { enabled: true }, - 'cluster-abc0': undefined, - 'cluster-abc1': { - count: 1, - high: 0, - low: 1, - medium: 0, - }, - 'cluster-abc2': { - count: 2, - high: 0, - low: 0, - medium: 2, - }, - 'cluster-abc3': { - count: 3, - high: 3, - low: 0, - medium: 0, - }, - 'cluster-no-license': undefined, - 'cluster-invalid': undefined, - }); - } - ); - }); - - it('aggregates alert count summary by cluster include static alert', () => { - const { mockReq } = createStubs(mockQueryResult, featureStub); - const clusterLicenseNeedsTLS = { license: { cluster_needs_tls: true } }; - const newClusters = Array.from(clusters); - - newClusters[0] = merge({}, clusters[0], clusterLicenseNeedsTLS); - newClusters[1] = merge({}, clusters[1], clusterLicenseNeedsTLS); - - return alertsClustersAggregation( - mockReq, - '.monitoring-alerts', - newClusters, - checkLicense - ).then((result) => { - expect(result).to.eql({ - alertsMeta: { enabled: true }, - 'cluster-abc0': { - count: 1, - high: 0, - medium: 0, - low: 1, - }, - 'cluster-abc1': { - count: 2, - high: 0, - low: 2, - medium: 0, - }, - 'cluster-abc2': { - count: 2, - high: 0, - low: 0, - medium: 2, - }, - 'cluster-abc3': { - count: 3, - high: 3, - low: 0, - medium: 0, - }, - 'cluster-no-license': undefined, - 'cluster-invalid': undefined, - }); - }); - }); - }); - - describe('with alerts disabled due to license', () => { - it('returns the input set if disabled because monitoring cluster checks', () => { - // monitoring clusters' license check to fail - const featureStub = sinon.stub().returns({ - getLicenseCheckResults: () => ({ - clusterAlerts: { enabled: false }, - message: 'monitoring cluster license is fail', - }), - }); - // prod clusters' license check to pass - const checkLicense = () => ({ clusterAlerts: { enabled: true } }); - const { mockReq } = createStubs(mockQueryResult, featureStub); - - return alertsClustersAggregation(mockReq, '.monitoring-alerts', clusters, checkLicense).then( - (result) => { - expect(result).to.eql({ - alertsMeta: { enabled: false, message: 'monitoring cluster license is fail' }, - }); - } - ); - }); - - it('returns the input set if disabled because production cluster checks', () => { - // monitoring clusters' license check to pass - const featureStub = sinon.stub().returns({ - getLicenseCheckResults: () => ({ clusterAlerts: { enabled: true } }), - }); - // prod clusters license check to fail - const checkLicense = () => ({ clusterAlerts: { enabled: false } }); - const { mockReq } = createStubs(mockQueryResult, featureStub); - - return alertsClustersAggregation(mockReq, '.monitoring-alerts', clusters, checkLicense).then( - (result) => { - expect(result).to.eql({ - alertsMeta: { enabled: true }, - 'cluster-abc0': { - clusterMeta: { - enabled: false, - message: - 'Cluster [cluster-abc0-name] license type [test_license] does not support Cluster Alerts', - }, - }, - 'cluster-abc1': { - clusterMeta: { - enabled: false, - message: - 'Cluster [cluster-abc1-name] license type [test_license] does not support Cluster Alerts', - }, - }, - 'cluster-abc2': { - clusterMeta: { - enabled: false, - message: - 'Cluster [cluster-abc2-name] license type [test_license] does not support Cluster Alerts', - }, - }, - 'cluster-abc3': { - clusterMeta: { - enabled: false, - message: - 'Cluster [cluster-abc3-name] license type [test_license] does not support Cluster Alerts', - }, - }, - 'cluster-no-license': { - clusterMeta: { - enabled: false, - message: `Cluster [cluster-no-license-name] license type [undefined] does not support Cluster Alerts`, - }, - }, - 'cluster-invalid': { - clusterMeta: { - enabled: false, - message: `Cluster [cluster-invalid-name] license type [undefined] does not support Cluster Alerts`, - }, - }, - }); - } - ); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/check_license.js b/x-pack/plugins/monitoring/server/cluster_alerts/check_license.js deleted file mode 100644 index 1010c7c8d5036..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/check_license.js +++ /dev/null @@ -1,111 +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 { includes } from 'lodash'; -import { i18n } from '@kbn/i18n'; - -/** - * Function to do the work of checking license for cluster alerts feature support - * Can be used to power XpackInfo license check results as well as checking license of monitored clusters - * - * @param {String} type License type if a valid license. {@code null} if license was deleted. - * @param {Boolean} active Indicating that the overall license is active - * @param {String} clusterSource 'monitoring' or 'production' - * @param {Boolean} watcher {@code true} if Watcher is provided (or if its availability should not be checked) - */ -export function checkLicense(type, active, clusterSource, watcher = true) { - // return object, set up with safe defaults - const licenseInfo = { - clusterAlerts: { enabled: false }, - }; - - // Disabled because there is no license - if (!type) { - return Object.assign(licenseInfo, { - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.checkLicense.licenseNotDeterminedDescription', - { - defaultMessage: `Cluster Alerts are not displayed because the [{clusterSource}] cluster's license could not be determined.`, - values: { - clusterSource, - }, - } - ), - }); - } - - // Disabled because the license type is not valid (basic) - if (!includes(['trial', 'standard', 'gold', 'platinum', 'enterprise'], type)) { - return Object.assign(licenseInfo, { - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.checkLicense.licenseIsBasicDescription', - { - defaultMessage: `Cluster Alerts are not displayed if Watcher is disabled or the [{clusterSource}] cluster's current license is Basic.`, - values: { - clusterSource, - }, - } - ), - }); - } - - // Disabled because the license is inactive - if (!active) { - return Object.assign(licenseInfo, { - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.checkLicense.licenseNotActiveDescription', - { - defaultMessage: `Cluster Alerts are not displayed because the [{clusterSource}] cluster's current license [{type}] is not active.`, - values: { - clusterSource, - type, - }, - } - ), - }); - } - - // Disabled because Watcher is not enabled (it may or may not be available) - if (!watcher) { - return Object.assign(licenseInfo, { - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.checkLicense.watcherIsDisabledDescription', - { - defaultMessage: 'Cluster Alerts are not enabled because Watcher is disabled.', - } - ), - }); - } - - return Object.assign(licenseInfo, { clusterAlerts: { enabled: true } }); -} - -/** - * Function to "generate" license check results for {@code xpackInfo}. - * - * @param {Object} xpackInfo license information for the _Monitoring_ cluster - * @param {Function} _checkLicense Method exposed for easier unit testing - * @returns {Object} Response from {@code checker} - */ -export function checkLicenseGenerator(xpackInfo, _checkLicense = checkLicense) { - let type; - let active = false; - let watcher = false; - - if (xpackInfo && xpackInfo.license) { - const watcherFeature = xpackInfo.feature('watcher'); - - if (watcherFeature) { - watcher = watcherFeature.isEnabled(); - } - - type = xpackInfo.license.getType(); - active = xpackInfo.license.isActive(); - } - - return _checkLicense(type, active, 'monitoring', watcher); -} diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js b/x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js deleted file mode 100644 index 2217d27dd0c00..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js +++ /dev/null @@ -1,149 +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 { checkLicense, checkLicenseGenerator } from './check_license'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -describe('Monitoring Check License', () => { - describe('License undeterminable', () => { - it('null active license - results false with a message', () => { - const result = checkLicense(null, true, 'test-cluster-abc'); - expect(result).to.eql({ - clusterAlerts: { enabled: false }, - message: `Cluster Alerts are not displayed because the [test-cluster-abc] cluster's license could not be determined.`, - }); - }); - }); - - describe('Inactive license', () => { - it('platinum inactive license - results false with a message', () => { - const result = checkLicense('platinum', false, 'test-cluster-def'); - expect(result).to.eql({ - clusterAlerts: { enabled: false }, - message: `Cluster Alerts are not displayed because the [test-cluster-def] cluster's current license [platinum] is not active.`, - }); - }); - }); - - describe('Active license', () => { - describe('Unsupported license types', () => { - it('basic active license - results false with a message', () => { - const result = checkLicense('basic', true, 'test-cluster-ghi'); - expect(result).to.eql({ - clusterAlerts: { enabled: false }, - message: `Cluster Alerts are not displayed if Watcher is disabled or the [test-cluster-ghi] cluster's current license is Basic.`, - }); - }); - }); - - describe('Supported license types', () => { - it('standard active license - results true with no message', () => { - const result = checkLicense('standard', true, 'test-cluster-jkl'); - expect(result).to.eql({ - clusterAlerts: { enabled: true }, - }); - }); - - it('gold active license - results true with no message', () => { - const result = checkLicense('gold', true, 'test-cluster-mno'); - expect(result).to.eql({ - clusterAlerts: { enabled: true }, - }); - }); - - it('platinum active license - results true with no message', () => { - const result = checkLicense('platinum', true, 'test-cluster-pqr'); - expect(result).to.eql({ - clusterAlerts: { enabled: true }, - }); - }); - - it('enterprise active license - results true with no message', () => { - const result = checkLicense('enterprise', true, 'test-cluster-pqr'); - expect(result).to.eql({ - clusterAlerts: { enabled: true }, - }); - }); - - describe('Watcher is not enabled', () => { - it('platinum active license - watcher disabled - results false with message', () => { - const result = checkLicense('platinum', true, 'test-cluster-pqr', false); - expect(result).to.eql({ - clusterAlerts: { enabled: false }, - message: 'Cluster Alerts are not enabled because Watcher is disabled.', - }); - }); - }); - }); - }); - - describe('XPackInfo checkLicenseGenerator', () => { - it('with deleted license', () => { - const expected = 123; - const checker = sinon.stub().returns(expected); - const result = checkLicenseGenerator(null, checker); - - expect(result).to.be(expected); - expect(checker.withArgs(undefined, false, 'monitoring', false).called).to.be(true); - }); - - it('license without watcher', () => { - const expected = 123; - const xpackInfo = { - license: { - getType: () => 'fake-type', - isActive: () => true, - }, - feature: () => null, - }; - const checker = sinon.stub().returns(expected); - const result = checkLicenseGenerator(xpackInfo, checker); - - expect(result).to.be(expected); - expect(checker.withArgs('fake-type', true, 'monitoring', false).called).to.be(true); - }); - - it('mock license with watcher', () => { - const expected = 123; - const feature = sinon - .stub() - .withArgs('watcher') - .returns({ isEnabled: () => true }); - const xpackInfo = { - license: { - getType: () => 'another-type', - isActive: () => true, - }, - feature, - }; - const checker = sinon.stub().returns(expected); - const result = checkLicenseGenerator(xpackInfo, checker); - - expect(result).to.be(expected); - expect(feature.withArgs('watcher').calledOnce).to.be(true); - expect(checker.withArgs('another-type', true, 'monitoring', true).called).to.be(true); - }); - - it('platinum license with watcher', () => { - const xpackInfo = { - license: { - getType: () => 'platinum', - isActive: () => true, - }, - feature: () => { - return { - isEnabled: () => true, - }; - }, - }; - const result = checkLicenseGenerator(xpackInfo); - - expect(result).to.eql({ clusterAlerts: { enabled: true } }); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.js b/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.js deleted file mode 100644 index e93db4ea96095..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; - -/** - * Determine if an API for Cluster Alerts should respond based on the license and configuration of the monitoring cluster. - * - * Note: This does not guarantee that any production cluster has a valid license; only that Cluster Alerts in general can be used! - * - * @param {Object} server Server object containing config and plugins - * @return {Boolean} {@code true} to indicate that cluster alerts can be used. - */ -export async function verifyMonitoringLicense(server) { - const config = server.config(); - - // if cluster alerts are enabled, then ensure that we can use it according to the license - if (config.get('monitoring.cluster_alerts.enabled')) { - const xpackInfo = get(server.plugins.monitoring, 'info'); - if (xpackInfo) { - const licenseService = await xpackInfo.getLicenseService(); - const watcherFeature = licenseService.getWatcherFeature(); - return { - enabled: watcherFeature.isEnabled, - message: licenseService.getMessage(), - }; - } - - return { - enabled: false, - message: i18n.translate('xpack.monitoring.clusterAlerts.notDeterminedLicenseDescription', { - defaultMessage: 'Status of Cluster Alerts feature could not be determined.', - }), - }; - } - - return { - enabled: false, - message: i18n.translate('xpack.monitoring.clusterAlerts.disabledLicenseDescription', { - defaultMessage: 'Cluster Alerts feature is disabled.', - }), - }; -} diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js b/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js deleted file mode 100644 index 6add3131bed96..0000000000000 --- a/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js +++ /dev/null @@ -1,88 +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 { verifyMonitoringLicense } from './verify_monitoring_license'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// TODO: tests were not running and are not up to date. -describe.skip('Monitoring Verify License', () => { - describe('Disabled by Configuration', () => { - const get = sinon.stub().withArgs('xpack.monitoring.cluster_alerts.enabled').returns(false); - const server = { config: sinon.stub().returns({ get }) }; - - it('verifyMonitoringLicense returns false without checking the license', () => { - const verification = verifyMonitoringLicense(server); - - expect(verification.enabled).to.be(false); - expect(verification.message).to.be('Cluster Alerts feature is disabled.'); - - expect(get.withArgs('xpack.monitoring.cluster_alerts.enabled').calledOnce).to.be(true); - }); - }); - - describe('Enabled by Configuration', () => { - it('verifyMonitoringLicense returns false if enabled by configuration, but not by license', () => { - const get = sinon.stub().withArgs('xpack.monitoring.cluster_alerts.enabled').returns(true); - const server = { - config: sinon.stub().returns({ get }), - plugins: { monitoring: { info: {} } }, - }; - const getLicenseCheckResults = sinon - .stub() - .returns({ clusterAlerts: { enabled: false }, message: 'failed!!' }); - const feature = sinon.stub().withArgs('monitoring').returns({ getLicenseCheckResults }); - - server.plugins.monitoring.info = { feature }; - - const verification = verifyMonitoringLicense(server); - - expect(verification.enabled).to.be(false); - expect(verification.message).to.be('failed!!'); - - expect(get.withArgs('xpack.monitoring.cluster_alerts.enabled').calledOnce).to.be(true); - expect(feature.withArgs('monitoring').calledOnce).to.be(true); - expect(getLicenseCheckResults.calledOnce).to.be(true); - }); - - it('verifyMonitoringLicense returns true if enabled by configuration and by license', () => { - const get = sinon.stub().withArgs('xpack.monitoring.cluster_alerts.enabled').returns(true); - const server = { - config: sinon.stub().returns({ get }), - plugins: { monitoring: { info: {} } }, - }; - const getLicenseCheckResults = sinon.stub().returns({ clusterAlerts: { enabled: true } }); - const feature = sinon.stub().withArgs('monitoring').returns({ getLicenseCheckResults }); - - server.plugins.monitoring.info = { feature }; - - const verification = verifyMonitoringLicense(server); - - expect(verification.enabled).to.be(true); - expect(verification.message).to.be.undefined; - - expect(get.withArgs('xpack.monitoring.cluster_alerts.enabled').calledOnce).to.be(true); - expect(feature.withArgs('monitoring').calledOnce).to.be(true); - expect(getLicenseCheckResults.calledOnce).to.be(true); - }); - }); - - it('Monitoring feature info cannot be determined', () => { - const get = sinon.stub().withArgs('xpack.monitoring.cluster_alerts.enabled').returns(true); - const server = { - config: sinon.stub().returns({ get }), - plugins: { monitoring: undefined }, // simulate race condition - }; - - const verification = verifyMonitoringLicense(server); - - expect(verification.enabled).to.be(false); - expect(verification.message).to.be('Status of Cluster Alerts feature could not be determined.'); - - expect(get.withArgs('xpack.monitoring.cluster_alerts.enabled').calledOnce).to.be(true); - }); -}); diff --git a/x-pack/plugins/monitoring/server/deprecations.test.js b/x-pack/plugins/monitoring/server/deprecations.test.js index 156fc76b6e076..d7e1a2340d295 100644 --- a/x-pack/plugins/monitoring/server/deprecations.test.js +++ b/x-pack/plugins/monitoring/server/deprecations.test.js @@ -36,25 +36,9 @@ describe('monitoring plugin deprecations', function () { expect(log).not.toHaveBeenCalled(); }); - it(`shouldn't log when cluster alerts are disabled`, function () { - const settings = { - cluster_alerts: { - enabled: false, - email_notifications: { - enabled: true, - }, - }, - }; - - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).not.toHaveBeenCalled(); - }); - it(`shouldn't log when email_address is specified`, function () { const settings = { cluster_alerts: { - enabled: true, email_notifications: { enabled: true, email_address: 'foo@bar.com', @@ -70,7 +54,6 @@ describe('monitoring plugin deprecations', function () { it(`should log when email_address is missing, but alerts/notifications are both enabled`, function () { const settings = { cluster_alerts: { - enabled: true, email_notifications: { enabled: true, }, diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index 47a01385c6308..a276cfcee0d35 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -45,9 +45,7 @@ export const deprecations = ({ ), renameFromRoot('xpack.monitoring', 'monitoring'), (config, fromPath, logger) => { - const clusterAlertsEnabled = get(config, 'cluster_alerts.enabled'); - const emailNotificationsEnabled = - clusterAlertsEnabled && get(config, 'cluster_alerts.email_notifications.enabled'); + const emailNotificationsEnabled = get(config, 'cluster_alerts.email_notifications.enabled'); if (emailNotificationsEnabled && !get(config, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { logger( `Config key [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] will be required for email notifications to work in 7.0."` diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index b282cf94ade28..5143613a25b9c 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -15,8 +15,6 @@ import { getKibanasForClusters } from '../kibana'; import { getLogstashForClusters } from '../logstash'; import { getLogstashPipelineIds } from '../logstash/get_pipeline_ids'; import { getBeatsForClusters } from '../beats'; -import { verifyMonitoringLicense } from '../../cluster_alerts/verify_monitoring_license'; -import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; import { getClustersSummary } from './get_clusters_summary'; import { STANDALONE_CLUSTER_CLUSTER_UUID, @@ -127,20 +125,7 @@ export async function getClustersFromRequest( clusters.map((cluster) => cluster.cluster_uuid) ); - const verification = await verifyMonitoringLicense(req.server); for (const cluster of clusters) { - if (!verification.enabled) { - // return metadata detailing that alerts is disabled because of the monitoring cluster license - cluster.alerts = { - alertsMeta: { - enabled: verification.enabled, - message: verification.message, // NOTE: this is only defined when the alert feature is disabled - }, - list: {}, - }; - continue; - } - if (!alertsClient) { cluster.alerts = { list: {}, @@ -148,17 +133,7 @@ export async function getClustersFromRequest( enabled: false, }, }; - continue; - } - - // check the license type of the production cluster for alerts feature support - const license = cluster.license || {}; - const prodLicenseInfo = checkLicenseForAlerts( - license.type, - license.status === 'active', - 'production' - ); - if (prodLicenseInfo.clusterAlerts.enabled) { + } else { try { cluster.alerts = { list: Object.keys(alertStatus).reduce((accum, alertName) => { @@ -190,29 +165,7 @@ export async function getClustersFromRequest( }, }; } - continue; } - - cluster.alerts = { - list: {}, - alertsMeta: { - enabled: false, - }, - clusterMeta: { - enabled: false, - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', - { - defaultMessage: - 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', - values: { - clusterName: cluster.cluster_name, - licenseType: `${license.type}`, - }, - } - ), - }, - }; } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b7a25ff62661..290ad19718efc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15586,15 +15586,6 @@ "xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel": "永続キューあり", "xpack.monitoring.cluster.overview.pageTitle": "クラスターの概要", "xpack.monitoring.cluster.overviewTitle": "概要", - "xpack.monitoring.clusterAlerts.checkLicense.licenseIsBasicDescription": "Watcher が無効になっているか、[{clusterSource}] クラスターの現在のライセンスがベーシックの場合、クラスターアラートは表示されません。", - "xpack.monitoring.clusterAlerts.checkLicense.licenseNotActiveDescription": "[{clusterSource}] クラスターの現在のライセンス [{type}] がアクティブでないため、クラスターアラートは表示されません。", - "xpack.monitoring.clusterAlerts.checkLicense.licenseNotDeterminedDescription": "[{clusterSource}] クラスターのライセンスが確認できなかったため、クラスターアラートは表示されません。", - "xpack.monitoring.clusterAlerts.checkLicense.watcherIsDisabledDescription": "Watcher が無効なため、クラスターアラートを利用できません。", - "xpack.monitoring.clusterAlerts.clusterNeedsTSLEnabledDescription": "セキュリティが有効な場合、ゴールドまたはプラチナライセンスの適用に TLS の構成が必要です。", - "xpack.monitoring.clusterAlerts.disabledLicenseDescription": "クラスターアラート機能は無効になっています。", - "xpack.monitoring.clusterAlerts.notDeterminedLicenseDescription": "クラスターアラート機能のステータスが確認できませんでした。", - "xpack.monitoring.clusterAlerts.seeDocumentationDescription": "詳細はドキュメンテーションをご覧ください。", - "xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription": "クラスター [{clusterName}] ライセンスタイプ [{licenseType}] はクラスターアラートをサポートしていません", "xpack.monitoring.clusterAlertsNavigation.clusterAlertsLinkText": "クラスターアラート", "xpack.monitoring.clustersNavigation.clustersLinkText": "クラスター", "xpack.monitoring.clusterStats.uuidNotFoundErrorMessage": "選択された時間範囲にクラスターが見つかりませんでした。UUID:{clusterUuid}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7bb27ee6626b8..c982931f91e13 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15810,15 +15810,6 @@ "xpack.monitoring.cluster.overview.logstashPanel.withPersistentQueuesLabel": "持久性队列", "xpack.monitoring.cluster.overview.pageTitle": "集群概览", "xpack.monitoring.cluster.overviewTitle": "概览", - "xpack.monitoring.clusterAlerts.checkLicense.licenseIsBasicDescription": "如果禁用了 Watcher 或 [{clusterSource}] 集群的当前许可为基本级许可,则“集群告警”将不会显示。", - "xpack.monitoring.clusterAlerts.checkLicense.licenseNotActiveDescription": "因为 [{clusterSource}] 集群的当前许可 [{type}] 未处于活动状态,所以“集群告警”将不会显示。", - "xpack.monitoring.clusterAlerts.checkLicense.licenseNotDeterminedDescription": "因为无法确定 [{clusterSource}] 集群的许可,所以“集群告警”将不会显示。", - "xpack.monitoring.clusterAlerts.checkLicense.watcherIsDisabledDescription": "因为禁用了 Watcher,所以“集群告警”未启用。", - "xpack.monitoring.clusterAlerts.clusterNeedsTSLEnabledDescription": "启用安全性时,需要配置 TLS,才能应用黄金或白金许可。", - "xpack.monitoring.clusterAlerts.disabledLicenseDescription": "“集群告警”功能已禁用。", - "xpack.monitoring.clusterAlerts.notDeterminedLicenseDescription": "无法确定“集群告警”功能的状态。", - "xpack.monitoring.clusterAlerts.seeDocumentationDescription": "有关详情,请参阅文档。", - "xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription": "集群 [{clusterName}] 许可类型 [{licenseType}] 不支持“集群告警”", "xpack.monitoring.clusterAlertsNavigation.clusterAlertsLinkText": "集群告警", "xpack.monitoring.clustersNavigation.clustersLinkText": "集群", "xpack.monitoring.clusterStats.uuidNotFoundErrorMessage": "在选定时间范围内找不到该集群。UUID:{clusterUuid}", diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json index 48861c88e86ad..027dc898cacb5 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json @@ -105,11 +105,7 @@ }, "alerts": { "alertsMeta": { - "enabled": false - }, - "clusterMeta": { - "enabled": false, - "message": "Cluster [clustertwo] license type [basic] does not support Cluster Alerts" + "enabled": true }, "list": {} }, diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json index 602e6d5c2be4f..c5006e8de824c 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json @@ -105,11 +105,7 @@ }, "alerts": { "alertsMeta": { - "enabled": false - }, - "clusterMeta": { - "enabled": false, - "message": "Cluster [monitoring] license type [basic] does not support Cluster Alerts" + "enabled": true }, "list": {} }, @@ -176,11 +172,7 @@ }, "alerts": { "alertsMeta": { - "enabled": false - }, - "clusterMeta": { - "enabled": false, - "message": "Cluster [] license type [undefined] does not support Cluster Alerts" + "enabled": true }, "list": {} }, diff --git a/x-pack/test/functional/apps/monitoring/cluster/list.js b/x-pack/test/functional/apps/monitoring/cluster/list.js index bc08ce25ce90f..e4f93042f0bf2 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/list.js +++ b/x-pack/test/functional/apps/monitoring/cluster/list.js @@ -109,7 +109,7 @@ export default function ({ getService, getPageObjects }) { it('primary basic cluster shows cluster metrics', async () => { expect(await clusterList.getClusterName(SUPPORTED_CLUSTER_UUID)).to.be('production'); - expect(await clusterList.getClusterStatus(SUPPORTED_CLUSTER_UUID)).to.be('N/A'); + expect(await clusterList.getClusterStatus(SUPPORTED_CLUSTER_UUID)).to.be('Clear'); expect(await clusterList.getClusterNodesCount(SUPPORTED_CLUSTER_UUID)).to.be('2'); expect(await clusterList.getClusterIndicesCount(SUPPORTED_CLUSTER_UUID)).to.be('4'); expect(await clusterList.getClusterDataSize(SUPPORTED_CLUSTER_UUID)).to.be('1.6 MB'); From 00c53c56b82e5f9a331c086ac50ba3a3ab4b458e Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 25 Mar 2021 09:15:59 -0400 Subject: [PATCH 56/88] [Fleet] Replace INTERNAL_POLICY_REASSIGN by POLICY_REASSIGN (#94116) --- .../fleet/common/types/models/agent.ts | 3 +-- .../agents/checkin/state_new_actions.ts | 21 +------------------ .../fleet/server/services/agents/reassign.ts | 4 ++-- .../fleet/server/types/models/agent.ts | 2 +- 4 files changed, 5 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 35b123b2c64ea..0629a67f0d8d3 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -36,8 +36,7 @@ export type AgentActionType = | 'UNENROLL' | 'UPGRADE' | 'SETTINGS' - // INTERNAL* actions are mean to interupt long polling calls these actions will not be distributed to the agent - | 'INTERNAL_POLICY_REASSIGN'; + | 'POLICY_REASSIGN'; export interface NewAgentAction { type: AgentActionType; diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 7dc19f63a5adb..8810dd6ff1263 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -39,7 +39,7 @@ import { getAgentPolicyActionByIds, } from '../actions'; import { appContextService } from '../../app_context'; -import { getAgentById, updateAgent } from '../crud'; +import { updateAgent } from '../crud'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; @@ -262,25 +262,6 @@ export function agentCheckinStateNewActionsFactory() { return EMPTY; } - const hasConfigReassign = newActions.some( - (action) => action.type === 'INTERNAL_POLICY_REASSIGN' - ); - if (hasConfigReassign) { - return from(getAgentById(esClient, agent.id)).pipe( - concatMap((refreshedAgent) => { - if (!refreshedAgent.policy_id) { - throw new Error('Agent does not have a policy assigned'); - } - const newAgentPolicy$ = getOrCreateAgentPolicyObservable(refreshedAgent.policy_id); - return newAgentPolicy$; - }), - rateLimiter(), - concatMap((policyAction) => - createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) - ) - ); - } - return of(newActions); }), filter((data) => data !== undefined), diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 5574c42ced053..81b00663d7a8a 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -44,7 +44,7 @@ export async function reassignAgent( await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: new Date().toISOString(), - type: 'INTERNAL_POLICY_REASSIGN', + type: 'POLICY_REASSIGN', }); } @@ -164,7 +164,7 @@ export async function reassignAgents( agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, - type: 'INTERNAL_POLICY_REASSIGN', + type: 'POLICY_REASSIGN', })) ); diff --git a/x-pack/plugins/fleet/server/types/models/agent.ts b/x-pack/plugins/fleet/server/types/models/agent.ts index b0b28fdb5b2c8..192bb83a88718 100644 --- a/x-pack/plugins/fleet/server/types/models/agent.ts +++ b/x-pack/plugins/fleet/server/types/models/agent.ts @@ -70,7 +70,7 @@ export const NewAgentActionSchema = schema.oneOf([ schema.literal('POLICY_CHANGE'), schema.literal('UNENROLL'), schema.literal('UPGRADE'), - schema.literal('INTERNAL_POLICY_REASSIGN'), + schema.literal('POLICY_REASSIGN'), ]), data: schema.maybe(schema.any()), ack_data: schema.maybe(schema.any()), From e894ee973fe8ef16580ecf4a6952d9829927689e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Thu, 25 Mar 2021 14:29:51 +0100 Subject: [PATCH 57/88] [Observability] Change icon ref (#95367) --- x-pack/plugins/observability/public/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 81c174932914b..5978c28b4e939 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -39,7 +39,7 @@ export class Plugin implements PluginClass) => { // Load application bundle const { renderApp } = await import('./application'); From e0534e42ae4bcf89a9c4c4fde1294d58db6e4e62 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 25 Mar 2021 15:27:30 +0100 Subject: [PATCH 58/88] Expose xy chart types component (#95275) --- x-pack/plugins/lens/public/mocks.ts | 1 + x-pack/plugins/lens/public/plugin.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/mocks.ts b/x-pack/plugins/lens/public/mocks.ts index 10d3be1d1b57d..fd1e38db242a8 100644 --- a/x-pack/plugins/lens/public/mocks.ts +++ b/x-pack/plugins/lens/public/mocks.ts @@ -14,6 +14,7 @@ const createStartContract = (): Start => { EmbeddableComponent: jest.fn(() => null), canUseEditor: jest.fn(() => true), navigateToPrefilledEditor: jest.fn(), + getXyVisTypes: jest.fn(), }; return startContract; }; 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; + }, }; } From a8b04d7c54c85f81e1a6723929d55a39fad1d04b Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 25 Mar 2021 16:21:10 +0100 Subject: [PATCH 59/88] [ML] Extract job selection resolver (#95394) --- .../anomaly_swimlane_setup_flyout.tsx | 104 ++++-------------- .../common/resolve_job_selection.tsx | 85 ++++++++++++++ 2 files changed, 109 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index f12eb4af4d1e1..1bacf9679cdaa 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -7,105 +7,49 @@ import React from 'react'; import { CoreStart } from 'kibana/public'; -import moment from 'moment'; -import { takeUntil } from 'rxjs/operators'; -import { from } from 'rxjs'; import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants'; -import { - KibanaContextProvider, - toMountPoint, -} from '../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer'; -import { JobSelectorFlyoutContent } from '../../application/components/job_selector/job_selector_flyout'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; -import { getInitialGroupsMap } from '../../application/components/job_selector/job_selector'; import { getDefaultPanelTitle } from './anomaly_swimlane_embeddable'; -import { getMlGlobalServices } from '../../application/app'; import { HttpService } from '../../application/services/http_service'; -import { DashboardConstants } from '../../../../../../src/plugins/dashboard/public'; import { AnomalySwimlaneEmbeddableInput } from '..'; +import { resolveJobSelection } from '../common/resolve_job_selection'; export async function resolveAnomalySwimlaneUserInput( coreStart: CoreStart, input?: AnomalySwimlaneEmbeddableInput ): Promise> { - const { - http, - uiSettings, - overlays, - application: { currentAppId$ }, - } = coreStart; + const { http, overlays } = coreStart; const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); return new Promise(async (resolve, reject) => { - const maps = { - groupsMap: getInitialGroupsMap([]), - jobsMap: {}, - }; + const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); - const tzConfig = uiSettings.get('dateFormat:tz'); - const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + const title = input?.title ?? getDefaultPanelTitle(jobIds); - const selectedIds = input?.jobIds; + const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - const flyoutSession = coreStart.overlays.openFlyout( - toMountPoint( - - { - flyoutSession.close(); - reject(); - }} - onSelectionConfirmed={async ({ jobIds, groups }) => { - const title = input?.title ?? getDefaultPanelTitle(jobIds); - - const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - - const influencers = anomalyDetectorService.extractInfluencers(jobs); - influencers.push(VIEW_BY_JOB_LABEL); + const influencers = anomalyDetectorService.extractInfluencers(jobs); + influencers.push(VIEW_BY_JOB_LABEL); - await flyoutSession.close(); - - const modalSession = overlays.openModal( - toMountPoint( - { - modalSession.close(); - resolve({ jobIds, title: panelTitle, swimlaneType, viewBy }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> - ) - ); - }} - maps={maps} - /> - - ), - { - 'data-test-subj': 'mlFlyoutJobSelector', - ownFocus: true, - closeButtonAriaLabel: 'jobSelectorFlyout', - } + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve({ jobIds, title: panelTitle, swimlaneType, viewBy }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + ) ); - - // Close the flyout when user navigates out of the dashboard plugin - currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => { - if (appId !== DashboardConstants.DASHBOARDS_ID) { - flyoutSession.close(); - } - }); }); } diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx new file mode 100644 index 0000000000000..8499ab624f790 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from 'kibana/public'; +import moment from 'moment'; +import { takeUntil } from 'rxjs/operators'; +import { from } from 'rxjs'; +import React from 'react'; +import { getInitialGroupsMap } from '../../application/components/job_selector/job_selector'; +import { + KibanaContextProvider, + toMountPoint, +} from '../../../../../../src/plugins/kibana_react/public'; +import { getMlGlobalServices } from '../../application/app'; +import { JobSelectorFlyoutContent } from '../../application/components/job_selector/job_selector_flyout'; +import { DashboardConstants } from '../../../../../../src/plugins/dashboard/public'; +import { JobId } from '../../../common/types/anomaly_detection_jobs'; + +/** + * Handles Anomaly detection jobs selection by a user. + * Intended to use independently of the ML app context, + * for instance on the dashboard for embeddables initialization. + * + * @param coreStart + * @param selectedJobIds + */ +export async function resolveJobSelection( + coreStart: CoreStart, + selectedJobIds?: JobId[] +): Promise<{ jobIds: string[]; groups: Array<{ groupId: string; jobIds: string[] }> }> { + const { + http, + uiSettings, + application: { currentAppId$ }, + } = coreStart; + + return new Promise(async (resolve, reject) => { + const maps = { + groupsMap: getInitialGroupsMap([]), + jobsMap: {}, + }; + + const tzConfig = uiSettings.get('dateFormat:tz'); + const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + + const flyoutSession = coreStart.overlays.openFlyout( + toMountPoint( + + { + flyoutSession.close(); + reject(); + }} + onSelectionConfirmed={async ({ jobIds, groups }) => { + await flyoutSession.close(); + resolve({ jobIds, groups }); + }} + maps={maps} + /> + + ), + { + 'data-test-subj': 'mlFlyoutJobSelector', + ownFocus: true, + closeButtonAriaLabel: 'jobSelectorFlyout', + } + ); + + // Close the flyout when user navigates out of the dashboard plugin + currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => { + if (appId !== DashboardConstants.DASHBOARDS_ID) { + flyoutSession.close(); + } + }); + }); +} From 9a64354db217afec29012d2447278093cd102111 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 25 Mar 2021 16:23:25 +0100 Subject: [PATCH 60/88] do not allow creating Question issue (#95396) --- .github/ISSUE_TEMPLATE/Question.md | 15 --------------- .github/ISSUE_TEMPLATE/config.yml | 4 ++++ 2 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/Question.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml 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..46627ac3effab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Question + url: https://discuss.elastic.co/c/kibana + about: Ask (and answer) questions here. From 6d6ef0092a4b0494c651441b407542d9231fb0b3 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 25 Mar 2021 16:56:51 +0100 Subject: [PATCH 61/88] Revert "do not allow creating Question issue (#95396)" (#95427) This reverts commit 9a64354db217afec29012d2447278093cd102111. --- .github/ISSUE_TEMPLATE/Question.md | 15 +++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 ---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/Question.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md new file mode 100644 index 0000000000000..38fcb7af30b47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.md @@ -0,0 +1,15 @@ +--- +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 deleted file mode 100644 index 46627ac3effab..0000000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,4 +0,0 @@ -contact_links: - - name: Question - url: https://discuss.elastic.co/c/kibana - about: Ask (and answer) questions here. From 373a108cfef19ef688ca8b705f091b7074438df2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Mar 2021 10:06:56 -0600 Subject: [PATCH 62/88] [Maps] upgrade server to use new elasticsearch-js client (#95314) * [Maps] upgrade server to use new elasticsearch-js client * update functional test expect --- x-pack/plugins/maps/server/routes.js | 11 ++++------- x-pack/test/api_integration/apis/maps/index.js | 1 + .../test/api_integration/apis/maps/index_settings.js | 12 +++++++++++- x-pack/test/functional/apps/maps/mvt_scaling.js | 2 +- .../functional/es_archives/maps/data/mappings.json | 4 +++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index d4c0652fa535c..f18bb29ed453d 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -575,13 +575,10 @@ export async function initRoutes( } try { - const resp = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.getSettings', - { - index: query.indexPatternTitle, - } - ); - const indexPatternSettings = getIndexPatternSettings(resp); + const resp = await context.core.elasticsearch.client.asCurrentUser.indices.getSettings({ + index: query.indexPatternTitle, + }); + const indexPatternSettings = getIndexPatternSettings(resp.body); return response.ok({ body: indexPatternSettings, }); diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index 898c3d56ecc2f..afbe201a18b0e 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -11,6 +11,7 @@ export default function ({ loadTestFile, getService }) { describe('Maps endpoints', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('maps/data'); }); describe('', () => { diff --git a/x-pack/test/api_integration/apis/maps/index_settings.js b/x-pack/test/api_integration/apis/maps/index_settings.js index 748128026f734..375b2e7eb21a0 100644 --- a/x-pack/test/api_integration/apis/maps/index_settings.js +++ b/x-pack/test/api_integration/apis/maps/index_settings.js @@ -11,7 +11,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('index settings', () => { - it('should return index settings', async () => { + it('should return default index settings when max_result_window and max_inner_result_window are not set', async () => { const resp = await supertest .get(`/api/maps/indexSettings?indexPatternTitle=logstash*`) .set('kbn-xsrf', 'kibana') @@ -20,5 +20,15 @@ export default function ({ getService }) { expect(resp.body.maxResultWindow).to.be(10000); expect(resp.body.maxInnerResultWindow).to.be(100); }); + + it('should return index settings', async () => { + const resp = await supertest + .get(`/api/maps/indexSettings?indexPatternTitle=geo_shape*`) + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(resp.body.maxResultWindow).to.be(10001); + expect(resp.body.maxInnerResultWindow).to.be(101); + }); }); } diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js index ba3cdf33ae24e..83467ed726581 100644 --- a/x-pack/test/functional/apps/maps/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect(mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0]).to.equal( - '/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape' + '/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape' ); //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) diff --git a/x-pack/test/functional/es_archives/maps/data/mappings.json b/x-pack/test/functional/es_archives/maps/data/mappings.json index 7e642ca49f3ae..4ad5d6c33295b 100644 --- a/x-pack/test/functional/es_archives/maps/data/mappings.json +++ b/x-pack/test/functional/es_archives/maps/data/mappings.json @@ -18,7 +18,9 @@ "settings": { "index": { "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "max_result_window": "10001", + "max_inner_result_window": "101" } } } From c042968b33a56d8cfc7e7d1666d942d530168343 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 25 Mar 2021 16:08:34 +0000 Subject: [PATCH 63/88] chore(NA): upgrade bazel rules nodejs to v3.2.3 (#95413) --- WORKSPACE.bazel | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 # From 50d7cea8122e3efbafd0caf171663173d3547525 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 25 Mar 2021 11:12:53 -0500 Subject: [PATCH 64/88] [Workplace Search] Add UI logic for GitHub Configure Step (#95254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix typo This was mis-copied from ent-search * No longer store preContentSourceId in query param In ent-search, we had the Rails server redirect with this param. Now, it is contained in the server response as JSON and is persisted in the logic file * Pass query params to SourceAdded component The entire state is stored in query params now and must be passed when doing a manual redirect * Redirect to config view if config needed * Don’t redirect if the config has already been completed This was really tricky and could use a refactor in the future, perhaps. The issue is that the persisted query params will contain the `preContentSourceId` even after the config has been completed. This caused the UI to attempt to navigate back to the config screen after it had been completed. This sets a prop once that has been completed and bypasses the redirect. * Use correct key to determine if config needed * Update tests --- .../components/add_source/add_source.tsx | 8 +++- .../add_source/add_source_logic.test.ts | 46 ++++++++++++++++-- .../components/add_source/add_source_logic.ts | 47 +++++++++++++++---- .../components/add_source/configure_oauth.tsx | 13 +---- .../components/add_source/constants.ts | 2 +- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 64431a800487f..30f5009ac0b3c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -6,6 +6,9 @@ */ import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { Location } from 'history'; import { useActions, useValues } from 'kea'; @@ -31,6 +34,7 @@ import { SaveCustom } from './save_custom'; import './add_source.scss'; export const AddSource: React.FC = (props) => { + const { search } = useLocation() as Location; const { initializeAddSource, setAddSourceStep, @@ -83,9 +87,9 @@ export const AddSource: React.FC = (props) => { const saveCustomSuccess = () => setAddSourceStep(AddSourceSteps.SaveCustomStep); const goToSaveCustom = () => createContentSource(CUSTOM_SERVICE_TYPE, saveCustomSuccess); - const goToFormSourceCreated = (sourceName: string) => { + const goToFormSourceCreated = () => { KibanaLogic.values.navigateToUrl( - `${getSourcesPath(SOURCE_ADDED_PATH, isOrganization)}/?name=${sourceName}` + `${getSourcesPath(SOURCE_ADDED_PATH, isOrganization)}${search}` ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index d0ab40399fa59..6c60cd74a9c9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -20,7 +20,7 @@ jest.mock('../../../../app_logic', () => ({ })); import { AppLogic } from '../../../../app_logic'; -import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; +import { ADD_GITHUB_PATH, SOURCES_PATH, getSourcesPath } from '../../../../routes'; import { CustomSource } from '../../../../types'; import { SourcesLogic } from '../../sources_logic'; @@ -55,10 +55,12 @@ describe('AddSourceLogic', () => { sourceConfigData: {} as SourceConfigData, sourceConnectData: {} as SourceConnectData, newCustomSource: {} as CustomSource, + oauthConfigCompleted: false, currentServiceType: '', githubOrganizations: [], selectedGithubOrganizationsMap: {} as OrganizationsMap, selectedGithubOrganizations: [], + preContentSourceId: '', }; const sourceConnectData = { @@ -182,6 +184,12 @@ describe('AddSourceLogic', () => { expect(AddSourceLogic.values.selectedGithubOrganizationsMap).toEqual({ foo: true }); }); + it('setPreContentSourceId', () => { + AddSourceLogic.actions.setPreContentSourceId('123'); + + expect(AddSourceLogic.values.preContentSourceId).toEqual('123'); + }); + it('setButtonNotLoading', () => { AddSourceLogic.actions.setButtonNotLoading(); @@ -317,6 +325,34 @@ describe('AddSourceLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith(getSourcesPath(SOURCES_PATH, false)); }); + it('redirects to oauth config when preContentSourceId is present', async () => { + const preContentSourceId = 'id123'; + const setPreContentSourceIdSpy = jest.spyOn( + AddSourceLogic.actions, + 'setPreContentSourceId' + ); + + http.get.mockReturnValue( + Promise.resolve({ + ...response, + hasConfigureStep: true, + preContentSourceId, + }) + ); + AddSourceLogic.actions.saveSourceParams(queryString); + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/sources/create', { + query: { + ...params, + kibana_host: '', + }, + }); + + await nextTick(); + + expect(setPreContentSourceIdSpy).toHaveBeenCalledWith(preContentSourceId); + expect(navigateToUrl).toHaveBeenCalledWith(`${ADD_GITHUB_PATH}/configure${queryString}`); + }); + it('handles error', async () => { http.get.mockReturnValue(Promise.reject('this is an error')); @@ -440,13 +476,14 @@ describe('AddSourceLogic', () => { describe('getPreContentSourceConfigData', () => { it('calls API and sets values', async () => { + mount({ preContentSourceId: '123' }); const setPreContentSourceConfigDataSpy = jest.spyOn( AddSourceLogic.actions, 'setPreContentSourceConfigData' ); http.get.mockReturnValue(Promise.resolve(config)); - AddSourceLogic.actions.getPreContentSourceConfigData('123'); + AddSourceLogic.actions.getPreContentSourceConfigData(); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/pre_sources/123'); await nextTick(); @@ -456,7 +493,7 @@ describe('AddSourceLogic', () => { it('handles error', async () => { http.get.mockReturnValue(Promise.reject('this is an error')); - AddSourceLogic.actions.getPreContentSourceConfigData('123'); + AddSourceLogic.actions.getPreContentSourceConfigData(); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); @@ -616,7 +653,8 @@ describe('AddSourceLogic', () => { }); it('getPreContentSourceConfigData', () => { - AddSourceLogic.actions.getPreContentSourceConfigData('123'); + mount({ preContentSourceId: '123' }); + AddSourceLogic.actions.getPreContentSourceConfigData(); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/account/pre_sources/123'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index e1f554d87551d..ed63f82764f7e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -22,7 +22,7 @@ import { KibanaLogic } from '../../../../../shared/kibana'; import { parseQueryParams } from '../../../../../shared/query_params'; import { AppLogic } from '../../../../app_logic'; import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; -import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; +import { SOURCES_PATH, ADD_GITHUB_PATH, getSourcesPath } from '../../../../routes'; import { CustomSource } from '../../../../types'; import { staticSourceData } from '../../source_data'; import { SourcesLogic } from '../../sources_logic'; @@ -74,6 +74,7 @@ export interface AddSourceActions { setSourceIndexPermissionsValue(indexPermissionsValue: boolean): boolean; setCustomSourceData(data: CustomSource): CustomSource; setPreContentSourceConfigData(data: PreContentSourceResponse): PreContentSourceResponse; + setPreContentSourceId(preContentSourceId: string): string; setSelectedGithubOrganizations(option: string): string; resetSourceState(): void; createContentSource( @@ -92,7 +93,7 @@ export interface AddSourceActions { successCallback: (oauthUrl: string) => void ): { serviceType: string; successCallback(oauthUrl: string): void }; getSourceReConnectData(sourceId: string): { sourceId: string }; - getPreContentSourceConfigData(preContentSourceId: string): { preContentSourceId: string }; + getPreContentSourceConfigData(): void; setButtonNotLoading(): void; } @@ -144,6 +145,8 @@ interface AddSourceValues { githubOrganizations: string[]; selectedGithubOrganizationsMap: OrganizationsMap; selectedGithubOrganizations: string[]; + preContentSourceId: string; + oauthConfigCompleted: boolean; } interface PreContentSourceResponse { @@ -181,6 +184,7 @@ export const AddSourceLogic = kea indexPermissionsValue, setCustomSourceData: (data: CustomSource) => data, setPreContentSourceConfigData: (data: PreContentSourceResponse) => data, + setPreContentSourceId: (preContentSourceId: string) => preContentSourceId, setSelectedGithubOrganizations: (option: string) => option, getSourceConfigData: (serviceType: string) => ({ serviceType }), getSourceConnectData: (serviceType: string, successCallback: (oauthUrl: string) => string) => ({ @@ -188,7 +192,7 @@ export const AddSourceLogic = kea ({ sourceId }), - getPreContentSourceConfigData: (preContentSourceId: string) => ({ preContentSourceId }), + getPreContentSourceConfigData: () => true, saveSourceConfig: (isUpdating: boolean, successCallback?: () => void) => ({ isUpdating, successCallback, @@ -344,6 +348,20 @@ export const AddSourceLogic = kea ({}), }, ], + preContentSourceId: [ + '', + { + setPreContentSourceId: (_, preContentSourceId) => preContentSourceId, + setPreContentSourceConfigData: () => '', + resetSourceState: () => '', + }, + ], + oauthConfigCompleted: [ + false, + { + setPreContentSourceConfigData: () => true, + }, + ], }, selectors: ({ selectors }) => ({ selectedGithubOrganizations: [ @@ -407,8 +425,9 @@ export const AddSourceLogic = kea { + getPreContentSourceConfigData: async () => { const { isOrganization } = AppLogic.values; + const { preContentSourceId } = values; const route = isOrganization ? `/api/workplace_search/org/pre_sources/${preContentSourceId}` : `/api/workplace_search/account/pre_sources/${preContentSourceId}`; @@ -480,12 +499,24 @@ export const AddSourceLogic = kea = ({ name, onFormCreated, header }) => { - const { search } = useLocation() as Location; - - const { preContentSourceId } = (parseQueryParams(search) as unknown) as OauthQueryParams; const [formLoading, setFormLoading] = useState(false); const { @@ -58,7 +48,7 @@ export const ConfigureOauth: React.FC = ({ name, onFormCrea const checkboxOptions = githubOrganizations.map((item) => ({ id: item, label: item })); useEffect(() => { - getPreContentSourceConfigData(preContentSourceId); + getPreContentSourceConfigData(); }, []); const handleChange = (option: string) => setSelectedGithubOrganizations(option); @@ -101,6 +91,7 @@ export const ConfigureOauth: React.FC = ({ name, onFormCrea return ( <> {header} + {sectionLoading ? : configfieldsForm} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts index dd756a51fded3..712be15e7c046 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts @@ -176,7 +176,7 @@ export const CONFIG_CUSTOM_BUTTON = i18n.translate( export const CONFIG_OAUTH_LABEL = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.configOauth.label', { - defaultMessage: 'Complete connection', + defaultMessage: 'Select GitHub organizations to sync', } ); From b9ef084130bc8078118c096a51dd696f1b19c256 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 25 Mar 2021 09:19:39 -0700 Subject: [PATCH 65/88] [App Search] API Logs - set up basic view & routing (#95369) * Add basic API Logs view * Update engine router + nav link --- .../components/api_logs/api_logs.test.tsx | 32 +++++++++++++ .../components/api_logs/api_logs.tsx | 47 +++++++++++++++++++ .../app_search/components/api_logs/index.ts | 1 + .../components/engine/engine_nav.tsx | 3 +- .../components/engine/engine_router.test.tsx | 8 ++++ .../components/engine/engine_router.tsx | 10 +++- .../public/applications/app_search/routes.ts | 2 +- 7 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx new file mode 100644 index 0000000000000..da57fd466ffe1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiPageHeader } from '@elastic/eui'; + +import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention'; + +import { ApiLogs } from './'; + +describe('ApiLogs', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs'); + // TODO: Check for ApiLogsTable + NewApiEventsPrompt when those get added + + expect(wrapper.find(LogRetentionCallout).prop('type')).toEqual('api'); + expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx new file mode 100644 index 0000000000000..7e3fadb44fc7a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiPageHeader, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs'; + +import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; + +import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; + +interface Props { + engineBreadcrumb: BreadcrumbTrail; +} +export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { + return ( + <> + + + + + + + + + +

{RECENT_API_EVENTS}

+
+
+ + + + {/* TODO: NewApiEventsPrompt */} +
+ + {/* TODO: ApiLogsTable */} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index b67dee28f80d7..104ae03b89220 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -6,3 +6,4 @@ */ export { API_LOGS_TITLE } from './constants'; +export { ApiLogs } from './api_logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index f3a67c0d10389..2d7e3438d4c02 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -246,8 +246,7 @@ export const EngineNav: React.FC = () => { )} {canViewEngineApiLogs && ( {API_LOGS_TITLE} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index 7355ee148814c..27ef42e72764c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -17,6 +17,7 @@ import { shallow } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { AnalyticsRouter } from '../analytics'; +import { ApiLogs } from '../api_logs'; import { CurationsRouter } from '../curations'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; @@ -119,4 +120,11 @@ describe('EngineRouter', () => { expect(wrapper.find(ResultSettings)).toHaveLength(1); }); + + it('renders an API logs view', () => { + setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); + const wrapper = shallow(); + + expect(wrapper.find(ApiLogs)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 8eb50626fcb2b..88a24755070ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -31,9 +31,10 @@ import { ENGINE_CURATIONS_PATH, ENGINE_RESULT_SETTINGS_PATH, // ENGINE_SEARCH_UI_PATH, - // ENGINE_API_LOGS_PATH, + ENGINE_API_LOGS_PATH, } from '../../routes'; import { AnalyticsRouter } from '../analytics'; +import { ApiLogs } from '../api_logs'; import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { OVERVIEW_TITLE } from '../engine_overview'; @@ -58,7 +59,7 @@ export const EngineRouter: React.FC = () => { canManageEngineCurations, canManageEngineResultSettings, // canManageEngineSearchUi, - // canViewEngineApiLogs, + canViewEngineApiLogs, }, } = useValues(AppLogic); @@ -115,6 +116,11 @@ export const EngineRouter: React.FC = () => { )} + {canViewEngineApiLogs && ( + + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 8b4f0f70039d3..a04707ad48338 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -55,4 +55,4 @@ export const ENGINE_CURATIONS_NEW_PATH = `${ENGINE_CURATIONS_PATH}/new`; export const ENGINE_CURATION_PATH = `${ENGINE_CURATIONS_PATH}/:curationId`; export const ENGINE_SEARCH_UI_PATH = `${ENGINE_PATH}/reference_application/new`; -export const ENGINE_API_LOGS_PATH = `${ENGINE_PATH}/api-logs`; +export const ENGINE_API_LOGS_PATH = `${ENGINE_PATH}/api_logs`; From 07f32d03b30a160511f67a9e59b563cde4200fbb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 25 Mar 2021 13:36:25 -0400 Subject: [PATCH 66/88] [Cases] Adding feature flag for sub cases (#95122) * Adding feature flag for sub cases * Disabling case as a connector in security solution * Fix connector test * Switching feature flag to global variable * Removing this.config use * Fixing circular import and renaming flag --- x-pack/plugins/cases/common/constants.ts | 9 +- .../cases/server/client/cases/create.ts | 13 +- .../plugins/cases/server/client/cases/get.ts | 32 +- .../plugins/cases/server/client/cases/push.ts | 9 +- .../cases/server/client/cases/update.ts | 21 + .../cases/server/client/comments/add.ts | 23 +- .../server/connectors/case/index.test.ts | 29 +- .../cases/server/connectors/case/index.ts | 7 + x-pack/plugins/cases/server/plugin.ts | 24 +- .../api/cases/comments/delete_all_comments.ts | 19 +- .../api/cases/comments/delete_comment.ts | 8 +- .../api/cases/comments/find_comments.ts | 8 +- .../api/cases/comments/get_all_comment.ts | 13 +- .../api/cases/comments/patch_comment.ts | 8 +- .../routes/api/cases/comments/post_comment.ts | 23 +- .../server/routes/api/cases/delete_cases.ts | 16 +- .../cases/server/routes/api/cases/get_case.ts | 8 +- .../plugins/cases/server/routes/api/index.ts | 17 +- x-pack/plugins/cases/server/services/index.ts | 19 +- .../security_solution/common/constants.ts | 3 +- .../rules/rule_actions_field/index.test.tsx | 21 +- .../tests/cases/comments/delete_comment.ts | 23 +- .../tests/cases/comments/find_comments.ts | 13 +- .../tests/cases/comments/get_all_comments.ts | 104 ++- .../basic/tests/cases/comments/get_comment.ts | 33 +- .../tests/cases/comments/patch_comment.ts | 23 +- .../tests/cases/comments/post_comment.ts | 15 +- .../basic/tests/cases/delete_cases.ts | 3 +- .../basic/tests/cases/find_cases.ts | 3 +- .../basic/tests/cases/get_case.ts | 10 + .../basic/tests/cases/patch_cases.ts | 31 +- .../basic/tests/cases/push_case.ts | 3 +- .../tests/cases/sub_cases/delete_sub_cases.ts | 126 +-- .../tests/cases/sub_cases/find_sub_cases.ts | 396 ++++----- .../tests/cases/sub_cases/get_sub_case.ts | 132 +-- .../tests/cases/sub_cases/patch_sub_cases.ts | 792 +++++++++--------- .../basic/tests/connectors/case.ts | 21 +- .../case_api_integration/common/lib/utils.ts | 4 +- 38 files changed, 1216 insertions(+), 846 deletions(-) diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 1e7cff99a00bd..148b81c346b6e 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants'; - export const APP_ID = 'cases'; /** @@ -53,5 +51,12 @@ export const SUPPORTED_CONNECTORS = [ * Alerts */ +// this value is from x-pack/plugins/security_solution/common/constants.ts +const DEFAULT_MAX_SIGNALS = 100; export const MAX_ALERTS_PER_SUB_CASE = 5000; export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS; + +/** + * This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete. + */ +export const ENABLE_CASE_CONNECTOR = false; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 59f9688836341..650b9aa81c990 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -35,6 +35,7 @@ import { CaseUserActionServiceSetup, } from '../../services'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface CreateCaseArgs { caseConfigureService: CaseConfigureServiceSetup; @@ -60,9 +61,19 @@ export const create = async ({ }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; + + if (!ENABLE_CASE_CONNECTOR && type === CaseType.collection) { + throw Boom.badRequest( + 'Case type cannot be collection when the case connector feature is disabled' + ); + } + const query = pipe( // decode with the defaulted type field - excess(CasesClientPostRequestRt).decode({ type, ...nonTypeCaseFields }), + excess(CasesClientPostRequestRt).decode({ + type, + ...nonTypeCaseFields, + }), fold(throwErrors(Boom.badRequest), identity) ); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index fa556986ee8d3..50725879278e4 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { SavedObjectsClientContract, Logger, SavedObject } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; -import { CaseResponseRt, CaseResponse } from '../../../common/api'; +import { CaseResponseRt, CaseResponse, ESCaseAttributes } from '../../../common/api'; import { CaseServiceSetup } from '../../services'; import { countAlertsForID } from '../../common'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; @@ -33,15 +34,26 @@ export const get = async ({ includeSubCaseComments = false, }: GetParams): Promise => { try { - const [theCase, subCasesForCaseId] = await Promise.all([ - caseService.getCase({ + let theCase: SavedObject; + let subCaseIds: string[] = []; + + if (ENABLE_CASE_CONNECTOR) { + const [caseInfo, subCasesForCaseId] = await Promise.all([ + caseService.getCase({ + client: savedObjectsClient, + id, + }), + caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + ]); + + theCase = caseInfo; + subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + } else { + theCase = await caseService.getCase({ client: savedObjectsClient, id, - }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), - ]); - - const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + }); + } if (!includeComments) { return CaseResponseRt.encode( @@ -58,7 +70,7 @@ export const get = async ({ sortField: 'created_at', sortOrder: 'asc', }, - includeSubCaseComments, + includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments, }); return CaseResponseRt.encode( diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 3217178768f89..216ef109534fb 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -40,6 +40,7 @@ import { } from '../../services'; import { CasesClientHandler } from '../client'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -92,7 +93,11 @@ export const push = async ({ try { [theCase, connector, userActions] = await Promise.all([ - casesClient.get({ id: caseId, includeComments: true, includeSubCaseComments: true }), + casesClient.get({ + id: caseId, + includeComments: true, + includeSubCaseComments: ENABLE_CASE_CONNECTOR, + }), actionsClient.get({ id: connectorId }), casesClient.getUserActions({ caseId }), ]); @@ -183,7 +188,7 @@ export const push = async ({ page: 1, perPage: theCase?.totalComment ?? 0, }, - includeSubCaseComments: true, + includeSubCaseComments: ENABLE_CASE_CONNECTOR, }), ]); } catch (e) { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index ff3c0a62407a1..b39bfe6ec4eb7 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -55,6 +55,7 @@ import { CasesClientHandler } from '..'; import { createAlertUpdateRequest } from '../../common'; import { UpdateAlertRequest } from '../types'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -97,6 +98,22 @@ function throwIfUpdateTypeCollectionToIndividual( } } +/** + * Throws an error if any of the requests attempt to update the type of a case. + */ +function throwIfUpdateType(requests: ESCasePatchRequest[]) { + const requestsUpdatingType = requests.filter((req) => req.type !== undefined); + + if (requestsUpdatingType.length > 0) { + const ids = requestsUpdatingType.map((req) => req.id); + throw Boom.badRequest( + `Updating the type of a case when sub cases are disabled is not allowed ids: [${ids.join( + ', ' + )}]` + ); + } +} + /** * Throws an error if any of the requests attempt to update an individual style cases' type field to a collection * when alerts are attached to the case. @@ -396,6 +413,10 @@ export const update = async ({ return acc; }, new Map>()); + if (!ENABLE_CASE_CONNECTOR) { + throwIfUpdateType(updateFilterCases); + } + throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); await throwIfInvalidUpdateOfTypeWithAlerts({ diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/comments/add.ts index 45746613dc1d4..5a119432b3ccb 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/comments/add.ts @@ -36,7 +36,10 @@ import { CommentableCase, createAlertUpdateRequest } from '../../common'; import { CasesClientHandler } from '..'; import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; -import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { + ENABLE_CASE_CONNECTOR, + MAX_GENERATED_ALERTS_PER_SUB_CASE, +} from '../../../common/constants'; async function getSubCase({ caseService, @@ -224,10 +227,14 @@ async function getCombinedCase({ client, id, }), - service.getSubCase({ - client, - id, - }), + ...(ENABLE_CASE_CONNECTOR + ? [ + service.getSubCase({ + client, + id, + }), + ] + : [Promise.reject('case connector feature is disabled')]), ]); if (subCasePromise.status === 'fulfilled') { @@ -287,6 +294,12 @@ export const addComment = async ({ ); if (isCommentRequestTypeGenAlert(comment)) { + if (!ENABLE_CASE_CONNECTOR) { + throw Boom.badRequest( + 'Attempting to add a generated alert when case connector feature is disabled' + ); + } + return addGeneratedAlerts({ caseId, comment, diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index fa2b10a0ccbdb..8a025ed0f79b7 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -886,7 +886,34 @@ describe('case connector', () => { }); }); - describe('execute', () => { + it('should throw an error when executing the connector', async () => { + expect.assertions(2); + const actionId = 'some-id'; + const params: CaseExecutorParams = { + // @ts-expect-error + subAction: 'not-supported', + // @ts-expect-error + subActionParams: {}, + }; + + const executorOptions: CaseActionTypeExecutorOptions = { + actionId, + config: {}, + params, + secrets: {}, + services, + }; + + try { + await caseActionType.executor(executorOptions); + } catch (e) { + expect(e).not.toBeNull(); + expect(e.message).toBe('[Action][Case] connector not supported'); + } + }); + + // ENABLE_CASE_CONNECTOR: enable these tests after the case connector feature is completed + describe.skip('execute', () => { it('allows only supported sub-actions', async () => { expect.assertions(2); const actionId = 'some-id'; diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index da993faf0ef5c..c5eb609e260ae 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -27,6 +27,7 @@ import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; import { nullUser } from '../../common'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; @@ -70,6 +71,12 @@ async function executor( }: GetActionTypeParams, execOptions: CaseActionTypeExecutorOptions ): Promise> { + if (!ENABLE_CASE_CONNECTOR) { + const msg = '[Action][Case] connector not supported'; + logger.error(msg); + throw new Error(msg); + } + const { actionId, params, services } = execOptions; const { subAction, subActionParams } = params; let data: CaseExecutorResponse | null = null; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 0c661cc18c21b..8b53fd77d98a5 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -10,7 +10,7 @@ import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; -import { APP_ID } from '../common/constants'; +import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -70,7 +70,6 @@ export class CasePlugin { core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); core.savedObjects.registerType(caseSavedObjectType); - core.savedObjects.registerType(subCaseSavedObjectType); core.savedObjects.registerType(caseUserActionSavedObjectType); this.log.debug( @@ -111,15 +110,18 @@ export class CasePlugin { router, }); - registerConnectors({ - actionsRegisterType: plugins.actions.registerType, - logger: this.log, - caseService: this.caseService, - caseConfigureService: this.caseConfigureService, - connectorMappingsService: this.connectorMappingsService, - userActionService: this.userActionService, - alertsService: this.alertsService, - }); + if (ENABLE_CASE_CONNECTOR) { + core.savedObjects.registerType(subCaseSavedObjectType); + registerConnectors({ + actionsRegisterType: plugins.actions.registerType, + logger: this.log, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + connectorMappingsService: this.connectorMappingsService, + userActionService: this.userActionService, + alertsService: this.alertsService, + }); + } } public start(core: CoreStart) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts index fd250b74fff1e..7f6cfb224fada 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts @@ -5,12 +5,12 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; - import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { AssociationType } from '../../../../../common/api'; export function initDeleteAllCommentsApi({ @@ -35,18 +35,23 @@ export function initDeleteAllCommentsApi({ }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const client = context.core.savedObjects.client; // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - const id = request.query?.subCaseId ?? request.params.case_id; + const subCaseId = request.query?.subCaseId; + const id = subCaseId ?? request.params.case_id; const comments = await caseService.getCommentsByAssociation({ client, id, - associationType: request.query?.subCaseId - ? AssociationType.subCase - : AssociationType.case, + associationType: subCaseId ? AssociationType.subCase : AssociationType.case, }); await Promise.all( @@ -66,7 +71,7 @@ export function initDeleteAllCommentsApi({ actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: request.params.case_id, - subCaseId: request.query?.subCaseId, + subCaseId, commentId: comment.id, fields: ['comment'], }) diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index f1c5fdc2b7cc8..f8771f92c417f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -12,7 +12,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_obje import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; export function initDeleteCommentApi({ caseService, @@ -37,6 +37,12 @@ export function initDeleteCommentApi({ }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const client = context.core.savedObjects.client; // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index 57ddd84e8742c..9468b2b01fe37 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -22,7 +22,7 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, transformComments, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { defaultPage, defaultPerPage } from '../..'; const FindQueryParamsRt = rt.partial({ @@ -49,6 +49,12 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe fold(throwErrors(Boom.badRequest), identity) ); + if (!ENABLE_CASE_CONNECTOR && query.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const id = query.subCaseId ?? request.params.case_id; const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; const args = query diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 770efe0109744..2699f7a0307f7 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -5,13 +5,14 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { SavedObjectsFindResponse } from 'kibana/server'; import { AllCommentsResponseRt, CommentAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObjects, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { defaultSortField } from '../../../../common'; export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { @@ -35,6 +36,16 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps const client = context.core.savedObjects.client; let comments: SavedObjectsFindResponse; + if ( + !ENABLE_CASE_CONNECTOR && + (request.query?.subCaseId !== undefined || + request.query?.includeSubCaseComments !== undefined) + ) { + throw Boom.badRequest( + 'The `subCaseId` and `includeSubCaseComments` are not supported when the case connector feature is disabled' + ); + } + if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ client, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts index f5db2dc004a1d..519692d2d78a1 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts @@ -19,7 +19,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_obje import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError, decodeCommentRequest } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { CaseServiceSetup } from '../../../../services'; interface CombinedCaseParams { @@ -82,6 +82,12 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const client = context.core.savedObjects.client; const query = pipe( CommentPatchRequestRt.decode(request.body), diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts index 110a16a610014..8658f9ba0aac5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts @@ -5,10 +5,11 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { escapeHatch, wrapError } from '../../utils'; import { RouteDeps } from '../../types'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { CommentRequest } from '../../../../../common/api'; export function initPostCommentApi({ router, logger }: RouteDeps) { @@ -28,15 +29,21 @@ export function initPostCommentApi({ router, logger }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } - const casesClient = context.cases.getCasesClient(); - const caseId = request.query?.subCaseId ?? request.params.case_id; - const comment = request.body as CommentRequest; + if (!context.cases) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + + const casesClient = context.cases.getCasesClient(); + const caseId = request.query?.subCaseId ?? request.params.case_id; + const comment = request.body as CommentRequest; - try { return response.ok({ body: await casesClient.addComment({ caseId, comment }), }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 5f2a6c67220c3..d91859d4e8cbb 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -11,7 +11,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; async function deleteSubCases({ @@ -91,7 +91,10 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log ); } - await deleteSubCases({ caseService, client, caseIds: request.query.ids }); + if (ENABLE_CASE_CONNECTOR) { + await deleteSubCases({ caseService, client, caseIds: request.query.ids }); + } + // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); @@ -104,7 +107,14 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: id, - fields: ['comment', 'description', 'status', 'tags', 'title', 'sub_case'], + fields: [ + 'comment', + 'description', + 'status', + 'tags', + 'title', + ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), + ], }) ), }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index f464f7e47fe7a..e8e35d875f42f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -7,9 +7,10 @@ import { schema } from '@kbn/config-schema'; +import Boom from '@hapi/boom'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { CASE_DETAILS_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( @@ -27,6 +28,11 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query.includeSubCaseComments !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } const casesClient = context.cases.getCasesClient(); const id = request.params.case_id; diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index 12d1da36077c7..c5b7aa85dc33e 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -37,6 +37,7 @@ import { initGetSubCaseApi } from './cases/sub_case/get_sub_case'; import { initPatchSubCasesApi } from './cases/sub_case/patch_sub_cases'; import { initFindSubCasesApi } from './cases/sub_case/find_sub_cases'; import { initDeleteSubCasesApi } from './cases/sub_case/delete_sub_cases'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** * Default page number when interacting with the saved objects API. @@ -56,12 +57,16 @@ export function initCaseApi(deps: RouteDeps) { initPostCaseApi(deps); initPushCaseApi(deps); initGetAllCaseUserActionsApi(deps); - initGetAllSubCaseUserActionsApi(deps); - // Sub cases - initGetSubCaseApi(deps); - initPatchSubCasesApi(deps); - initFindSubCasesApi(deps); - initDeleteSubCasesApi(deps); + + if (ENABLE_CASE_CONNECTOR) { + // Sub cases + initGetAllSubCaseUserActionsApi(deps); + initGetSubCaseApi(deps); + initPatchSubCasesApi(deps); + initFindSubCasesApi(deps); + initDeleteSubCasesApi(deps); + } + // Comments initDeleteCommentApi(deps); initDeleteAllCommentsApi(deps); diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 11ceb48d11e9f..7c5f06d48bb05 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -34,6 +34,7 @@ import { caseTypeField, CasesFindRequest, } from '../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; import { combineFilters, defaultSortField, groupTotalAlertsByID } from '../common'; import { defaultPage, defaultPerPage } from '../routes/api'; import { @@ -282,13 +283,15 @@ export class CaseService implements CaseServiceSetup { options: caseOptions, }); - const subCasesResp = await this.findSubCasesGroupByCase({ - client, - options: subCaseOptions, - ids: cases.saved_objects - .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) - .map((caseInfo) => caseInfo.id), - }); + const subCasesResp = ENABLE_CASE_CONNECTOR + ? await this.findSubCasesGroupByCase({ + client, + options: subCaseOptions, + ids: cases.saved_objects + .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) + .map((caseInfo) => caseInfo.id), + }) + : { subCasesMap: new Map(), page: 0, perPage: 0 }; const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => { const subCasesForCase = subCasesResp.subCasesMap.get(caseInfo.id); @@ -407,7 +410,7 @@ export class CaseService implements CaseServiceSetup { let subCasesTotal = 0; - if (subCaseOptions) { + if (ENABLE_CASE_CONNECTOR && subCaseOptions) { subCasesTotal = await this.findSubCaseStatusStats({ client, options: subCaseOptions, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 143384d160471..4c62179f9ed54 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ENABLE_CASE_CONNECTOR } from '../../cases/common/constants'; + export const APP_ID = 'securitySolution'; export const SERVER_APP_ID = 'siem'; export const APP_NAME = 'Security'; @@ -171,7 +173,6 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID]; /* Rule notifications options */ -export const ENABLE_CASE_CONNECTOR = true; export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx index 5dbe1f1cef5be..fb71c6c4b0350 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx @@ -94,7 +94,26 @@ describe('RuleActionsField', () => { `); }); - it('if we do NOT have an error on case action creation, we are supporting case connector', () => { + // sub-cases-enabled: remove this once the sub cases and connector feature is completed + // https://github.com/elastic/kibana/issues/94115 + it('should not contain the case connector as a supported action', () => { + expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".jira", + "minimumLicenseRequired": "gold", + "name": "My Jira", + }, + ] + `); + }); + + // sub-cases-enabled: unskip after sub cases and the case connector is supported + // https://github.com/elastic/kibana/issues/94115 + it.skip('if we do NOT have an error on case action creation, we are supporting case connector', () => { expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(` Array [ Object { diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index 7cb66b6815b98..fece9abd5fa35 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -85,7 +85,28 @@ export default ({ getService }: FtrProviderContext): void => { .expect(404); }); - describe('sub case comments', () => { + it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('subCaseId'); + }); + + it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index 7bbc8e344ee23..44ff1c7ebffe1 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -111,7 +111,18 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - describe('sub case comments', () => { + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments/_find?search=unique&subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts index 723c9eba33beb..e73614d88ca95 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts @@ -24,13 +24,6 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); describe('get_all_comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); afterEach(async () => { await deleteAllCaseItems(es); }); @@ -63,47 +56,78 @@ export default ({ getService }: FtrProviderContext): void => { expect(comments.length).to.eql(2); }); - it('should get comments from a case and its sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?subCaseId=value`) .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) - .expect(200); + .send() + .expect(400); - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); + expect(body.message).to.contain('subCaseId'); }); - it('should get comments from a sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) + it('should return a 400 when passing the includeSubCaseComments parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?includeSubCaseComments=true`) .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .expect(200); + .send() + .expect(400); - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); + expect(body.message).to.contain('includeSubCaseComments'); }); - it('should not find any comments for an invalid case id', async () => { - const { body } = await supertest - .get(`${CASES_URL}/fake-id/comments`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - expect(body.length).to.eql(0); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + + it('should get comments from a case and its sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should get comments from a sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should not find any comments for an invalid case id', async () => { + const { body } = await supertest + .get(`${CASES_URL}/fake-id/comments`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 1a1bb727bd429..a74d8f86d225b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -24,13 +24,6 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); describe('get_comment', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); afterEach(async () => { await deleteAllCaseItems(es); }); @@ -57,14 +50,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(comment).to.eql(patchedCase.comments[0]); }); - it('should get a sub case comment', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: comment }: { body: CommentResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) - .expect(200); - expect(comment.type).to.be(CommentType.generatedAlert); - }); - it('unhappy path - 404s when comment is not there', async () => { await supertest .get(`${CASES_URL}/fake-id/comments/fake-comment`) @@ -72,5 +57,23 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(404); }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + it('should get a sub case comment', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const { body: comment }: { body: CommentResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) + .expect(200); + expect(comment.type).to.be(CommentType.generatedAlert); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index bddc620535dda..b59a248ee07e4 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -39,7 +39,28 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - describe('sub case comments', () => { + it('should return a 400 when the subCaseId parameter is passed', async () => { + const { body } = await supertest + .patch(`${CASES_URL}/case-id}/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send({ + id: 'id', + version: 'version', + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }) + .expect(400); + + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index 5e48e39164e6b..c46698a0905b4 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -227,7 +227,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - it('400s when adding an alert to a collection case', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('400s when adding an alert to a collection case', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -376,7 +377,17 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('sub case comments', () => { + it('should return a 400 when passing the subCaseId', async () => { + const { body } = await supertest + .post(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(400); + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index b5187931a9f01..706fded263282 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -90,7 +90,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(404); }); - describe('sub cases', () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index b808ff4ccdf35..7277c93b7b75b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -248,7 +248,8 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.count_in_progress_cases).to.eql(1); }); - describe('stats with sub cases', () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('stats with sub cases', () => { let collection: CreateSubCaseResp; let actionID: string; before(async () => { diff --git a/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts index fb4ab2c86469a..43bd7a616e729 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts @@ -43,6 +43,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql(postCaseResp(postedCase.id)); }); + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('subCaseId'); + }); + it('unhappy path - 404s when case is not there', async () => { await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index c202111f0e5e4..f43b47da19ade 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -134,7 +134,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(404); }); - it('should 400 and not allow converting a collection back to an individual case', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should 400 and not allow converting a collection back to an individual case', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -156,7 +157,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - it('should allow converting an individual case to a collection when it does not have alerts', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -212,7 +214,30 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - it("should 400 when attempting to update a collection case's status", async () => { + it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.collection, + }, + ], + }) + .expect(400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip("should 400 when attempting to update a collection case's status", async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index 2db15eb603f7c..47842eeca6f1c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -234,7 +234,8 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.status).to.eql('closed'); }); - it('should push a collection case but not close it when closure_type: close-by-pushing', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { body: connector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts index d179120cd3d85..15b3b9311e3c3 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts @@ -26,75 +26,87 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - describe('delete_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('delete_sub_cases disabled routes', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["sub-case-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); }); - it('should delete a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('delete_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); - const { body: subCase } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(200); + it('should delete a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); - expect(subCase.id).to.not.eql(undefined); + const { body: subCase } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(200); - const { body } = await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + expect(subCase.id).to.not.eql(undefined); - expect(body).to.eql({}); - await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(404); - }); + const { body } = await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); - it(`should delete a sub case's comments when that case gets deleted`, async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(404); + }); - // there should be two comments on the sub case now - const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .query({ subCaseId: caseInfo.subCases![0].id }) - .send(postCommentUserReq) - .expect(200); + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); - const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.comments![1].id - }`; - // make sure we can get the second comment - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + // there should be two comments on the sub case now + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseId: caseInfo.subCases![0].id }) + .send(postCommentUserReq) + .expect(200); - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); - }); + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); - it('unhappy path - 404s when sub case id is invalid', async () => { - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + + it('unhappy path - 404s when sub case id is invalid', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts index 2c1bd9c7bd883..b7f2094b0acc3 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts @@ -29,226 +29,234 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('find_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('find_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(`${getSubCasesUrl('case-id')}/_find`).expect(404); }); - it('should not find any sub cases when none exist', async () => { - const { body: caseResp }: { body: CaseResponse } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - const { body: findSubCases } = await supertest - .get(`${getSubCasesUrl(caseResp.id)}/_find`) - .expect(200); - - expect(findSubCases).to.eql({ - page: 1, - per_page: 20, - total: 0, - subCases: [], - count_open_cases: 0, - count_closed_cases: 0, - count_in_progress_cases: 0, + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('find_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); }); - }); - - it('should return a sub cases with comment stats', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 1, - // find should not return the comments themselves only the stats - subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], - count_open_cases: 1, + after(async () => { + await deleteCaseAction(supertest, actionID); }); - }); - - it('should return multiple sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 2, - // find should not return the comments themselves only the stats - subCases: [ - { - // there should only be 1 closed sub case - ...subCase2Resp.modifiedSubCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - status: CaseStatuses.closed, - }, - { - ...subCase2Resp.newSubCaseInfo.subCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - }, - ], - count_open_cases: 1, - count_closed_cases: 1, + afterEach(async () => { + await deleteAllCaseItems(es); }); - }); - - it('should only return open when filtering for open', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) - .expect(200); - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should only return closed when filtering for closed', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + it('should not find any sub cases when none exist', async () => { + const { body: caseResp }: { body: CaseResponse } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCollectionReq) + .expect(200); + + const { body: findSubCases } = await supertest + .get(`${getSubCasesUrl(caseResp.id)}/_find`) + .expect(200); + + expect(findSubCases).to.eql({ + page: 1, + per_page: 20, + total: 0, + subCases: [], + count_open_cases: 0, + count_closed_cases: 0, + count_in_progress_cases: 0, + }); + }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) - .expect(200); + it('should return a sub cases with comment stats', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); - it('should only return in progress when filtering for in progress', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + expect(body).to.eql({ + ...findSubCasesResp, + total: 1, + // find should not return the comments themselves only the stats + subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], + count_open_cases: 1, + }); }); - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + it('should return multiple sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); + + expect(body).to.eql({ + ...findSubCasesResp, + total: 2, + // find should not return the comments themselves only the stats + subCases: [ + { + // there should only be 1 closed sub case + ...subCase2Resp.modifiedSubCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + status: CaseStatuses.closed, + }, + { + ...subCase2Resp.newSubCaseInfo.subCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + }, + ], + count_open_cases: 1, + count_closed_cases: 1, + }); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) - .expect(200); + it('should only return open when filtering for open', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); - }); + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) + .expect(200); - it('should sort on createdAt field in descending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) - .expect(200); + it('should only return closed when filtering for closed', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.subCases[1].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) + .expect(200); - it('should sort on createdAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should sort on updatedAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + it('should only return in progress when filtering for in progress', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); }); - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + it('should sort on createdAt field in descending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.subCases[1].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) - .expect(200); + it('should sort on createdAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); + it('should sort on updatedAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts index 440731cd07fe7..8d4ffafbf763a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts @@ -37,79 +37,87 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('get_sub_case', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('get_sub_case disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(getSubCaseDetailsUrl('case-id', 'sub-case-id')).expect(404); }); - it('should return a case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('get_sub_case', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + it('should return a case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], - associationType: AssociationType.subCase, - }) - ); + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) - ); - }); + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], + associationType: AssociationType.subCase, + }) + ); - it('should return the correct number of alerts with multiple types of alerts', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) + ); + }); - const { body: singleAlert }: { body: CaseResponse } = await supertest - .post(getCaseCommentsUrl(caseInfo.id)) - .query({ subCaseId: caseInfo.subCases![0].id }) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); + it('should return the correct number of alerts with multiple types of alerts', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const { body: singleAlert }: { body: CaseResponse } = await supertest + .post(getCaseCommentsUrl(caseInfo.id)) + .query({ subCaseId: caseInfo.subCases![0].id }) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [ - { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, - { - comment: postCommentAlertReq, - id: singleAlert.comments![1].id, - }, - ], - associationType: AssociationType.subCase, - }) - ); + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) - ); - }); + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [ + { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, + { + comment: postCommentAlertReq, + id: singleAlert.comments![1].id, + }, + ], + associationType: AssociationType.subCase, + }) + ); + + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) + ); + }); - it('unhappy path - 404s when case is not there', async () => { - await supertest - .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index e5cc2489a12e9..d993a627d186b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -36,463 +36,475 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); - describe('patch_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - beforeEach(async () => { - await esArchiver.load('cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/default'); - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('patch_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .patch(SUB_CASES_PATCH_DEL_URL) + .set('kbn-xsrf', 'true') + .send({ subCases: [] }) + .expect(404); }); - it('should update the status of a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('patch_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); }); - const { body: subCase }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .expect(200); - - expect(subCase.status).to.eql(CaseStatuses['in-progress']); - }); - - it('should update the status of one alert attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - }, + after(async () => { + await deleteCaseAction(supertest, actionID); }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); }); - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of multiple alerts attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + it('should update the status of a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, + await setStatus({ + supertest, + cases: [ { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, - ]), - type: CommentType.generatedAlert, - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); + ], + type: 'sub_case', + }); + const { body: subCase }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .expect(200); - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], + expect(subCase.status).to.eql(CaseStatuses['in-progress']); }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], + it('should update the status of one alert attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, - ], - type: 'sub_case', - }); + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); - it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, - }, - }, - comment: { - alerts: createAlertsString([ + await setStatus({ + supertest, + cases: [ { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, - ]), - type: CommentType.generatedAlert, - }, - }); + ], + type: 'sub_case', + }); - // This will close the first sub case and create a new one - const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ - supertest, - actionID, - caseID: initialCaseInfo.id, - comment: { - alerts: createAlertsString([ - { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - }, - }); + await es.indices.refresh({ index: defaultSignalsIndex }); - await es.indices.refresh({ index: defaultSignalsIndex }); + signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: collectionWithSecondSub.subCases![0].id, - version: collectionWithSecondSub.subCases![0].version, - status: CaseStatuses['in-progress'], + it('should update the status of multiple alerts attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, - ], - type: 'sub_case', - }); + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - // There still should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + await setStatus({ + supertest, cases: [ { - id: collectionWithSecondSub.id, - version: collectionWithSecondSub.version, - settings: { syncAlerts: true }, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, ], - }) - .expect(200); + type: 'sub_case', + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); - it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - const { body: individualCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, + const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, + }, + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, }); - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, + // This will close the first sub case and create a new one + const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ + supertest, + actionID, + caseID: initialCaseInfo.id, + comment: { + alerts: createAlertsString([ + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, - }, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - }, - }); + }); - const { body: updatedIndWithComment } = await supertest - .post(`${CASES_URL}/${individualCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + await es.indices.refresh({ index: defaultSignalsIndex }); - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); - const updatedIndWithStatus = ( await setStatus({ supertest, cases: [ { - id: updatedIndWithComment.id, - version: updatedIndWithComment.version, - status: CaseStatuses.closed, + id: collectionWithSecondSub.subCases![0].id, + version: collectionWithSecondSub.subCases![0].version, + status: CaseStatuses['in-progress'], }, ], - type: 'case', - }) - )[0]; // there should only be a single entry in the response + type: 'sub_case', + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There still should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: collectionWithSecondSub.id, + version: collectionWithSecondSub.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); - // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); + it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { body: individualCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, }, - ], - }) - .expect(200); + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + }, + }); - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + const { body: updatedIndWithComment } = await supertest + .post(`${CASES_URL}/${individualCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, cases: [ { - id: updatedIndWithStatus.id, - version: updatedIndWithStatus.version, - settings: { syncAlerts: true }, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, ], - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); + type: 'sub_case', + }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + const updatedIndWithStatus = ( + await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment.id, + version: updatedIndWithComment.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + }) + )[0]; // there should only be a single entry in the response + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - }); + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: updatedIndWithStatus.id, + version: updatedIndWithStatus.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - it('404s when sub case id is invalid', async () => { - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: 'fake-id', - version: 'blah', - status: CaseStatuses.open, - }, - ], - }) - .expect(404); - }); + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + }); - it('406s when updating invalid fields for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + it('404s when sub case id is invalid', async () => { + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: 'fake-id', + version: 'blah', + status: CaseStatuses.open, + }, + ], + }) + .expect(404); + }); - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - type: 'blah', - }, - ], - }) - .expect(406); + it('406s when updating invalid fields for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + type: 'blah', + }, + ], + }) + .expect(406); + }); }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index ee4d671f7880f..0f9cba4b51f75 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -36,7 +36,20 @@ export default ({ getService }: FtrProviderContext): void => { describe('case_connector', () => { let createdActionId = ''; - it('should return 200 when creating a case action successfully', async () => { + it('should return 400 when creating a case action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should return 200 when creating a case action successfully', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -70,7 +83,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('create', () => { + describe.skip('create', () => { it('should respond with a 400 Bad Request when creating a case without title', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') @@ -500,7 +513,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('update', () => { + describe.skip('update', () => { it('should respond with a 400 Bad Request when updating a case without id', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') @@ -624,7 +637,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('addComment', () => { + describe.skip('addComment', () => { it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 6fb108f69ad22..f7ff49727df33 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import type { estypes } from '@elastic/elasticsearch'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import * as st from 'supertest'; @@ -48,7 +48,7 @@ export const getSignalsWithES = async ({ indices: string | string[]; ids: string | string[]; }): Promise>>> => { - const signals = await es.search({ + const signals: ApiResponse> = await es.search({ index: indices, body: { size: 10000, From 50bdbfc18e7b121d38f64d015e5dbddf8b4ae552 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 25 Mar 2021 13:47:15 -0400 Subject: [PATCH 67/88] [Dashboard] Fix Title BWC (#95355) * allow saving blank string to panel title --- .../common/embeddable/embeddable_saved_object_converters.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 }), }; } From 02fce982544a63efc20de4fe027ba66833bab9b8 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 25 Mar 2021 12:51:05 -0500 Subject: [PATCH 68/88] skip flaky test. #89389 --- .../tests/exception_operators_data_types/ip.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts index 521a5c01a1203..058ae16dac8a9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts @@ -626,7 +626,8 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql(['127.0.0.1', '127.0.0.3']); }); - it('will return 4 results if we have a list that excludes all ips', async () => { + // flaky https://github.com/elastic/kibana/issues/89389 + it.skip('will return 4 results if we have a list that excludes all ips', async () => { await importFile( supertest, 'ip', From 6a571486fc1905835cccc714cee7c3a3d3be8b43 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 25 Mar 2021 19:25:49 +0100 Subject: [PATCH 69/88] [Security Solution][Detections] Improves indicator match Cypress tests (#94913) * updates the data used in the test * adds matches test * adds enrichment test * improves speed and adds missing files * fixes type check issue * adds 'data-test-subj' for the json view tab * refactor * fixes typecheck issue * updates tests with latest master changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indicator_match_rule.spec.ts | 113 +- .../security_solution/cypress/objects/rule.ts | 14 +- .../cypress/screens/alerts_details.ts | 12 + .../cypress/screens/fields_browser.ts | 4 +- .../cypress/screens/rule_details.ts | 7 + .../cypress/tasks/alerts_details.ts | 17 + .../cypress/tasks/api_calls/rules.ts | 10 +- .../cypress/tasks/create_new_rule.ts | 2 +- .../cypress/tasks/fields_browser.ts | 11 +- .../cypress/tasks/rule_details.ts | 9 + .../event_details/event_details.tsx | 1 + .../suspicious_source_event/data.json | 13 + .../suspicious_source_event/mappings.json | 29 + .../es_archives/threat_data/data.json.gz | Bin 1086 -> 0 bytes .../es_archives/threat_data/mappings.json | 3577 ----------------- .../es_archives/threat_indicator/data.json | 71 +- .../threat_indicator/mappings.json | 800 +++- 17 files changed, 1075 insertions(+), 3615 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/screens/alerts_details.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts create mode 100644 x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json create mode 100644 x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json delete mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz delete mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index ef9c7f49cb371..e1e78f8e310e1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -16,6 +16,7 @@ import { ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; +import { JSON_LINES } from '../../screens/alerts_details'; import { CUSTOM_RULES_BTN, RISK_SCORE, @@ -50,14 +51,17 @@ import { SCHEDULE_DETAILS, SEVERITY_DETAILS, TAGS_DETAILS, + TIMELINE_FIELD, TIMELINE_TEMPLATE_DETAILS, } from '../../screens/rule_details'; import { + expandFirstAlert, goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; +import { openJsonView, scrollJsonViewToBottom } from '../../tasks/alerts_details'; import { changeRowsPerPageTo300, duplicateFirstRule, @@ -98,7 +102,7 @@ import { import { waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { goBackToAllRulesTable } from '../../tasks/rule_details'; +import { addsFieldsToTimeline, goBackToAllRulesTable } from '../../tasks/rule_details'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -114,11 +118,11 @@ describe('indicator match', () => { before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); - esArchiverLoad('threat_data'); + esArchiverLoad('suspicious_source_event'); }); after(() => { esArchiverUnload('threat_indicator'); - esArchiverUnload('threat_data'); + esArchiverUnload('suspicious_source_event'); }); describe('Creating new indicator match rules', () => { @@ -216,7 +220,7 @@ describe('indicator match', () => { it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getDefineContinueButton().click(); @@ -235,7 +239,7 @@ describe('indicator match', () => { it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: 'non-existent-value', validColumns: 'indexField', }); @@ -245,7 +249,7 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorAndButton().click(); @@ -267,14 +271,14 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: 'non-existent-value', validColumns: 'indexField', }); getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: 'second-non-existent-value', validColumns: 'indexField', }); @@ -305,7 +309,7 @@ describe('indicator match', () => { it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorDeleteButton().click(); @@ -317,7 +321,7 @@ describe('indicator match', () => { it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorAndButton().click(); @@ -330,16 +334,22 @@ describe('indicator match', () => { getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 3, - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorDeleteButton(2).click(); - getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorIndexComboField(1).should( + 'text', + newThreatIndicatorRule.indicatorMappingField + ); getIndicatorMappingComboField(1).should( 'text', newThreatIndicatorRule.indicatorIndexField ); - getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorIndexComboField(2).should( + 'text', + newThreatIndicatorRule.indicatorMappingField + ); getIndicatorMappingComboField(2).should( 'text', newThreatIndicatorRule.indicatorIndexField @@ -357,11 +367,14 @@ describe('indicator match', () => { getIndicatorOrButton().click(); fillIndicatorMatchRow({ rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorIndexComboField().should( + 'text', + newThreatIndicatorRule.indicatorMappingField + ); getIndicatorMappingComboField().should( 'text', newThreatIndicatorRule.indicatorIndexField @@ -441,7 +454,7 @@ describe('indicator match', () => { ); getDetails(INDICATOR_MAPPING).should( 'have.text', - `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + `${newThreatIndicatorRule.indicatorMappingField} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` ); getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); }); @@ -471,6 +484,74 @@ describe('indicator match', () => { }); }); + describe('Enrichment', () => { + const fieldSearch = 'threat.indicator.matched'; + const fields = [ + 'threat.indicator.matched.atomic', + 'threat.indicator.matched.type', + 'threat.indicator.matched.field', + ]; + const expectedFieldsText = [ + newThreatIndicatorRule.atomic, + newThreatIndicatorRule.type, + newThreatIndicatorRule.indicatorMappingField, + ]; + + const expectedEnrichment = [ + { line: 4, text: ' "threat": {' }, + { + line: 3, + text: + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + }, + { line: 2, text: ' }' }, + ]; + + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('suspicious_source_event'); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('suspicious_source_event'); + }); + + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + goToRuleDetails(); + }); + + it('Displays matches on the timeline', () => { + addsFieldsToTimeline(fieldSearch, fields); + + fields.forEach((field, index) => { + cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFieldsText[index]); + }); + }); + + it('Displays enrichment on the JSON view', () => { + expandFirstAlert(); + openJsonView(); + scrollJsonViewToBottom(); + + cy.get(JSON_LINES).then((elements) => { + const length = elements.length; + expectedEnrichment.forEach((enrichment) => { + cy.wrap(elements) + .eq(length - enrichment.line) + .should('have.text', enrichment.text); + }); + }); + }); + }); + describe('Duplicates the indicator rule', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 88dcd998fc06d..68c7796f7ca3b 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -71,8 +71,10 @@ export interface OverrideRule extends CustomRule { export interface ThreatIndicatorRule extends CustomRule { indicatorIndexPattern: string[]; - indicatorMapping: string; + indicatorMappingField: string; indicatorIndexField: string; + type?: string; + atomic?: string; } export interface MachineLearningRule { @@ -299,7 +301,7 @@ export const eqlSequenceRule: CustomRule = { export const newThreatIndicatorRule: ThreatIndicatorRule = { name: 'Threat Indicator Rule Test', description: 'The threat indicator rule description.', - index: ['threat-data-*'], + index: ['suspicious-*'], severity: 'Critical', riskScore: '20', tags: ['test', 'threat'], @@ -309,9 +311,11 @@ export const newThreatIndicatorRule: ThreatIndicatorRule = { note: '# test markdown', runsEvery, lookBack, - indicatorIndexPattern: ['threat-indicator-*'], - indicatorMapping: 'agent.id', - indicatorIndexField: 'agent.threat', + indicatorIndexPattern: ['filebeat-*'], + indicatorMappingField: 'myhash.mysha256', + indicatorIndexField: 'threatintel.indicator.file.hash.sha256', + type: 'file', + atomic: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', timeline, maxSignals: 100, }; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts new file mode 100644 index 0000000000000..417cf73de47f6 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const JSON_CONTENT = '[data-test-subj="jsonView"]'; + +export const JSON_LINES = '.ace_line'; + +export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index ea274c446c014..1115dfb00914e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -5,10 +5,12 @@ * 2.0. */ +export const CLOSE_BTN = '[data-test-subj="close"]'; + export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; export const FIELDS_BROWSER_CHECKBOX = (id: string) => { - return `[data-test-subj="field-${id}-checkbox`; + return `[data-test-subj="category-table-container"] [data-test-subj="field-${id}-checkbox"]`; }; export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index f9590b34a0a11..d94be17a0530a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -53,6 +53,9 @@ export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobS export const MITRE_ATTACK_DETAILS = 'MITRE ATT&CK'; +export const FIELDS_BROWSER_BTN = + '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]'; + export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]'; export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]'; @@ -92,6 +95,10 @@ export const TIMELINE_TEMPLATE_DETAILS = 'Timeline template'; export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override'; +export const TIMELINE_FIELD = (field: string) => { + return `[data-test-subj="draggable-content-${field}"]`; +}; + export const getDetails = (title: string) => cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts new file mode 100644 index 0000000000000..1582f35989e2c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_details.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 { JSON_CONTENT, JSON_VIEW_TAB } from '../screens/alerts_details'; + +export const openJsonView = () => { + cy.get(JSON_VIEW_TAB).click(); +}; + +export const scrollJsonViewToBottom = () => { + cy.get(JSON_CONTENT).click({ force: true }); + cy.get(JSON_CONTENT).type('{pagedown}{pagedown}{pagedown}'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 4bf5508c19aa9..0b051f3a26581 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -45,9 +45,9 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r { entries: [ { - field: rule.indicatorMapping, + field: rule.indicatorMappingField, type: 'mapping', - value: rule.indicatorMapping, + value: rule.indicatorIndexField, }, ], }, @@ -55,13 +55,13 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r threat_query: '*:*', threat_language: 'kuery', threat_filters: [], - threat_index: ['mock*'], + threat_index: rule.indicatorIndexPattern, threat_indicator_path: '', from: 'now-17520h', - index: ['exceptions-*'], + index: rule.index, query: rule.customQuery || '*:*', language: 'kuery', - enabled: false, + enabled: true, }, headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index b317f158ae614..0c663a95a4bda 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -426,7 +426,7 @@ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => { fillIndexAndIndicatorIndexPattern(rule.index, rule.indicatorIndexPattern); fillIndicatorMatchRow({ - indexField: rule.indicatorMapping, + indexField: rule.indicatorMappingField, indicatorIndexField: rule.indicatorIndexField, }); getDefineContinueButton().should('exist').click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index 9ee242dcebbe8..72945f557ac1b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -15,8 +15,15 @@ import { FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX, FIELDS_BROWSER_MESSAGE_CHECKBOX, FIELDS_BROWSER_RESET_FIELDS, + FIELDS_BROWSER_CHECKBOX, + CLOSE_BTN, } from '../screens/fields_browser'; -import { KQL_SEARCH_BAR } from '../screens/hosts/main'; + +export const addsFields = (fields: string[]) => { + fields.forEach((field) => { + cy.get(FIELDS_BROWSER_CHECKBOX(field)).click(); + }); +}; export const addsHostGeoCityNameToTimeline = () => { cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX).check({ @@ -44,7 +51,7 @@ export const clearFieldsBrowser = () => { }; export const closeFieldsBrowser = () => { - cy.get(KQL_SEARCH_BAR).click({ force: true }); + cy.get(CLOSE_BTN).click({ force: true }); }; export const filterFieldsBrowser = (fieldName: string) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 21a2745395419..37c425c5488bc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -20,10 +20,12 @@ import { ALERTS_TAB, BACK_TO_RULES, EXCEPTIONS_TAB, + FIELDS_BROWSER_BTN, REFRESH_BUTTON, REMOVE_EXCEPTION_BTN, RULE_SWITCH, } from '../screens/rule_details'; +import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser'; export const activatesRule = () => { cy.intercept('PATCH', '/api/detection_engine/rules/_bulk_update').as('bulk_update'); @@ -49,6 +51,13 @@ export const addsException = (exception: Exception) => { cy.get(CONFIRM_BTN).should('not.exist'); }; +export const addsFieldsToTimeline = (search: string, fields: string[]) => { + cy.get(FIELDS_BROWSER_BTN).click(); + filterFieldsBrowser(search); + addsFields(fields); + closeFieldsBrowser(); +}; + export const openExceptionModalFromRuleSettings = () => { cy.get(ADD_EXCEPTIONS_BTN).click(); cy.get(LOADING_SPINNER).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index ddb3d98cafca8..4979d70ce2d7b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -107,6 +107,7 @@ const EventDetailsComponent: React.FC = ({ }, { id: EventsViewType.jsonView, + 'data-test-subj': 'jsonViewTab', name: i18n.JSON_VIEW, content: ( <> diff --git a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json new file mode 100644 index 0000000000000..11b5e9bd0828b --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json @@ -0,0 +1,13 @@ +{ + "type": "doc", + "value": { + "id": "_eZE7mwBOpWiDweStB_c", + "index": "suspicious-source-event-001", + "source": { + "@timestamp": "2021-02-22T21:00:49.337Z", + "myhash": { + "mysha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json new file mode 100644 index 0000000000000..83b2b4d64a510 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json @@ -0,0 +1,29 @@ +{ + "type": "index", + "value": { + "aliases": { + "thread-data": { + "is_write_index": false + }, + "beats": { + }, + "siem-read-alias": { + } + }, + "index": "suspicious-source-event-001", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "myhash": { + "properties": { + "mysha256": { + "type": "keyword" + } + } + } + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz deleted file mode 100644 index ab63f9a47a7baa9e6ea0e830f55ec8c5e48df4c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1086 zcmV-E1i||siwFpR@w;CD17u-zVJ>QOZ*Bm+R!eW&I1s+)R|GzFfyyCeJq;|d2+~8b zD0*lX1+rKe6g9HBmPmo5?6^h#`;xNkkdlISFAah?o;PQ{`N;1#x3>#@YGJXyU6g_@ z-dn+e)SZ=lH($(GR$A=_o<5|_@&0rBl|3Bci@x8S&8-D5;n^DLodlwTl4uejgfDs} zI!Rw68p$7;HJ~(UTI&`foCnDK;zxwm5niKYiE#Qf_#1n&1+JX{Mg;8+8jz&koC{2F zM#DUTAdagvh;o90EJJC4(yTNJicpze0~-IGP@0pbKf3B9qqb-!j>I)Ohej((3q&EP zl9&cjnC1aVY{_wj%eW{ILWS#f=_u(+rVG;%S9t)bnBZ2QEzuG!2Gz^;u(W2A(-tQU z%7`N5R@ZkAqa_Y)s1Un(T0-}rtq*pkLfXiyEQ+$3#G)(xyyQSwO$t^secF5zygyf` z0%|HWy~lyyE^cPZy-_=D%u^mqu6maX7l5?TD&-!8bWuBPZC`^&v9TY zDTyqDa6UpS#lJxHe5p_qr5OzrgXT^511mvV>n&}kQ!EX>87KNY>zPr8GowuMWf(`x z;q#}*nW0H~pvq6{;0`bG9PZ#SfgPbk{RY7(9<*>xA(yr0(iaMfBKUb=@<4jd`u7cll-u| zzf+$-Ztp(+?(I2~a3vJc=|Xg7WoJn)bgxrMxEh#lp}lrp37@rxXu2GRq$wyh-jA&s z1Lm$%@~&X~u083U;48nYSM64aZ4Dc9PtyHH?cum72{h(Bv+$z!F$4~OWkHxf;>1wP zI$jxqM;?E{Gtf?x;!IWJik9gdCyWa6df87W4dY2y6v#t=asBd3$!2Dw=fQP^136Ef z#;?a;^&A=s^1)-DbYoH&ZZuzNC%WxtfZmV9-K==tm~m}b!@P36`x`BNh+fHMV{6%v zvXp1sFVJ&kezGb`qGXjog3#D;sKyb#+>HNwZAz!c(D~Ub>*n(J<>uw)KU(UR5_${( E0C|!QdjJ3c diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json deleted file mode 100644 index 3ccdee6bdb5eb..0000000000000 --- a/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json +++ /dev/null @@ -1,3577 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - "thread-data": { - "is_write_index": false - }, - "beats": { - }, - "siem-read-alias": { - } - }, - "index": "threat-data-001", - "mappings": { - "_meta": { - "beat": "auditbeat", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "auditd": { - "properties": { - "data": { - "properties": { - "a0": { - "ignore_above": 1024, - "type": "keyword" - }, - "a1": { - "ignore_above": 1024, - "type": "keyword" - }, - "a2": { - "ignore_above": 1024, - "type": "keyword" - }, - "a3": { - "ignore_above": 1024, - "type": "keyword" - }, - "a[0-3]": { - "ignore_above": 1024, - "type": "keyword" - }, - "acct": { - "ignore_above": 1024, - "type": "keyword" - }, - "acl": { - "ignore_above": 1024, - "type": "keyword" - }, - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "added": { - "ignore_above": 1024, - "type": "keyword" - }, - "addr": { - "ignore_above": 1024, - "type": "keyword" - }, - "apparmor": { - "ignore_above": 1024, - "type": "keyword" - }, - "arch": { - "ignore_above": 1024, - "type": "keyword" - }, - "argc": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_backlog_limit": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_backlog_wait_time": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_enabled": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_failure": { - "ignore_above": 1024, - "type": "keyword" - }, - "banners": { - "ignore_above": 1024, - "type": "keyword" - }, - "bool": { - "ignore_above": 1024, - "type": "keyword" - }, - "bus": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fe": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fi": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fp": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fver": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "capability": { - "ignore_above": 1024, - "type": "keyword" - }, - "cgroup": { - "ignore_above": 1024, - "type": "keyword" - }, - "changed": { - "ignore_above": 1024, - "type": "keyword" - }, - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "cmd": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "compat": { - "ignore_above": 1024, - "type": "keyword" - }, - "daddr": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "default-context": { - "ignore_above": 1024, - "type": "keyword" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "dir": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "dmac": { - "ignore_above": 1024, - "type": "keyword" - }, - "dport": { - "ignore_above": 1024, - "type": "keyword" - }, - "enforcing": { - "ignore_above": 1024, - "type": "keyword" - }, - "entries": { - "ignore_above": 1024, - "type": "keyword" - }, - "exit": { - "ignore_above": 1024, - "type": "keyword" - }, - "fam": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "fd": { - "ignore_above": 1024, - "type": "keyword" - }, - "fe": { - "ignore_above": 1024, - "type": "keyword" - }, - "feature": { - "ignore_above": 1024, - "type": "keyword" - }, - "fi": { - "ignore_above": 1024, - "type": "keyword" - }, - "file": { - "ignore_above": 1024, - "type": "keyword" - }, - "flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "format": { - "ignore_above": 1024, - "type": "keyword" - }, - "fp": { - "ignore_above": 1024, - "type": "keyword" - }, - "fver": { - "ignore_above": 1024, - "type": "keyword" - }, - "grantors": { - "ignore_above": 1024, - "type": "keyword" - }, - "grp": { - "ignore_above": 1024, - "type": "keyword" - }, - "hook": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "icmp_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "igid": { - "ignore_above": 1024, - "type": "keyword" - }, - "img-ctx": { - "ignore_above": 1024, - "type": "keyword" - }, - "info": { - "ignore_above": 1024, - "type": "keyword" - }, - "inif": { - "ignore_above": 1024, - "type": "keyword" - }, - "ino": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode_gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode_uid": { - "ignore_above": 1024, - "type": "keyword" - }, - "invalid_context": { - "ignore_above": 1024, - "type": "keyword" - }, - "ioctlcmd": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "ignore_above": 1024, - "type": "keyword" - }, - "ipid": { - "ignore_above": 1024, - "type": "keyword" - }, - "ipx-net": { - "ignore_above": 1024, - "type": "keyword" - }, - "items": { - "ignore_above": 1024, - "type": "keyword" - }, - "iuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "ksize": { - "ignore_above": 1024, - "type": "keyword" - }, - "laddr": { - "ignore_above": 1024, - "type": "keyword" - }, - "len": { - "ignore_above": 1024, - "type": "keyword" - }, - "list": { - "ignore_above": 1024, - "type": "keyword" - }, - "lport": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "macproto": { - "ignore_above": 1024, - "type": "keyword" - }, - "maj": { - "ignore_above": 1024, - "type": "keyword" - }, - "major": { - "ignore_above": 1024, - "type": "keyword" - }, - "minor": { - "ignore_above": 1024, - "type": "keyword" - }, - "model": { - "ignore_above": 1024, - "type": "keyword" - }, - "msg": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "nargs": { - "ignore_above": 1024, - "type": "keyword" - }, - "net": { - "ignore_above": 1024, - "type": "keyword" - }, - "new": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-chardev": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-disk": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-enabled": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-fs": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-level": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-log_passwd": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-mem": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-net": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-range": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-rng": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-role": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-seuser": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-vcpu": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_lock": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "nlnk-fam": { - "ignore_above": 1024, - "type": "keyword" - }, - "nlnk-grp": { - "ignore_above": 1024, - "type": "keyword" - }, - "nlnk-pid": { - "ignore_above": 1024, - "type": "keyword" - }, - "oauid": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_uid": { - "ignore_above": 1024, - "type": "keyword" - }, - "ocomm": { - "ignore_above": 1024, - "type": "keyword" - }, - "oflag": { - "ignore_above": 1024, - "type": "keyword" - }, - "old": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-auid": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-chardev": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-disk": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-enabled": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-fs": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-level": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-log_passwd": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-mem": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-net": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-range": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-rng": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-role": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-ses": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-seuser": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-vcpu": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_enforcing": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_lock": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pa": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_prom": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_val": { - "ignore_above": 1024, - "type": "keyword" - }, - "op": { - "ignore_above": 1024, - "type": "keyword" - }, - "operation": { - "ignore_above": 1024, - "type": "keyword" - }, - "opid": { - "ignore_above": 1024, - "type": "keyword" - }, - "oses": { - "ignore_above": 1024, - "type": "keyword" - }, - "outif": { - "ignore_above": 1024, - "type": "keyword" - }, - "pa": { - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "per": { - "ignore_above": 1024, - "type": "keyword" - }, - "perm": { - "ignore_above": 1024, - "type": "keyword" - }, - "perm_mask": { - "ignore_above": 1024, - "type": "keyword" - }, - "permissive": { - "ignore_above": 1024, - "type": "keyword" - }, - "pfs": { - "ignore_above": 1024, - "type": "keyword" - }, - "pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "printer": { - "ignore_above": 1024, - "type": "keyword" - }, - "profile": { - "ignore_above": 1024, - "type": "keyword" - }, - "prom": { - "ignore_above": 1024, - "type": "keyword" - }, - "proto": { - "ignore_above": 1024, - "type": "keyword" - }, - "qbytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "range": { - "ignore_above": 1024, - "type": "keyword" - }, - "reason": { - "ignore_above": 1024, - "type": "keyword" - }, - "removed": { - "ignore_above": 1024, - "type": "keyword" - }, - "res": { - "ignore_above": 1024, - "type": "keyword" - }, - "resrc": { - "ignore_above": 1024, - "type": "keyword" - }, - "rport": { - "ignore_above": 1024, - "type": "keyword" - }, - "sauid": { - "ignore_above": 1024, - "type": "keyword" - }, - "scontext": { - "ignore_above": 1024, - "type": "keyword" - }, - "selected-context": { - "ignore_above": 1024, - "type": "keyword" - }, - "seperm": { - "ignore_above": 1024, - "type": "keyword" - }, - "seperms": { - "ignore_above": 1024, - "type": "keyword" - }, - "seqno": { - "ignore_above": 1024, - "type": "keyword" - }, - "seresult": { - "ignore_above": 1024, - "type": "keyword" - }, - "ses": { - "ignore_above": 1024, - "type": "keyword" - }, - "seuser": { - "ignore_above": 1024, - "type": "keyword" - }, - "sig": { - "ignore_above": 1024, - "type": "keyword" - }, - "sigev_signo": { - "ignore_above": 1024, - "type": "keyword" - }, - "smac": { - "ignore_above": 1024, - "type": "keyword" - }, - "socket": { - "properties": { - "addr": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "ignore_above": 1024, - "type": "keyword" - }, - "saddr": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "spid": { - "ignore_above": 1024, - "type": "keyword" - }, - "sport": { - "ignore_above": 1024, - "type": "keyword" - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "subj": { - "ignore_above": 1024, - "type": "keyword" - }, - "success": { - "ignore_above": 1024, - "type": "keyword" - }, - "syscall": { - "ignore_above": 1024, - "type": "keyword" - }, - "table": { - "ignore_above": 1024, - "type": "keyword" - }, - "tclass": { - "ignore_above": 1024, - "type": "keyword" - }, - "tcontext": { - "ignore_above": 1024, - "type": "keyword" - }, - "terminal": { - "ignore_above": 1024, - "type": "keyword" - }, - "tty": { - "ignore_above": 1024, - "type": "keyword" - }, - "unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "uri": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "val": { - "ignore_above": 1024, - "type": "keyword" - }, - "ver": { - "ignore_above": 1024, - "type": "keyword" - }, - "virt": { - "ignore_above": 1024, - "type": "keyword" - }, - "vm": { - "ignore_above": 1024, - "type": "keyword" - }, - "vm-ctx": { - "ignore_above": 1024, - "type": "keyword" - }, - "vm-pid": { - "ignore_above": 1024, - "type": "keyword" - }, - "watch": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "paths": { - "properties": { - "cap_fe": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fi": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fp": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fver": { - "ignore_above": 1024, - "type": "keyword" - }, - "dev": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "item": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "nametype": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_role": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_user": { - "ignore_above": 1024, - "type": "keyword" - }, - "objtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "ogid": { - "ignore_above": 1024, - "type": "keyword" - }, - "ouid": { - "ignore_above": 1024, - "type": "keyword" - }, - "rdev": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "sequence": { - "type": "long" - }, - "session": { - "ignore_above": 1024, - "type": "keyword" - }, - "summary": { - "properties": { - "actor": { - "properties": { - "primary": { - "ignore_above": 1024, - "type": "keyword" - }, - "secondary": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "how": { - "ignore_above": 1024, - "type": "keyword" - }, - "object": { - "properties": { - "primary": { - "ignore_above": 1024, - "type": "keyword" - }, - "secondary": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "client": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "instance": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "container": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "origin": { - "fields": { - "raw": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "selinux": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "role": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "setgid": { - "type": "boolean" - }, - "setuid": { - "type": "boolean" - }, - "size": { - "type": "long" - }, - "target_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "flow": { - "properties": { - "complete": { - "type": "boolean" - }, - "final": { - "type": "boolean" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geoip": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "blake2b_256": { - "ignore_above": 1024, - "type": "keyword" - }, - "blake2b_384": { - "ignore_above": 1024, - "type": "keyword" - }, - "blake2b_512": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha224": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha384": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_224": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_384": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_512": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512_224": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512_256": { - "ignore_above": 1024, - "type": "keyword" - }, - "xxh64": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "jolokia": { - "properties": { - "agent": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "secured": { - "type": "boolean" - }, - "server": { - "properties": { - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "properties": { - "annotations": { - "type": "object" - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "type": "object" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "observer": { - "properties": { - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "sha1": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "working_directory": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "ip": { - "type": "ip" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "socket": { - "properties": { - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "system": { - "properties": { - "audit": { - "properties": { - "host": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "boottime": { - "type": "date" - }, - "containerized": { - "type": "boolean" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timezone": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "offset": { - "properties": { - "sec": { - "type": "long" - } - } - } - } - }, - "uptime": { - "type": "long" - } - } - }, - "newsocket": { - "properties": { - "egid": { - "type": "long" - }, - "euid": { - "type": "long" - }, - "gid": { - "type": "long" - }, - "internal_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel_sock_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "type": "long" - } - } - }, - "package": { - "properties": { - "arch": { - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "installtime": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "release": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "summary": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "socket": { - "properties": { - "egid": { - "type": "long" - }, - "euid": { - "type": "long" - }, - "gid": { - "type": "long" - }, - "internal_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel_sock_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "type": "long" - } - } - }, - "user": { - "properties": { - "dir": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "properties": { - "last_changed": { - "type": "date" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "shell": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - }, - "user_information": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "properties": { - "audit": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "effective": { - "properties": { - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "filesystem": { - "properties": { - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "name_map": { - "type": "object" - }, - "saved": { - "properties": { - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "selinux": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "role": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "terminal": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "lifecycle": { - "indexing_complete": "true", - "name": "auditbeat-8.0.0", - "rollover_alias": "auditbeat-8.0.0" - }, - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "client.address", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.user.email", - "client.user.full_name", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "ecs.version", - "error.code", - "error.id", - "error.message", - "event.action", - "event.category", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.timezone", - "event.type", - "file.device", - "file.extension", - "file.gid", - "file.group", - "file.inode", - "file.mode", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.id", - "group.name", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.email", - "host.user.full_name", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.original", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "process.args", - "process.executable", - "process.name", - "process.title", - "process.working_directory", - "server.address", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.user.email", - "server.user.full_name", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.user.email", - "source.user.full_name", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "url.domain", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.scheme", - "url.username", - "user.email", - "user.full_name", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "agent.hostname", - "error.type", - "cloud.project.id", - "host.os.build", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "jolokia.agent.version", - "jolokia.agent.id", - "jolokia.server.product", - "jolokia.server.version", - "jolokia.server.vendor", - "jolokia.url", - "raw", - "file.origin", - "file.selinux.user", - "file.selinux.role", - "file.selinux.domain", - "file.selinux.level", - "user.audit.id", - "user.audit.name", - "user.effective.id", - "user.effective.name", - "user.effective.group.id", - "user.effective.group.name", - "user.filesystem.id", - "user.filesystem.name", - "user.filesystem.group.id", - "user.filesystem.group.name", - "user.saved.id", - "user.saved.name", - "user.saved.group.id", - "user.saved.group.name", - "user.selinux.user", - "user.selinux.role", - "user.selinux.domain", - "user.selinux.level", - "user.selinux.category", - "source.path", - "destination.path", - "auditd.message_type", - "auditd.session", - "auditd.result", - "auditd.summary.actor.primary", - "auditd.summary.actor.secondary", - "auditd.summary.object.type", - "auditd.summary.object.primary", - "auditd.summary.object.secondary", - "auditd.summary.how", - "auditd.paths.inode", - "auditd.paths.dev", - "auditd.paths.obj_user", - "auditd.paths.obj_role", - "auditd.paths.obj_domain", - "auditd.paths.obj_level", - "auditd.paths.objtype", - "auditd.paths.ouid", - "auditd.paths.rdev", - "auditd.paths.nametype", - "auditd.paths.ogid", - "auditd.paths.item", - "auditd.paths.mode", - "auditd.paths.name", - "auditd.data.action", - "auditd.data.minor", - "auditd.data.acct", - "auditd.data.addr", - "auditd.data.cipher", - "auditd.data.id", - "auditd.data.entries", - "auditd.data.kind", - "auditd.data.ksize", - "auditd.data.spid", - "auditd.data.arch", - "auditd.data.argc", - "auditd.data.major", - "auditd.data.unit", - "auditd.data.table", - "auditd.data.terminal", - "auditd.data.grantors", - "auditd.data.direction", - "auditd.data.op", - "auditd.data.tty", - "auditd.data.syscall", - "auditd.data.data", - "auditd.data.family", - "auditd.data.mac", - "auditd.data.pfs", - "auditd.data.items", - "auditd.data.a0", - "auditd.data.a1", - "auditd.data.a2", - "auditd.data.a3", - "auditd.data.hostname", - "auditd.data.lport", - "auditd.data.rport", - "auditd.data.exit", - "auditd.data.fp", - "auditd.data.laddr", - "auditd.data.sport", - "auditd.data.capability", - "auditd.data.nargs", - "auditd.data.new-enabled", - "auditd.data.audit_backlog_limit", - "auditd.data.dir", - "auditd.data.cap_pe", - "auditd.data.model", - "auditd.data.new_pp", - "auditd.data.old-enabled", - "auditd.data.oauid", - "auditd.data.old", - "auditd.data.banners", - "auditd.data.feature", - "auditd.data.vm-ctx", - "auditd.data.opid", - "auditd.data.seperms", - "auditd.data.seresult", - "auditd.data.new-rng", - "auditd.data.old-net", - "auditd.data.sigev_signo", - "auditd.data.ino", - "auditd.data.old_enforcing", - "auditd.data.old-vcpu", - "auditd.data.range", - "auditd.data.res", - "auditd.data.added", - "auditd.data.fam", - "auditd.data.nlnk-pid", - "auditd.data.subj", - "auditd.data.a[0-3]", - "auditd.data.cgroup", - "auditd.data.kernel", - "auditd.data.ocomm", - "auditd.data.new-net", - "auditd.data.permissive", - "auditd.data.class", - "auditd.data.compat", - "auditd.data.fi", - "auditd.data.changed", - "auditd.data.msg", - "auditd.data.dport", - "auditd.data.new-seuser", - "auditd.data.invalid_context", - "auditd.data.dmac", - "auditd.data.ipx-net", - "auditd.data.iuid", - "auditd.data.macproto", - "auditd.data.obj", - "auditd.data.ipid", - "auditd.data.new-fs", - "auditd.data.vm-pid", - "auditd.data.cap_pi", - "auditd.data.old-auid", - "auditd.data.oses", - "auditd.data.fd", - "auditd.data.igid", - "auditd.data.new-disk", - "auditd.data.parent", - "auditd.data.len", - "auditd.data.oflag", - "auditd.data.uuid", - "auditd.data.code", - "auditd.data.nlnk-grp", - "auditd.data.cap_fp", - "auditd.data.new-mem", - "auditd.data.seperm", - "auditd.data.enforcing", - "auditd.data.new-chardev", - "auditd.data.old-rng", - "auditd.data.outif", - "auditd.data.cmd", - "auditd.data.hook", - "auditd.data.new-level", - "auditd.data.sauid", - "auditd.data.sig", - "auditd.data.audit_backlog_wait_time", - "auditd.data.printer", - "auditd.data.old-mem", - "auditd.data.perm", - "auditd.data.old_pi", - "auditd.data.state", - "auditd.data.format", - "auditd.data.new_gid", - "auditd.data.tcontext", - "auditd.data.maj", - "auditd.data.watch", - "auditd.data.device", - "auditd.data.grp", - "auditd.data.bool", - "auditd.data.icmp_type", - "auditd.data.new_lock", - "auditd.data.old_prom", - "auditd.data.acl", - "auditd.data.ip", - "auditd.data.new_pi", - "auditd.data.default-context", - "auditd.data.inode_gid", - "auditd.data.new-log_passwd", - "auditd.data.new_pe", - "auditd.data.selected-context", - "auditd.data.cap_fver", - "auditd.data.file", - "auditd.data.net", - "auditd.data.virt", - "auditd.data.cap_pp", - "auditd.data.old-range", - "auditd.data.resrc", - "auditd.data.new-range", - "auditd.data.obj_gid", - "auditd.data.proto", - "auditd.data.old-disk", - "auditd.data.audit_failure", - "auditd.data.inif", - "auditd.data.vm", - "auditd.data.flags", - "auditd.data.nlnk-fam", - "auditd.data.old-fs", - "auditd.data.old-ses", - "auditd.data.seqno", - "auditd.data.fver", - "auditd.data.qbytes", - "auditd.data.seuser", - "auditd.data.cap_fe", - "auditd.data.new-vcpu", - "auditd.data.old-level", - "auditd.data.old_pp", - "auditd.data.daddr", - "auditd.data.old-role", - "auditd.data.ioctlcmd", - "auditd.data.smac", - "auditd.data.apparmor", - "auditd.data.fe", - "auditd.data.perm_mask", - "auditd.data.ses", - "auditd.data.cap_fi", - "auditd.data.obj_uid", - "auditd.data.reason", - "auditd.data.list", - "auditd.data.old_lock", - "auditd.data.bus", - "auditd.data.old_pe", - "auditd.data.new-role", - "auditd.data.prom", - "auditd.data.uri", - "auditd.data.audit_enabled", - "auditd.data.old-log_passwd", - "auditd.data.old-seuser", - "auditd.data.per", - "auditd.data.scontext", - "auditd.data.tclass", - "auditd.data.ver", - "auditd.data.new", - "auditd.data.val", - "auditd.data.img-ctx", - "auditd.data.old-chardev", - "auditd.data.old_val", - "auditd.data.success", - "auditd.data.inode_uid", - "auditd.data.removed", - "auditd.data.socket.port", - "auditd.data.socket.saddr", - "auditd.data.socket.addr", - "auditd.data.socket.family", - "auditd.data.socket.path", - "geoip.continent_name", - "geoip.city_name", - "geoip.region_name", - "geoip.country_iso_code", - "hash.blake2b_256", - "hash.blake2b_384", - "hash.blake2b_512", - "hash.md5", - "hash.sha1", - "hash.sha224", - "hash.sha256", - "hash.sha384", - "hash.sha3_224", - "hash.sha3_256", - "hash.sha3_384", - "hash.sha3_512", - "hash.sha512", - "hash.sha512_224", - "hash.sha512_256", - "hash.xxh64", - "event.origin", - "user.entity_id", - "user.terminal", - "process.entity_id", - "socket.entity_id", - "system.audit.host.timezone.name", - "system.audit.host.hostname", - "system.audit.host.id", - "system.audit.host.architecture", - "system.audit.host.mac", - "system.audit.host.os.platform", - "system.audit.host.os.name", - "system.audit.host.os.family", - "system.audit.host.os.version", - "system.audit.host.os.kernel", - "system.audit.package.entity_id", - "system.audit.package.name", - "system.audit.package.version", - "system.audit.package.release", - "system.audit.package.arch", - "system.audit.package.license", - "system.audit.package.summary", - "system.audit.package.url", - "system.audit.user.name", - "system.audit.user.uid", - "system.audit.user.gid", - "system.audit.user.dir", - "system.audit.user.shell", - "system.audit.user.user_information", - "system.audit.user.password.type", - "fields.*" - ] - }, - "refresh_interval": "5s" - } - } - } -} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index dfe0444e0bbd4..9573372d02e9c 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -1,12 +1,75 @@ { "type": "doc", "value": { - "id": "_uZE6nwBOpWiDweSth_D", - "index": "threat-indicator-0001", + "id": "84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "filebeat-7.12.0-2021.03.10-000001", "source": { - "@timestamp": "2019-09-01T00:41:06.527Z", + "@timestamp": "2021-03-10T14:51:05.766Z", "agent": { - "threat": "03ccb0ce-f65c-4279-a619-05f1d5bb000b" + "ephemeral_id": "34c78500-8df5-4a07-ba87-1cc738b98431", + "hostname": "test", + "id": "08a3d064-8f23-41f3-84b2-f917f6ff9588", + "name": "test", + "type": "filebeat", + "version": "7.12.0" + }, + "fileset": { + "name": "abusemalware" + }, + "threatintel": { + "indicator": { + "first_seen": "2021-03-10T08:02:14.000Z", + "file": { + "size": 80280, + "pe": {}, + "type": "elf", + "hash": { + "sha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "tlsh": "6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE", + "ssdeep": "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL", + "md5": "9b6c3518a91d23ed77504b5416bfb5b3" + } + }, + "type": "file" + }, + "abusemalware": { + "virustotal": { + "result": "38 / 61", + "link": "https://www.virustotal.com/gui/file/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/detection/f-a04ac6d", + "percent": "62.30" + } + } + }, + "tags": [ + "threatintel-abusemalware", + "forwarded" + ], + "input": { + "type": "httpjson" + }, + "@timestamp": "2021-03-10T14:51:07.663Z", + "ecs": { + "version": "1.6.0" + }, + "related": { + "hash": [ + "9b6c3518a91d23ed77504b5416bfb5b3", + "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL" + ] + }, + "service": { + "type": "threatintel" + }, + "event": { + "reference": "https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/", + "ingested": "2021-03-10T14:51:09.809069Z", + "created": "2021-03-10T14:51:07.663Z", + "kind": "enrichment", + "module": "threatintel", + "category": "threat", + "type": "indicator", + "dataset": "threatintel.abusemalware" } } } diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json index 0c24fa429d908..efd23c5a6bba4 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -2,29 +2,821 @@ "type": "index", "value": { "aliases": { - "threat-indicator": { - "is_write_index": false + "filebeat-7.12.0": { + "is_write_index": true } }, - "index": "threat-indicator-0001", + "index": "filebeat-7.12.0-2021.03.10-000001", "mappings": { + "_meta": { + "beat": "filebeat", + "version": "7.12.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "kubernetes.service.selectors.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.service.selectors.*" + } + }, + { + "docker.attrs": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.attrs.*" + } + }, + { + "azure.activitylogs.identity.claims.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.activitylogs.identity.claims.*" + } + }, + { + "azure.platformlogs.properties.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.platformlogs.properties.*" + } + }, + { + "kibana.log.meta": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "kibana.log.meta.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], "properties": { "@timestamp": { "type": "date" }, "agent": { "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "ephemeral_id": { "ignore_above": 1024, "type": "keyword" }, - "threat": { + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { "ignore_above": 1024, "type": "keyword" } } + }, + "apache": { + "properties": { + "access": { + "properties": { + "ssl": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "module": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "fileset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "threatintel": { + "properties": { + "abusemalware": { + "properties": { + "file_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_download": { + "ignore_above": 1024, + "type": "keyword" + }, + "virustotal": { + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "percent": { + "type": "float" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "abuseurl": { + "properties": { + "blacklists": { + "properties": { + "spamhaus_dbl": { + "ignore_above": 1024, + "type": "keyword" + }, + "surbl": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "larted": { + "type": "boolean" + }, + "reporter": { + "ignore_above": 1024, + "type": "keyword" + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "ignore_above": 1024, + "type": "keyword" + }, + "url_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "anomali": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "labels": { + "ignore_above": 1024, + "type": "keyword" + }, + "modified": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_marking_refs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pattern": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "valid_from": { + "type": "date" + } + } + }, + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + }, + "tlsh": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "imphash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "strings": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "misp": { + "properties": { + "attribute": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "comment": { + "ignore_above": 1024, + "type": "keyword" + }, + "deleted": { + "type": "boolean" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "type": "long" + }, + "event_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_relation": { + "ignore_above": 1024, + "type": "keyword" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "to_ids": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "attribute_count": { + "type": "long" + }, + "date": { + "type": "date" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "ignore_above": 1024, + "type": "keyword" + }, + "extends_uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "info": { + "ignore_above": 1024, + "type": "keyword" + }, + "locked": { + "type": "boolean" + }, + "org": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "org_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "orgc": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "orgc_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "proposal_email_lock": { + "type": "boolean" + }, + "publish_timestamp": { + "type": "date" + }, + "published": { + "type": "boolean" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat_level_id": { + "type": "long" + }, + "timestamp": { + "type": "date" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "otx": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } } } + }, + "settings": { + "index": { + "lifecycle": { + "name": "filebeat", + "rollover_alias": "filebeat-7.12.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } } } } From fad3b74f2f61d2445ade1d63f826e699a36dcc64 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 25 Mar 2021 19:33:16 +0100 Subject: [PATCH 70/88] [Discover] Unskip functional test of saved queries (#94705) --- test/functional/apps/discover/_saved_queries.ts | 14 ++++++++++++-- .../services/saved_query_management_component.ts | 7 +++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 23f3af37bbdf6..9726b097c8f62 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,8 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/89477 - describe.skip('saved queries saved objects', function describeIndexTests() { + describe('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); @@ -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/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`); From 43e3d558fd6d7e50949f6bde95591114d1d182db Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 25 Mar 2021 14:41:58 -0400 Subject: [PATCH 71/88] [Snapshot + Restore] Add callout when restoring snapshot (#95104) --- .../public/doc_links/doc_links_service.ts | 1 + .../helpers/restore_snapshot.helpers.ts | 13 ++++++- .../restore_snapshot.test.ts | 38 ++++++++++++++++--- .../steps/step_settings/step_settings.tsx | 2 +- .../restore_snapshot_form.tsx | 3 +- .../steps/step_logistics/step_logistics.tsx | 38 ++++++++++++++++--- .../system_indices_overwritten_callout.tsx | 29 ++++++++++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6279d62d2c40e..9711d546fc947 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -284,6 +284,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`, diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts index 644ad6ea3089b..c0ffae81a4258 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; import { RestoreSnapshot } from '../../../public/application/sections/restore_snapshot'; @@ -23,11 +24,19 @@ const initTestBed = registerTestBed( ); const setupActions = (testBed: TestBed) => { - const { find } = testBed; + const { find, component, form } = testBed; return { findDataStreamCallout() { return find('dataStreamWarningCallOut'); }, + + toggleGlobalState() { + act(() => { + form.toggleEuiSwitch('includeGlobalStateSwitch'); + }); + + component.update(); + }, }; }; @@ -48,4 +57,6 @@ export const setup = async (): Promise => { export type RestoreSnapshotFormTestSubject = | 'snapshotRestoreStepLogistics' + | 'includeGlobalStateSwitch' + | 'systemIndicesInfoCallOut' | 'dataStreamWarningCallOut'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts index 36cd178060f83..2fecce36f09df 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -4,8 +4,9 @@ * 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 { nextTick, pageHelpers, setupEnvironment } from './helpers'; +import { pageHelpers, setupEnvironment } from './helpers'; import { RestoreSnapshotTestBed } from './helpers/restore_snapshot.helpers'; import * as fixtures from '../../test/fixtures'; @@ -20,11 +21,15 @@ describe('', () => { afterAll(() => { server.restore(); }); + describe('with data streams', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); - testBed = await setup(); - await nextTick(); + + await act(async () => { + testBed = await setup(); + }); + testBed.component.update(); }); @@ -37,8 +42,10 @@ describe('', () => { describe('without data streams', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot({ totalDataStreams: 0 })); - testBed = await setup(); - await nextTick(); + await act(async () => { + testBed = await setup(); + }); + testBed.component.update(); }); @@ -47,4 +54,25 @@ describe('', () => { expect(exists('dataStreamWarningCallOut')).toBe(false); }); }); + + describe('global state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + it('shows an info callout when include_global_state is enabled', () => { + const { exists, actions } = testBed; + + expect(exists('systemIndicesInfoCallOut')).toBe(false); + + actions.toggleGlobalState(); + + expect(exists('systemIndicesInfoCallOut')).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index dcaad024eb0f7..fc230affc980b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -142,7 +142,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ description={ } fullWidth diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx index 4a281b270210c..f672300db8821 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx @@ -112,7 +112,8 @@ export const RestoreSnapshotForm: React.FunctionComponent = ({ errors={validation.errors} updateCurrentStep={updateCurrentStep} /> - + + {saveError ? ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index bb66585579d7d..de30eadad4543 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -7,6 +7,7 @@ import React, { Fragment, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import semverGt from 'semver/functions/gt'; import { EuiButtonEmpty, EuiDescribedFormGroup, @@ -38,6 +39,8 @@ import { DataStreamsGlobalStateCallOut } from './data_streams_global_state_call_ import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; +import { SystemIndicesOverwrittenCallOut } from './system_indices_overwritten_callout'; + export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({ snapshotDetails, restoreSettings, @@ -50,6 +53,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = indices: unfilteredSnapshotIndices, dataStreams: snapshotDataStreams = [], includeGlobalState: snapshotIncludeGlobalState, + version, } = snapshotDetails; const snapshotIndices = unfilteredSnapshotIndices.filter( @@ -564,11 +568,34 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } description={ - + <> + + {i18n.translate( + 'xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink', + { defaultMessage: 'Learn more.' } + )} + + ), + }} + /> + + {/* Only display callout if include_global_state is enabled and the snapshot was created by ES 7.12+ + * Note: Once we support features states in the UI, we will also need to add a check here for that + * See https://github.com/elastic/kibana/issues/95128 more details + */} + {includeGlobalState && semverGt(version, '7.12.0') && ( + <> + + + + )} + } fullWidth > @@ -594,6 +621,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = checked={includeGlobalState === undefined ? false : includeGlobalState} onChange={(e) => updateRestoreSettings({ includeGlobalState: e.target.checked })} disabled={!snapshotIncludeGlobalState} + data-test-subj="includeGlobalStateSwitch" /> diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx new file mode 100644 index 0000000000000..fac21de0bce22 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +export const SystemIndicesOverwrittenCallOut: FunctionComponent = () => { + return ( + + ); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 290ad19718efc..5a6e9a6164cd1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21323,7 +21323,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesToggleListLink": "データストリームとインデックスを選択", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "すべて選択解除", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "スナップショットと復元ドキュメント", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続的な設定も復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c982931f91e13..dce5c3ad85d5a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21661,7 +21661,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesToggleListLink": "选择数据流和索引", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "取消全选", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "快照和还原文档", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态", From 6b6404954edf6cac1610f143da3c2670fbc6c216 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:59:18 +0000 Subject: [PATCH 72/88] [Logs / Metrics UI] Separate logs / metrics source configuration awareness (#95334) * Remove metrics awareness of logs fields --- .../resolve_log_source_configuration.ts | 19 ++ .../infra/common/metrics_sources/index.ts | 81 +++++++++ .../source_configuration.ts} | 165 +++++++++--------- .../inventory/components/expression.test.tsx | 2 +- .../inventory/components/expression.tsx | 5 +- .../components/expression.test.tsx | 2 +- .../metric_anomaly/components/expression.tsx | 5 +- .../components/expression.test.tsx | 2 +- .../components/expression.tsx | 5 +- .../components/expression_chart.test.tsx | 7 +- .../components/expression_chart.tsx | 4 +- .../components/expression_row.test.tsx | 2 +- .../hooks/use_metrics_explorer_chart_data.ts | 4 +- .../components/source_configuration/index.ts | 10 -- .../log_columns_configuration_form_state.tsx | 156 ----------------- .../{source => metrics_source}/index.ts | 0 .../{source => metrics_source}/source.tsx | 36 ++-- .../use_source_via_http.ts | 51 +++--- .../containers/saved_view/saved_view.tsx | 4 +- .../containers/with_source/with_source.tsx | 22 +-- x-pack/plugins/infra/public/lib/lib.ts | 4 +- .../infra/public/metrics_overview_fetchers.ts | 4 +- .../redirect_to_host_detail_via_ip.tsx | 4 +- .../settings/fields_configuration_panel.tsx | 2 +- .../settings/indices_configuration_panel.tsx | 2 +- .../logs/stream/page_no_indices_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 8 +- .../inventory_view/components/layout.tsx | 2 +- .../anomalies_table/anomalies_table.tsx | 2 +- .../anomaly_detection_flyout.tsx | 3 +- .../ml/anomaly_detection/job_setup_screen.tsx | 5 +- .../node_details/tabs/metrics/metrics.tsx | 2 +- .../node_details/tabs/properties/index.tsx | 2 +- .../inventory_view/components/search_bar.tsx | 2 +- .../components/timeline/timeline.tsx | 2 +- .../components/toolbars/toolbar.tsx | 4 +- .../components/toolbars/toolbar_wrapper.tsx | 2 +- .../waffle/conditional_tooltip.test.tsx | 2 +- .../components/waffle/conditional_tooltip.tsx | 2 +- .../inventory_view/hooks/use_process_list.ts | 2 +- .../hooks/use_waffle_filters.test.ts | 2 +- .../hooks/use_waffle_filters.ts | 2 +- .../pages/metrics/inventory_view/index.tsx | 4 +- .../lib/create_uptime_link.test.ts | 1 - .../metric_detail/components/invalid_node.tsx | 2 +- .../pages/metrics/metric_detail/index.tsx | 2 +- .../metrics/metric_detail/page_providers.tsx | 2 +- .../metrics_explorer/components/chart.tsx | 4 +- .../components/chart_context_menu.tsx | 6 +- .../metrics_explorer/components/charts.tsx | 4 +- .../components/helpers/create_tsvb_link.ts | 4 +- .../hooks/use_metric_explorer_state.ts | 4 +- .../hooks/use_metrics_explorer_data.test.tsx | 4 +- .../hooks/use_metrics_explorer_data.ts | 4 +- .../pages/metrics/metrics_explorer/index.tsx | 4 +- .../infra/public/pages/metrics/settings.tsx | 2 +- .../settings}/fields_configuration_panel.tsx | 2 +- .../indices_configuration_form_state.ts | 18 +- .../settings}/indices_configuration_panel.tsx | 4 +- .../settings}/ml_configuration_panel.tsx | 2 +- .../source_configuration_form_state.tsx | 60 ++----- .../source_configuration_settings.tsx | 10 +- .../public/utils/source_configuration.ts | 10 +- x-pack/plugins/infra/server/infra_server.ts | 4 +- .../metrics/kibana_metrics_adapter.ts | 6 +- .../evaluate_condition.ts | 6 +- .../inventory_metric_threshold_executor.ts | 6 + ...review_inventory_metric_threshold_alert.ts | 7 +- .../metric_threshold/lib/evaluate_alert.ts | 2 +- .../preview_metric_threshold_alert.ts | 2 +- .../infra/server/lib/domains/fields_domain.ts | 9 +- .../log_entries_domain/log_entries_domain.ts | 4 +- .../plugins/infra/server/lib/infra_types.ts | 4 +- .../plugins/infra/server/lib/metrics/index.ts | 2 +- .../infra/server/lib/sources/defaults.ts | 2 +- .../plugins/infra/server/lib/sources/index.ts | 2 +- ...0_add_new_indexing_strategy_index_names.ts | 2 +- .../infra/server/lib/sources/sources.ts | 2 +- x-pack/plugins/infra/server/plugin.ts | 4 +- .../infra/server/routes/alerting/preview.ts | 11 +- .../{source => metrics_sources}/index.ts | 60 +++---- .../infra/server/routes/snapshot/index.ts | 8 +- ...alculate_index_pattern_based_on_metrics.ts | 23 --- .../server/routes/snapshot/lib/get_nodes.ts | 70 +++++++- ...ransform_request_to_metrics_api_request.ts | 11 +- .../log_queries/get_log_query_fields.ts | 32 ++++ .../apis/metrics_ui/http_source.ts | 28 +-- .../apis/metrics_ui/sources.ts | 51 +----- .../services/infraops_source_configuration.ts | 15 +- 89 files changed, 538 insertions(+), 632 deletions(-) create mode 100644 x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts create mode 100644 x-pack/plugins/infra/common/metrics_sources/index.ts rename x-pack/plugins/infra/common/{http_api/source_api.ts => source_configuration/source_configuration.ts} (61%) delete mode 100644 x-pack/plugins/infra/public/components/source_configuration/index.ts delete mode 100644 x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx rename x-pack/plugins/infra/public/containers/{source => metrics_source}/index.ts (100%) rename x-pack/plugins/infra/public/containers/{source => metrics_source}/source.tsx (79%) rename x-pack/plugins/infra/public/containers/{source => metrics_source}/use_source_via_http.ts (62%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/fields_configuration_panel.tsx (98%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/indices_configuration_form_state.ts (91%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/indices_configuration_panel.tsx (93%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/ml_configuration_panel.tsx (96%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/source_configuration_form_state.tsx (57%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/source_configuration_settings.tsx (94%) rename x-pack/plugins/infra/server/routes/{source => metrics_sources}/index.ts (69%) delete mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts create mode 100644 x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts 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/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts deleted file mode 100644 index 50db601234a8c..0000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './input_fields'; -export { SourceConfigurationSettings } from './source_configuration_settings'; -export { ViewSourceConfigurationButton } from './view_source_configuration_button'; 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/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/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index e390d6525cd60..921634361f4a2 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 @@ -34,7 +34,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; @@ -112,7 +112,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 +132,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 439764f80186e..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,6 +104,7 @@ const getData = async ( metric: SnapshotMetricType, timerange: InfraTimerangeInput, source: InfraSource, + logQueryFields: LogQueryFields, filterQuery?: string, customMetric?: SnapshotCustomMetricInput ) => { @@ -124,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 f6214edc5d0ab..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'; 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_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/test/api_integration/apis/metrics_ui/http_source.ts b/x-pack/test/api_integration/apis/metrics_ui/http_source.ts index aecff3eaa5cb8..912266bf87e42 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/http_source.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/http_source.ts @@ -15,16 +15,14 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const fetchSource = async (): Promise => { const response = await supertest - .get('/api/metrics/source/default/metrics') + .get('/api/metrics/source/default') .set('kbn-xsrf', 'xxx') .expect(200); return response.body; }; - const fetchHasData = async ( - type: 'logs' | 'metrics' - ): Promise<{ hasData: boolean } | undefined> => { + const fetchHasData = async (): Promise<{ hasData: boolean } | undefined> => { const response = await supertest - .get(`/api/metrics/source/default/${type}/hasData`) + .get(`/api/metrics/source/default/hasData`) .set('kbn-xsrf', 'xxx') .expect(200); return response.body; @@ -34,41 +32,27 @@ export default function ({ getService }: FtrProviderContext) { describe('8.0.0', () => { before(() => esArchiver.load('infra/8.0.0/logs_and_metrics')); after(() => esArchiver.unload('infra/8.0.0/logs_and_metrics')); - describe('/api/metrics/source/default/metrics', () => { + describe('/api/metrics/source/default', () => { it('should just work', async () => { const resp = fetchSource(); return resp.then((data) => { expect(data).to.have.property('source'); expect(data?.source.configuration.metricAlias).to.equal('metrics-*,metricbeat-*'); - expect(data?.source.configuration.logAlias).to.equal( - 'logs-*,filebeat-*,kibana_sample_data_logs*' - ); expect(data?.source.configuration.fields).to.eql({ container: 'container.id', host: 'host.name', - message: ['message', '@message'], pod: 'kubernetes.pod.uid', tiebreaker: '_doc', timestamp: '@timestamp', }); expect(data?.source).to.have.property('status'); expect(data?.source.status?.metricIndicesExist).to.equal(true); - expect(data?.source.status?.logIndicesExist).to.equal(true); }); }); }); - describe('/api/metrics/source/default/metrics/hasData', () => { + describe('/api/metrics/source/default/hasData', () => { it('should just work', async () => { - const resp = fetchHasData('metrics'); - return resp.then((data) => { - expect(data).to.have.property('hasData'); - expect(data?.hasData).to.be(true); - }); - }); - }); - describe('/api/metrics/source/default/logs/hasData', () => { - it('should just work', async () => { - const resp = fetchHasData('logs'); + const resp = fetchHasData(); return resp.then((data) => { expect(data).to.have.property('hasData'); expect(data?.hasData).to.be(true); diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index a5bab8de92f38..d55530a501366 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -8,10 +8,10 @@ import expect from '@kbn/expect'; import { - SourceResponse, - InfraSavedSourceConfiguration, - SourceResponseRuntimeType, -} from '../../../../plugins/infra/common/http_api/source_api'; + MetricsSourceConfigurationResponse, + PartialMetricsSourceConfigurationProperties, + metricsSourceConfigurationResponseRT, +} from '../../../../plugins/infra/common/metrics_sources'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -19,8 +19,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const SOURCE_API_URL = '/api/metrics/source/default'; const patchRequest = async ( - body: InfraSavedSourceConfiguration - ): Promise => { + body: PartialMetricsSourceConfigurationProperties + ): Promise => { const response = await supertest .patch(SOURCE_API_URL) .set('kbn-xsrf', 'xxx') @@ -51,10 +51,9 @@ export default function ({ getService }: FtrProviderContext) { name: 'UPDATED_NAME', description: 'UPDATED_DESCRIPTION', metricAlias: 'metricbeat-**', - logAlias: 'filebeat-**', }); - expect(SourceResponseRuntimeType.is(updateResponse)).to.be(true); + expect(metricsSourceConfigurationResponseRT.is(updateResponse)).to.be(true); const version = updateResponse?.source.version; const updatedAt = updateResponse?.source.updatedAt; @@ -67,15 +66,12 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration?.name).to.be('UPDATED_NAME'); expect(configuration?.description).to.be('UPDATED_DESCRIPTION'); expect(configuration?.metricAlias).to.be('metricbeat-**'); - expect(configuration?.logAlias).to.be('filebeat-**'); expect(configuration?.fields.host).to.be('host.name'); expect(configuration?.fields.pod).to.be('kubernetes.pod.uid'); expect(configuration?.fields.tiebreaker).to.be('_doc'); expect(configuration?.fields.timestamp).to.be('@timestamp'); expect(configuration?.fields.container).to.be('container.id'); - expect(configuration?.logColumns).to.have.length(3); expect(configuration?.anomalyThreshold).to.be(50); - expect(status?.logIndicesExist).to.be(true); expect(status?.metricIndicesExist).to.be(true); }); @@ -105,8 +101,6 @@ export default function ({ getService }: FtrProviderContext) { expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt || 0); expect(configuration?.metricAlias).to.be('metricbeat-**'); - expect(configuration?.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); - expect(status?.logIndicesExist).to.be(true); expect(status?.metricIndicesExist).to.be(true); }); @@ -144,37 +138,6 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration?.fields.timestamp).to.be('@timestamp'); }); - it('applies a log column update to an existing source', async () => { - const creationResponse = await patchRequest({ - name: 'NAME', - }); - - const initialVersion = creationResponse?.source.version; - const createdAt = creationResponse?.source.updatedAt; - - const updateResponse = await patchRequest({ - logColumns: [ - { - fieldColumn: { - id: 'ADDED_COLUMN_ID', - field: 'ADDED_COLUMN_FIELD', - }, - }, - ], - }); - - const version = updateResponse?.source.version; - const updatedAt = updateResponse?.source.updatedAt; - const configuration = updateResponse?.source.configuration; - expect(version).to.be.a('string'); - expect(version).to.not.be(initialVersion); - expect(updatedAt).to.be.greaterThan(createdAt || 0); - expect(configuration?.logColumns).to.have.length(1); - expect(configuration?.logColumns[0]).to.have.key('fieldColumn'); - const fieldColumn = (configuration?.logColumns[0] as any).fieldColumn; - expect(fieldColumn).to.have.property('id', 'ADDED_COLUMN_ID'); - expect(fieldColumn).to.have.property('field', 'ADDED_COLUMN_FIELD'); - }); it('validates anomalyThreshold is between range 1-100', async () => { // create config with bad request await supertest diff --git a/x-pack/test/api_integration/services/infraops_source_configuration.ts b/x-pack/test/api_integration/services/infraops_source_configuration.ts index 5c1566827b701..f78cc880a1d17 100644 --- a/x-pack/test/api_integration/services/infraops_source_configuration.ts +++ b/x-pack/test/api_integration/services/infraops_source_configuration.ts @@ -6,17 +6,17 @@ */ import { - InfraSavedSourceConfiguration, - SourceResponse, -} from '../../../plugins/infra/common/http_api/source_api'; + PartialMetricsSourceConfiguration, + MetricsSourceConfigurationResponse, +} from '../../../plugins/infra/common/metrics_sources'; import { FtrProviderContext } from '../ftr_provider_context'; export function InfraOpsSourceConfigurationProvider({ getService }: FtrProviderContext) { const log = getService('log'); const supertest = getService('supertest'); const patchRequest = async ( - body: InfraSavedSourceConfiguration - ): Promise => { + body: PartialMetricsSourceConfiguration + ): Promise => { const response = await supertest .patch('/api/metrics/source/default') .set('kbn-xsrf', 'xxx') @@ -26,7 +26,10 @@ export function InfraOpsSourceConfigurationProvider({ getService }: FtrProviderC }; return { - async createConfiguration(sourceId: string, sourceProperties: InfraSavedSourceConfiguration) { + async createConfiguration( + sourceId: string, + sourceProperties: PartialMetricsSourceConfiguration + ) { log.debug( `Creating Infra UI source configuration "${sourceId}" with properties ${JSON.stringify( sourceProperties From c5e3e78de8469abde694b5644137868ae39ae1c7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Mar 2021 13:00:16 -0600 Subject: [PATCH 73/88] [Maps] do not track total hits for elasticsearch search requests (#91754) * [Maps] do not track total hits for elasticsearch search requests * set track_total_hits for es_search_source tooltip fetch * tslint * searchSource doc updates, set track_total_hits in MVT requests * revert changes made to searchsourcefields docs * tslint * review feedback * tslint * remove Hits (Total) from functional tests * remove sleep in functional test * tslint * fix method name Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../maps/common/elasticsearch_util/index.ts | 1 + .../elasticsearch_util/total_hits.test.ts | 48 +++++++++++++++ .../common/elasticsearch_util/total_hits.ts | 33 +++++++++++ .../blended_vector_layer.ts | 10 +++- .../es_geo_grid_source/es_geo_grid_source.tsx | 1 + .../es_geo_line_source/es_geo_line_source.tsx | 2 + .../es_pew_pew_source/es_pew_pew_source.js | 7 ++- .../es_search_source/es_search_source.tsx | 11 ++-- .../classes/sources/es_source/es_source.ts | 8 ++- .../sources/es_term_source/es_term_source.ts | 1 + x-pack/plugins/maps/server/mvt/get_tile.ts | 24 +++++++- .../apps/maps/blended_vector_layer.js | 19 ++---- .../maps/documents_source/docvalue_fields.js | 18 +----- .../apps/maps/embeddable/dashboard.js | 33 ++++------- .../apps/maps/es_geo_grid_source.js | 58 ++++--------------- .../functional/apps/maps/es_pew_pew_source.js | 12 +--- x-pack/test/functional/apps/maps/joins.js | 22 ++----- .../test/functional/page_objects/gis_page.ts | 27 +++++++++ 18 files changed, 198 insertions(+), 137 deletions(-) create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts diff --git a/x-pack/plugins/maps/common/elasticsearch_util/index.ts b/x-pack/plugins/maps/common/elasticsearch_util/index.ts index 0b6eaa435264c..24dd56b217401 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/index.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/index.ts @@ -8,3 +8,4 @@ export * from './es_agg_utils'; export * from './convert_to_geojson'; export * from './elasticsearch_geo_utils'; +export { isTotalHitsGreaterThan, TotalHits } from './total_hits'; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts new file mode 100644 index 0000000000000..211cb2d302f2c --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isTotalHitsGreaterThan, TotalHits } from './total_hits'; + +describe('total.relation: eq', () => { + const totalHits = { + value: 100, + relation: 'eq' as TotalHits['relation'], + }; + + test('total.value: 100 should be more than 90', () => { + expect(isTotalHitsGreaterThan(totalHits, 90)).toBe(true); + }); + + test('total.value: 100 should not be more than 100', () => { + expect(isTotalHitsGreaterThan(totalHits, 100)).toBe(false); + }); + + test('total.value: 100 should not be more than 110', () => { + expect(isTotalHitsGreaterThan(totalHits, 110)).toBe(false); + }); +}); + +describe('total.relation: gte', () => { + const totalHits = { + value: 100, + relation: 'gte' as TotalHits['relation'], + }; + + test('total.value: 100 should be more than 90', () => { + expect(isTotalHitsGreaterThan(totalHits, 90)).toBe(true); + }); + + test('total.value: 100 should be more than 100', () => { + expect(isTotalHitsGreaterThan(totalHits, 100)).toBe(true); + }); + + test('total.value: 100 should throw error when value is more than 100', () => { + expect(() => { + isTotalHitsGreaterThan(totalHits, 110); + }).toThrow(); + }); +}); diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts new file mode 100644 index 0000000000000..5de38d3f28851 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export interface TotalHits { + value: number; + relation: 'eq' | 'gte'; +} + +export function isTotalHitsGreaterThan(totalHits: TotalHits, value: number) { + if (totalHits.relation === 'eq') { + return totalHits.value > value; + } + + if (value > totalHits.value) { + throw new Error( + i18n.translate('xpack.maps.totalHits.lowerBoundPrecisionExceeded', { + defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower then value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, + values: { + totalHitsString: JSON.stringify(totalHits, null, ''), + value, + }, + }) + ); + } + + return true; +} diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index d795315acbf50..6dd454137be7d 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -22,6 +22,7 @@ import { LAYER_STYLE_TYPE, FIELD_ORIGIN, } from '../../../../common/constants'; +import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { IESSource } from '../../sources/es_source'; @@ -323,13 +324,18 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { syncContext.startLoading(dataRequestId, requestToken, searchFilters); const abortController = new AbortController(); syncContext.registerCancelCallback(requestToken, () => abortController.abort()); + const maxResultWindow = await this._documentSource.getMaxResultWindow(); const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', maxResultWindow + 1); const resp = await searchSource.fetch({ abortSignal: abortController.signal, sessionId: syncContext.dataFilters.searchSessionId, + legacyHitsTotal: false, }); - const maxResultWindow = await this._documentSource.getMaxResultWindow(); - isSyncClustered = resp.hits.total > maxResultWindow; + isSyncClustered = isTotalHitsGreaterThan( + (resp.hits.total as unknown) as TotalHits, + maxResultWindow + ); const countData = { isSyncClustered } as CountData; syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); } catch (error) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 4715398dab97b..7910e931e60e6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -368,6 +368,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle ): Promise { const indexPattern: IndexPattern = await this.getIndexPattern(); const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); let bucketsPerGrid = 1; this.getMetricFields().forEach((metricField) => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index c652935d7188a..9a1f23e055af1 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -190,6 +190,7 @@ export class ESGeoLineSource extends AbstractESAggSource { // Fetch entities // const entitySearchSource = await this.makeSearchSource(searchFilters, 0); + entitySearchSource.setField('trackTotalHits', false); const splitField = getField(indexPattern, this._descriptor.splitField); const cardinalityAgg = { precision_threshold: 1 }; const termsAgg = { size: MAX_TRACKS }; @@ -250,6 +251,7 @@ export class ESGeoLineSource extends AbstractESAggSource { const tracksSearchFilters = { ...searchFilters }; delete tracksSearchFilters.buffer; const tracksSearchSource = await this.makeSearchSource(tracksSearchFilters, 0); + tracksSearchSource.setField('trackTotalHits', false); tracksSearchSource.setField('aggs', { tracks: { filters: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index e3ee9599d86a9..781cc7f8c36b0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -109,6 +109,7 @@ export class ESPewPewSource extends AbstractESAggSource { async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { destSplit: { terms: { @@ -168,6 +169,7 @@ export class ESPewPewSource extends AbstractESAggSource { async getBoundsForFilters(boundsFilters, registerCancelCallback) { const searchSource = await this.makeSearchSource(boundsFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { destFitToBounds: { geo_bounds: { @@ -185,7 +187,10 @@ export class ESPewPewSource extends AbstractESAggSource { try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); + const esResp = await searchSource.fetch({ + abortSignal: abortController.signal, + legacyHitsTotal: false, + }); if (esResp.aggregations.destFitToBounds.bounds) { corners.push([ esResp.aggregations.destFitToBounds.bounds.top_left.lon, diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 3b6a7202691b6..eae00710c4c25 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -18,6 +18,7 @@ import { addFieldToDSL, getField, hitsToGeoJson, + isTotalHitsGreaterThan, PreIndexedShape, } from '../../../../common/elasticsearch_util'; // @ts-expect-error @@ -313,6 +314,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }; const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { totalEntities: { cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), @@ -343,11 +345,10 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye const areEntitiesTrimmed = entityBuckets.length >= DEFAULT_MAX_BUCKETS_LIMIT; let areTopHitsTrimmed = false; entityBuckets.forEach((entityBucket: any) => { - const total = _.get(entityBucket, 'entityHits.hits.total', 0); const hits = _.get(entityBucket, 'entityHits.hits.hits', []); // Reverse hits list so top documents by sort are drawn on top allHits.push(...hits.reverse()); - if (total > hits.length) { + if (isTotalHitsGreaterThan(entityBucket.entityHits.hits.total, hits.length)) { areTopHitsTrimmed = true; } }); @@ -385,6 +386,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye maxResultWindow, initialSearchContext ); + searchSource.setField('trackTotalHits', maxResultWindow + 1); searchSource.setField('fieldsFromSource', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields if (sourceOnlyFields.length === 0) { searchSource.setField('source', false); // do not need anything from _source @@ -408,7 +410,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top meta: { resultsCount: resp.hits.hits.length, - areResultsTrimmed: resp.hits.total > resp.hits.hits.length, + areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length), }, }; } @@ -508,6 +510,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source const searchService = getSearchService(); const searchSource = await searchService.searchSource.create(initialSearchContext as object); + searchSource.setField('trackTotalHits', false); searchSource.setField('index', indexPattern); searchSource.setField('size', 1); @@ -520,7 +523,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSource.setField('query', query); searchSource.setField('fieldsFromSource', this._getTooltipPropertyNames()); - const resp = await searchSource.fetch(); + const resp = await searchSource.fetch({ legacyHitsTotal: false }); const hit = _.get(resp, 'hits.hits[0]'); if (!hit) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index c55a564951c4e..222c49abfa16a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -195,6 +195,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource resp = await searchSource.fetch({ abortSignal: abortController.signal, sessionId: searchSessionId, + legacyHitsTotal: false, }); if (inspectorRequest) { const responseStats = search.getResponseInspectorStats(resp, searchSource); @@ -247,6 +248,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource } } const searchService = getSearchService(); + const searchSource = await searchService.searchSource.create(initialSearchContext); searchSource.setField('index', indexPattern); @@ -272,6 +274,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource registerCancelCallback: (callback: () => void) => void ): Promise { const searchSource = await this.makeSearchSource(boundsFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { fitToBounds: { geo_bounds: { @@ -284,7 +287,10 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); + const esResp = await searchSource.fetch({ + abortSignal: abortController.signal, + legacyHitsTotal: false, + }); if (!esResp.aggregations) { return null; diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 5c41971fb629c..caae4385aeec6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -127,6 +127,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource const indexPattern = await this.getIndexPattern(); const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); const termsField = getField(indexPattern, this._termField.getName()); const termsAgg = { size: this._descriptor.size !== undefined ? this._descriptor.size : DEFAULT_MAX_BUCKETS_LIMIT, diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index d6ebf2fb216b2..95b8e043e0ce4 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -23,7 +23,12 @@ import { SUPER_FINE_ZOOM_DELTA, } from '../../common/constants'; -import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; +import { + convertRegularRespToGeoJson, + hitsToGeoJson, + isTotalHitsGreaterThan, + TotalHits, +} from '../../common/elasticsearch_util'; import { flattenHit } from './util'; import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; @@ -67,6 +72,7 @@ export async function getGridTile({ MAX_ZOOM ); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; + requestBody.track_total_hits = false; const response = await context .search!.search( @@ -78,6 +84,7 @@ export async function getGridTile({ }, { sessionId: searchSessionId, + legacyHitsTotal: false, abortSignal, } ) @@ -130,6 +137,7 @@ export async function getTile({ const searchOptions = { sessionId: searchSessionId, + legacyHitsTotal: false, abortSignal, }; @@ -141,6 +149,7 @@ export async function getTile({ body: { size: 0, query: requestBody.query, + track_total_hits: requestBody.size + 1, }, }, }, @@ -148,7 +157,12 @@ export async function getTile({ ) .toPromise(); - if (countResponse.rawResponse.hits.total > requestBody.size) { + if ( + isTotalHitsGreaterThan( + (countResponse.rawResponse.hits.total as unknown) as TotalHits, + requestBody.size + ) + ) { // Generate "too many features"-bounds const bboxResponse = await context .search!.search( @@ -165,6 +179,7 @@ export async function getTile({ }, }, }, + track_total_hits: false, }, }, }, @@ -191,7 +206,10 @@ export async function getTile({ { params: { index, - body: requestBody, + body: { + ...requestBody, + track_total_hits: false, + }, }, }, searchOptions diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js index e12ffefb71080..6d4fca1b0b7c0 100644 --- a/x-pack/test/functional/apps/maps/blended_vector_layer.js +++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js @@ -27,28 +27,21 @@ export default function ({ getPageObjects, getService }) { }); it('should request documents when zoomed to smaller regions showing less data', async () => { - const hits = await PageObjects.maps.getHits(); + const response = await PageObjects.maps.getResponse(); // Allow a range of hits to account for variances in browser window size. - expect(parseInt(hits)).to.be.within(30, 40); + expect(response.hits.hits.length).to.be.within(30, 40); }); it('should request clusters when zoomed to larger regions showing lots of data', async () => { await PageObjects.maps.setView(20, -90, 2); - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - - expect(hits).to.equal('0'); - expect(totalHits).to.equal('14000'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(17); }); it('should request documents when query narrows data', async () => { await PageObjects.maps.setAndSubmitQuery('bytes > 19000'); - const hits = await PageObjects.maps.getHits(); - expect(hits).to.equal('75'); + const response = await PageObjects.maps.getResponse(); + expect(response.hits.hits.length).to.equal(75); }); }); } diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index a49ab7d7dd980..1d6477b243cdf 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -9,9 +9,6 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); - const inspector = getService('inspector'); - const monacoEditor = getService('monacoEditor'); - const testSubjects = getService('testSubjects'); const security = getService('security'); describe('docvalue_fields', () => { @@ -24,18 +21,9 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - async function getResponse() { - await inspector.open(); - await inspector.openInspectorRequestsView(); - await testSubjects.click('inspectorRequestDetailResponse'); - const responseBody = await monacoEditor.getCodeEditorValue(); - await inspector.close(); - return JSON.parse(responseBody); - } - it('should only fetch geo_point field and nothing else when source does not have data driven styling', async () => { await PageObjects.maps.loadSavedMap('document example'); - const response = await getResponse(); + const response = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); expect(firstHit.fields).to.only.have.keys(['geo.coordinates']); @@ -43,7 +31,7 @@ export default function ({ getPageObjects, getService }) { it('should only fetch geo_point field and data driven styling fields', async () => { await PageObjects.maps.loadSavedMap('document example with data driven styles'); - const response = await getResponse(); + const response = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); expect(firstHit.fields).to.only.have.keys(['bytes', 'geo.coordinates', 'hour_of_day']); @@ -51,7 +39,7 @@ export default function ({ getPageObjects, getService }) { it('should format date fields as epoch_millis when data driven styling is applied to a date field', async () => { await PageObjects.maps.loadSavedMap('document example with data driven styles on date field'); - const response = await getResponse(); + const response = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); expect(firstHit.fields).to.only.have.keys(['@timestamp', 'bytes', 'geo.coordinates']); diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 0f331e7763a76..89c1cbded9a26 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -14,7 +14,6 @@ export default function ({ getPageObjects, getService }) { const filterBar = getService('filterBar'); const dashboardPanelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); const security = getService('security'); @@ -81,11 +80,10 @@ export default function ({ getPageObjects, getService }) { }); it('should apply container state (time, query, filters) to embeddable when loaded', async () => { - await dashboardPanelActions.openInspectorByTitle('geo grid vector grid example'); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - expect(totalHits).to.equal('6'); + const response = await PageObjects.maps.getResponseFromDashboardPanel( + 'geo grid vector grid example' + ); + expect(response.aggregations.gridSplit.buckets.length).to.equal(6); }); it('should apply new container state (time, query, filters) to embeddable', async () => { @@ -94,25 +92,16 @@ export default function ({ getPageObjects, getService }) { await filterBar.selectIndexPattern('meta_for_geo_shapes*'); await filterBar.addFilter('shape_name', 'is', 'alpha'); - await dashboardPanelActions.openInspectorByTitle('geo grid vector grid example'); - const geoGridRequestStats = await inspector.getTableData(); - const geoGridTotalHits = PageObjects.maps.getInspectorStatRowHit( - geoGridRequestStats, - 'Hits (total)' + const gridResponse = await PageObjects.maps.getResponseFromDashboardPanel( + 'geo grid vector grid example' ); - await inspector.close(); - expect(geoGridTotalHits).to.equal('1'); + expect(gridResponse.aggregations.gridSplit.buckets.length).to.equal(1); - await dashboardPanelActions.openInspectorByTitle('join example'); - await testSubjects.click('inspectorRequestChooser'); - await testSubjects.click('inspectorRequestChoosermeta_for_geo_shapes*.shape_name'); - const joinRequestStats = await inspector.getTableData(); - const joinTotalHits = PageObjects.maps.getInspectorStatRowHit( - joinRequestStats, - 'Hits (total)' + const joinResponse = await PageObjects.maps.getResponseFromDashboardPanel( + 'join example', + 'meta_for_geo_shapes*.shape_name' ); - await inspector.close(); - expect(joinTotalHits).to.equal('3'); + expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); it('should re-fetch query when "refresh" is clicked', async () => { diff --git a/x-pack/test/functional/apps/maps/es_geo_grid_source.js b/x-pack/test/functional/apps/maps/es_geo_grid_source.js index 5a9e62b94f2a2..6dee4b87bceea 100644 --- a/x-pack/test/functional/apps/maps/es_geo_grid_source.js +++ b/x-pack/test/functional/apps/maps/es_geo_grid_source.js @@ -141,12 +141,8 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to geotile_grid aggregation request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - expect(hits).to.equal('1'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(1); }); }); @@ -156,18 +152,8 @@ export default function ({ getPageObjects, getService }) { }); it('should contain geotile_grid aggregation elasticsearch request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('6'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('logstash-*'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(4); }); it('should not contain any elasticsearch request after layer is deleted', async () => { @@ -218,12 +204,8 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to geotile_grid aggregation request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - expect(hits).to.equal('1'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(1); }); }); @@ -233,18 +215,8 @@ export default function ({ getPageObjects, getService }) { }); it('should contain geotile_grid aggregation elasticsearch request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('6'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('logstash-*'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(4); }); it('should not contain any elasticsearch request after layer is deleted', async () => { @@ -272,18 +244,8 @@ export default function ({ getPageObjects, getService }) { }); it('should contain geotile_grid aggregation elasticsearch request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('4'); //4 geometries result in 13 cells due to way they overlap geotile_grid cells - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('geo_shapes*'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(13); }); }); }); diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/es_pew_pew_source.js index 41dadff1b6f93..66406cd6d8f91 100644 --- a/x-pack/test/functional/apps/maps/es_pew_pew_source.js +++ b/x-pack/test/functional/apps/maps/es_pew_pew_source.js @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); - const inspector = getService('inspector'); const security = getService('security'); const VECTOR_SOURCE_ID = '67c1de2c-2fc5-4425-8983-094b589afe61'; @@ -25,15 +24,8 @@ export default function ({ getPageObjects, getService }) { }); it('should request source clusters for destination locations', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - - expect(hits).to.equal('0'); - expect(totalHits).to.equal('4'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.destSplit.buckets.length).to.equal(2); }); it('should render lines', async () => { diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 49717016f9c60..8b40651ea5674 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -121,17 +121,8 @@ export default function ({ getPageObjects, getService }) { }); it('should not apply query to source and apply query to join', async () => { - await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name'); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('3'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('meta_for_geo_shapes*'); + const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + expect(joinResponse.aggregations.join.buckets.length).to.equal(2); }); }); @@ -145,13 +136,8 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to join request', async () => { - await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name'); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('2'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - await inspector.close(); + const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); it('should update dynamic data range in legend with new results', async () => { diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 3f6b5691314bb..3d9572dcac24e 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -23,6 +23,8 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const browser = getService('browser'); const MenuToggle = getService('MenuToggle'); const listingTable = getService('listingTable'); + const monacoEditor = getService('monacoEditor'); + const dashboardPanelActions = getService('dashboardPanelActions'); const setViewPopoverToggle = new MenuToggle({ name: 'SetView Popover', @@ -614,6 +616,31 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte return mapboxStyle; } + async getResponse(requestName: string) { + await inspector.open(); + const response = await this._getResponse(requestName); + await inspector.close(); + return response; + } + + async _getResponse(requestName: string) { + if (requestName) { + await testSubjects.click('inspectorRequestChooser'); + await testSubjects.click(`inspectorRequestChooser${requestName}`); + } + await inspector.openInspectorRequestsView(); + await testSubjects.click('inspectorRequestDetailResponse'); + const responseBody = await monacoEditor.getCodeEditorValue(); + return JSON.parse(responseBody); + } + + async getResponseFromDashboardPanel(panelTitle: string, requestName: string) { + await dashboardPanelActions.openInspectorByTitle(panelTitle); + const response = await this._getResponse(requestName); + await inspector.close(); + return response; + } + getInspectorStatRowHit(stats: string[][], rowName: string) { const STATS_ROW_NAME_INDEX = 0; const STATS_ROW_VALUE_INDEX = 1; From d70d02ee83b18128f07d3d6fd92c50b539a25571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Thu, 25 Mar 2021 15:05:23 -0400 Subject: [PATCH 74/88] Add a11y test coverage to Rule Creation Flow for Detections tab (#94377) [Security Solution] Add a11y test coverage to Detections rule creation flow (#80060) --- test/functional/page_objects/common_page.ts | 15 ++ test/functional/services/common/find.ts | 9 +- .../services/common/test_subjects.ts | 13 +- .../web_element_wrapper.ts | 8 +- .../accessibility/apps/security_solution.ts | 142 +++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + x-pack/test/functional/config.js | 3 + x-pack/test/functional/page_objects/index.ts | 2 + .../page_objects/detections/index.ts | 149 ++++++++++++++++++ 9 files changed, 330 insertions(+), 12 deletions(-) create mode 100644 x-pack/test/accessibility/apps/security_solution.ts create mode 100644 x-pack/test/security_solution_ftr/page_objects/detections/index.ts 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/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/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/x-pack/test/accessibility/apps/security_solution.ts b/x-pack/test/accessibility/apps/security_solution.ts new file mode 100644 index 0000000000000..0ee4e88d712c8 --- /dev/null +++ b/x-pack/test/accessibility/apps/security_solution.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const a11y = getService('a11y'); + const { common, detections } = getPageObjects(['common', 'detections']); + const security = getService('security'); + const toasts = getService('toasts'); + const testSubjects = getService('testSubjects'); + + describe('Security Solution', () => { + before(async () => { + await security.testUser.setRoles(['superuser'], false); + await common.navigateToApp('security'); + }); + + after(async () => { + await security.testUser.restoreDefaults(false); + }); + + describe('Detections', () => { + describe('Create Rule Flow', () => { + beforeEach(async () => { + await detections.navigateToCreateRule(); + }); + + describe('Custom Query Rule', () => { + describe('Define Step', () => { + it('default view meets a11y requirements', async () => { + await toasts.dismissAllToasts(); + await testSubjects.click('customRuleType'); + await a11y.testAppSnapshot(); + }); + + describe('import query modal', () => { + it('contents of the default tab meets a11y requirements', async () => { + await detections.openImportQueryModal(); + await a11y.testAppSnapshot(); + }); + + it('contents of the templates tab meets a11y requirements', async () => { + await common.scrollKibanaBodyTop(); + await detections.openImportQueryModal(); + await detections.viewTemplatesInImportQueryModal(); + await a11y.testAppSnapshot(); + }); + }); + + it('preview section meets a11y requirements', async () => { + await detections.addCustomQuery('_id'); + await detections.preview(); + await a11y.testAppSnapshot(); + }); + + describe('About Step', () => { + beforeEach(async () => { + await detections.addCustomQuery('_id'); + await detections.continue('define'); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + + it('advanced settings view meets a11y requirements', async () => { + await detections.revealAdvancedSettings(); + await a11y.testAppSnapshot(); + }); + + describe('Schedule Step', () => { + beforeEach(async () => { + await detections.addNameAndDescription(); + await detections.continue('about'); + }); + + it('meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + + describe('Actions Step', () => { + it('meets a11y requirements', async () => { + await detections.continue('schedule'); + await a11y.testAppSnapshot(); + }); + }); + }); + }); + }); + }); + + describe('Machine Learning Rule First Step', () => { + it('default view meets a11y requirements', async () => { + await detections.selectMLRule(); + await a11y.testAppSnapshot(); + }); + }); + + describe('Threshold Rule Rule First Step', () => { + beforeEach(async () => { + await detections.selectThresholdRule(); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + + it('preview section meets a11y requirements', async () => { + await detections.addCustomQuery('_id'); + await detections.preview(); + await a11y.testAppSnapshot(); + }); + }); + + describe('Event Correlation Rule First Step', () => { + beforeEach(async () => { + await detections.selectEQLRule(); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + }); + + describe('Indicator Match Rule First Step', () => { + beforeEach(async () => { + await detections.selectIndicatorMatchRule(); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index c6d85c8755a6b..2a8840c364927 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -34,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/lens'), require.resolve('./apps/upgrade_assistant'), require.resolve('./apps/canvas'), + require.resolve('./apps/security_solution'), ], pageObjects, diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 9b1df72aa78c8..c0323d96026ef 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -197,6 +197,9 @@ export default async function ({ readConfigFile }) { reporting: { pathname: '/app/management/insightsAndAlerting/reporting', }, + securitySolution: { + pathname: '/app/security', + }, }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 804f49e5ea075..cf92191075fba 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -40,6 +40,7 @@ import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; import { TagManagementPageProvider } from './tag_management_page'; import { NavigationalSearchProvider } from './navigational_search'; import { SearchSessionsPageProvider } from './search_sessions_management_page'; +import { DetectionsPageProvider } from '../../security_solution_ftr/page_objects/detections'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -77,4 +78,5 @@ export const pageObjects = { roleMappings: RoleMappingsPageProvider, ingestPipelines: IngestPipelinesPageProvider, navigationalSearch: NavigationalSearchProvider, + detections: DetectionsPageProvider, }; diff --git a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts new file mode 100644 index 0000000000000..dd17548df6e3f --- /dev/null +++ b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +export function DetectionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const { common } = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + class DetectionsPage { + async navigateHome(): Promise { + await this.navigateToDetectionsPage(); + } + + async navigateToRules(): Promise { + await this.navigateToDetectionsPage('rules'); + } + + async navigateToRuleMonitoring(): Promise { + await common.clickAndValidate('allRulesTableTab-monitoring', 'monitoring-table'); + } + + async navigateToExceptionList(): Promise { + await common.clickAndValidate('allRulesTableTab-exceptions', 'exceptions-table'); + } + + async navigateToCreateRule(): Promise { + await this.navigateToDetectionsPage('rules/create'); + } + + async replaceIndexPattern(): Promise { + const buttons = await find.allByCssSelector('[data-test-subj="comboBoxInput"] button'); + await buttons.map(async (button: WebElementWrapper) => await button.click()); + await testSubjects.setValue('comboBoxSearchInput', '*'); + } + + async openImportQueryModal(): Promise { + const element = await testSubjects.find('importQueryFromSavedTimeline'); + await element.click(500); + await testSubjects.exists('open-timeline-modal-body-filter-default'); + } + + async viewTemplatesInImportQueryModal(): Promise { + await common.clickAndValidate('open-timeline-modal-body-filter-template', 'timelines-table'); + } + + async closeImportQueryModal(): Promise { + await find.clickByCssSelector('.euiButtonIcon.euiButtonIcon--text.euiModal__closeIcon'); + } + + async selectMachineLearningJob(): Promise { + await find.clickByCssSelector('[data-test-subj="mlJobSelect"] button'); + await find.clickByCssSelector('#high_distinct_count_error_message'); + } + + async openAddFilterPopover(): Promise { + const addButtons = await testSubjects.findAll('addFilter'); + await addButtons[1].click(); + await testSubjects.exists('saveFilter'); + } + + async closeAddFilterPopover(): Promise { + await testSubjects.click('cancelSaveFilter'); + } + + async toggleFilterActions(): Promise { + const filterActions = await testSubjects.findAll('addFilter'); + await filterActions[1].click(); + } + + async toggleSavedQueries(): Promise { + const filterActions = await find.allByCssSelector( + '[data-test-subj="saved-query-management-popover-button"]' + ); + await filterActions[1].click(); + } + + async addNameAndDescription( + name: string = 'test rule name', + description: string = 'test rule description' + ): Promise { + await find.setValue(`[aria-describedby="detectionEngineStepAboutRuleName"]`, name, 500); + await find.setValue( + `[aria-describedby="detectionEngineStepAboutRuleDescription"]`, + description, + 500 + ); + } + + async goBackToAllRules(): Promise { + await common.clickAndValidate('ruleDetailsBackToAllRules', 'create-new-rule'); + } + + async revealAdvancedSettings(): Promise { + await common.clickAndValidate( + 'advancedSettings', + 'detectionEngineStepAboutRuleReferenceUrls' + ); + } + + async preview(): Promise { + await common.clickAndValidate( + 'queryPreviewButton', + 'queryPreviewCustomHistogram', + undefined, + 500 + ); + } + + async continue(prefix: string): Promise { + await testSubjects.click(`${prefix}-continue`); + } + + async addCustomQuery(query: string): Promise { + await testSubjects.setValue('queryInput', query, undefined, 500); + } + + async selectMLRule(): Promise { + await common.clickAndValidate('machineLearningRuleType', 'mlJobSelect'); + } + + async selectEQLRule(): Promise { + await common.clickAndValidate('eqlRuleType', 'eqlQueryBarTextInput'); + } + + async selectIndicatorMatchRule(): Promise { + await common.clickAndValidate('threatMatchRuleType', 'comboBoxInput'); + } + + async selectThresholdRule(): Promise { + await common.clickAndValidate('thresholdRuleType', 'input'); + } + + private async navigateToDetectionsPage(path: string = ''): Promise { + const subUrl = `detections${path ? `/${path}` : ''}`; + await common.navigateToUrl('securitySolution', subUrl, { + shouldUseHashForSubUrl: false, + }); + } + } + + return new DetectionsPage(); +} From 6eb31178ed172fbf452aeb78f60e681e74f386ef Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 25 Mar 2021 20:20:01 +0100 Subject: [PATCH 75/88] blank_issues_enabled: false. it seems contact_links not supported otherwise (#95423) --- .github/ISSUE_TEMPLATE/Question.md | 15 --------------- .github/ISSUE_TEMPLATE/config.yml | 4 ++++ 2 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/Question.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml 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..e8050c846b254 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +blank_issues_enabled: false +contact_links: + - name: Question + url: https://discuss.elastic.co/c/kibana From a10c4188b76044cbcc1c7480e97f8a6c62cc2c05 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 25 Mar 2021 12:24:36 -0700 Subject: [PATCH 76/88] Re-enable skipped test (discover with async scripted fields) (#94653) * Re-enable skipped test * remove comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test/functional/apps/discover/async_scripted_fields.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 9696eb0460142..7364f2883bd1a 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -19,8 +19,7 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const security = getService('security'); - // Failing: See https://github.com/elastic/kibana/issues/78553 - describe.skip('async search with scripted fields', function () { + describe('async search with scripted fields', function () { this.tags(['skipFirefox']); before(async function () { @@ -42,7 +41,7 @@ export default function ({ getService, getPageObjects }) { await security.testUser.restoreDefaults(); }); - it.skip('query should show failed shards pop up', async function () { + it('query should show failed shards pop up', async function () { if (false) { /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' */ @@ -74,7 +73,7 @@ export default function ({ getService, getPageObjects }) { }); }); - it.skip('query return results with valid scripted field', async function () { + it('query return results with valid scripted field', async function () { if (false) { /* the commented-out steps below were used to create the scripted fields in the logstash-* index pattern which are now saved in the esArchive. From 3bb9220db90a5976388b21e23ce7f3b6327e22e6 Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 25 Mar 2021 20:29:21 +0100 Subject: [PATCH 77/88] add about section for link in github issue template --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e8050c846b254..aa68db29974a8 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,3 +2,4 @@ blank_issues_enabled: false contact_links: - name: Question url: https://discuss.elastic.co/c/kibana + about: Please ask and answer questions here. From dd10c8b5f2197c936a2efbc45feed7fbb88ebe08 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 25 Mar 2021 12:57:32 -0700 Subject: [PATCH 78/88] [kbn/test] switch to @elastic/elasticsearch (#95443) Co-authored-by: spalger --- packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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(), }); } From 306a42c03c00aa640ad2ceb89d085ad0ce85af67 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 25 Mar 2021 14:59:14 -0500 Subject: [PATCH 79/88] Index pattern management - use new es client instead of legacy (#95293) * use new es client instead of legacy * use resolve api on client --- .../server/routes/resolve_index.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/plugins/index_pattern_management/server/routes/resolve_index.ts b/src/plugins/index_pattern_management/server/routes/resolve_index.ts index 851a2578231aa..22c214f2adee2 100644 --- a/src/plugins/index_pattern_management/server/routes/resolve_index.ts +++ b/src/plugins/index_pattern_management/server/routes/resolve_index.ts @@ -31,19 +31,11 @@ export function registerResolveIndexRoute(router: IRouter): void { }, }, async (context, req, res) => { - const queryString = req.query.expand_wildcards - ? { expand_wildcards: req.query.expand_wildcards } - : null; - const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ - queryString ? '?' + new URLSearchParams(queryString).toString() : '' - }`, - } - ); - return res.ok({ body: result }); + const { body } = await context.core.elasticsearch.client.asCurrentUser.indices.resolveIndex({ + name: req.params.query, + expand_wildcards: req.query.expand_wildcards || 'open', + }); + return res.ok({ body }); } ); } From 724e21e0070b9679c7bdf9fa4da08f8bfd05a18e Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 25 Mar 2021 15:11:40 -0500 Subject: [PATCH 80/88] skip flaky test. #95345 --- .../management/users/edit_user/create_user_page.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx index 2c40fec2ec31d..4c0bb6a67f2e4 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx @@ -53,7 +53,8 @@ describe('CreateUserPage', () => { }); }); - it('validates form', async () => { + // flaky https://github.com/elastic/kibana/issues/95345 + it.skip('validates form', async () => { const coreStart = coreMock.createStart(); const history = createMemoryHistory({ initialEntries: ['/create'] }); const authc = securityMock.createSetup().authc; From bd3f5d4863fccac61f093e03c7900c303a4a71c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 25 Mar 2021 21:19:48 +0100 Subject: [PATCH 81/88] Update APM readme (#95383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update APM readme * Update readme.md * Update x-pack/plugins/apm/readme.md Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> --- x-pack/plugins/apm/readme.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index b35024844a892..b125407a160aa 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -31,19 +31,19 @@ _Docker Compose is required_ ## Testing -### E2E (Cypress) tests +### Cypress tests ```sh -x-pack/plugins/apm/e2e/run-e2e.sh +node x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js ``` _Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sample data into Elasticsearch via APM Server and runs the Cypress tests_ -### Unit testing +### Jest tests Note: Run the following commands from `kibana/x-pack/plugins/apm`. -#### Run unit tests +#### Run ``` npx jest --watch @@ -82,8 +82,11 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### API integration tests -Our tests are separated in two suites: one suite runs with a basic license, and the other -with a trial license (the equivalent of gold+). This requires separate test servers and test runners. +API tests are separated in two suites: + - a basic license test suite + - a trial license test suite (the equivalent of gold+) + +This requires separate test servers and test runners. **Basic** @@ -109,7 +112,10 @@ node scripts/functional_test_runner --config x-pack/test/apm_api_integration/tri The API tests for "trial" are located in `x-pack/test/apm_api_integration/trial/tests`. -For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + +**API Test tips** + - For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + - To update snapshots append `--updateSnapshots` to the functional_test_runner command ## Linting From e01f317d9cd94461e1c11c17bde0cf3b70047012 Mon Sep 17 00:00:00 2001 From: Greg Back <1045796+gtback@users.noreply.github.com> Date: Thu, 25 Mar 2021 17:57:25 -0400 Subject: [PATCH 82/88] Add Vega help link to DocLinksService (#87721) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/core/public/doc_links/doc_links_service.ts | 1 + .../vis_type_vega/public/components/vega_help_menu.tsx | 4 +++- src/plugins/vis_type_vega/public/plugin.ts | 2 ++ src/plugins/vis_type_vega/public/services.ts | 4 +++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 9711d546fc947..0bb5ddd29609e 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -191,6 +191,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`, 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/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'); From ba029aa95eee81d0c6a102e5d5d16f7a10fede66 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Mar 2021 16:22:02 -0600 Subject: [PATCH 83/88] [Maps] split out DrawFilterControl and DrawControl (#95255) * [Maps] split out DrawFilterControl and DrawControl * clean up * update i18n id * give 'global_index_pattern_management_all' permission to functional test because new check blocks access without it * revert last change Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../mb_map/draw_control/draw_control.tsx | 114 ++-------------- .../draw_filter_control.tsx | 126 ++++++++++++++++++ .../draw_control/draw_filter_control/index.ts | 32 +++++ .../mb_map/draw_control/draw_tooltip.tsx | 9 +- .../mb_map/draw_control/index.ts | 26 +--- .../connected_components/mb_map/mb_map.tsx | 4 +- 6 files changed, 179 insertions(+), 132 deletions(-) create mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx create mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index f68875dc81394..a1bea4a8e93dc 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -12,20 +12,10 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw'; // @ts-expect-error import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { Map as MbMap } from 'mapbox-gl'; -import { i18n } from '@kbn/i18n'; -import { Filter } from 'src/plugins/data/public'; -import { Feature, Polygon } from 'geojson'; -import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants'; -import { DrawState } from '../../../../common/descriptor_types'; -import { DrawCircle, DrawCircleProperties } from './draw_circle'; -import { - createDistanceFilterWithMeta, - createSpatialFilterWithGeometry, - getBoundingBoxGeometry, - roundCoordinates, -} from '../../../../common/elasticsearch_util'; +import { Feature } from 'geojson'; +import { DRAW_TYPE } from '../../../../common/constants'; +import { DrawCircle } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; -import { getToasts } from '../../../kibana_services'; const DRAW_RECTANGLE = 'draw_rectangle'; const DRAW_CIRCLE = 'draw_circle'; @@ -35,10 +25,8 @@ mbDrawModes[DRAW_RECTANGLE] = DrawRectangle; mbDrawModes[DRAW_CIRCLE] = DrawCircle; export interface Props { - addFilters: (filters: Filter[], actionId: string) => Promise; - disableDrawState: () => void; - drawState?: DrawState; - isDrawingFilter: boolean; + drawType?: DRAW_TYPE; + onDraw: (event: { features: Feature[] }) => void; mbMap: MbMap; } @@ -70,100 +58,26 @@ export class DrawControl extends Component { return; } - if (this.props.isDrawingFilter) { + if (this.props.drawType) { this._updateDrawControl(); } else { this._removeDrawControl(); } }, 0); - _onDraw = async (e: { features: Feature[] }) => { - if ( - !e.features.length || - !this.props.drawState || - !this.props.drawState.geoFieldName || - !this.props.drawState.indexPatternId - ) { - return; - } - - let filter: Filter | undefined; - if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { - const circle = e.features[0] as Feature & { properties: DrawCircleProperties }; - const distanceKm = _.round( - circle.properties.radiusKm, - circle.properties.radiusKm > 10 ? 0 : 2 - ); - // Only include as much precision as needed for distance - let precision = 2; - if (distanceKm <= 1) { - precision = 5; - } else if (distanceKm <= 10) { - precision = 4; - } else if (distanceKm <= 100) { - precision = 3; - } - filter = createDistanceFilterWithMeta({ - alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '', - distanceKm, - geoFieldName: this.props.drawState.geoFieldName, - indexPatternId: this.props.drawState.indexPatternId, - point: [ - _.round(circle.properties.center[0], precision), - _.round(circle.properties.center[1], precision), - ], - }); - } else { - const geometry = e.features[0].geometry as Polygon; - // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number - roundCoordinates(geometry.coordinates); - - filter = createSpatialFilterWithGeometry({ - geometry: - this.props.drawState.drawType === DRAW_TYPE.BOUNDS - ? getBoundingBoxGeometry(geometry) - : geometry, - indexPatternId: this.props.drawState.indexPatternId, - geoFieldName: this.props.drawState.geoFieldName, - geoFieldType: this.props.drawState.geoFieldType - ? this.props.drawState.geoFieldType - : ES_GEO_FIELD_TYPE.GEO_POINT, - geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '', - relation: this.props.drawState.relation - ? this.props.drawState.relation - : ES_SPATIAL_RELATIONS.INTERSECTS, - }); - } - - try { - await this.props.addFilters([filter!], this.props.drawState.actionId); - } catch (error) { - getToasts().addWarning( - i18n.translate('xpack.maps.drawControl.unableToCreatFilter', { - defaultMessage: `Unable to create filter, error: '{errorMsg}'.`, - values: { - errorMsg: error.message, - }, - }) - ); - } finally { - this.props.disableDrawState(); - } - }; - _removeDrawControl() { if (!this._mbDrawControlAdded) { return; } this.props.mbMap.getCanvas().style.cursor = ''; - this.props.mbMap.off('draw.create', this._onDraw); + this.props.mbMap.off('draw.create', this.props.onDraw); this.props.mbMap.removeControl(this._mbDrawControl); this._mbDrawControlAdded = false; } _updateDrawControl() { - if (!this.props.drawState) { + if (!this.props.drawType) { return; } @@ -171,27 +85,27 @@ export class DrawControl extends Component { this.props.mbMap.addControl(this._mbDrawControl); this._mbDrawControlAdded = true; this.props.mbMap.getCanvas().style.cursor = 'crosshair'; - this.props.mbMap.on('draw.create', this._onDraw); + this.props.mbMap.on('draw.create', this.props.onDraw); } const drawMode = this._mbDrawControl.getMode(); - if (drawMode !== DRAW_RECTANGLE && this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + if (drawMode !== DRAW_RECTANGLE && this.props.drawType === DRAW_TYPE.BOUNDS) { this._mbDrawControl.changeMode(DRAW_RECTANGLE); - } else if (drawMode !== DRAW_CIRCLE && this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + } else if (drawMode !== DRAW_CIRCLE && this.props.drawType === DRAW_TYPE.DISTANCE) { this._mbDrawControl.changeMode(DRAW_CIRCLE); } else if ( drawMode !== this._mbDrawControl.modes.DRAW_POLYGON && - this.props.drawState.drawType === DRAW_TYPE.POLYGON + this.props.drawType === DRAW_TYPE.POLYGON ) { this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON); } } render() { - if (!this.props.isDrawingFilter || !this.props.drawState) { + if (!this.props.drawType) { return null; } - return ; + return ; } } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx new file mode 100644 index 0000000000000..c0cbd3566ca8f --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; +import React, { Component } from 'react'; +import { Map as MbMap } from 'mapbox-gl'; +import { i18n } from '@kbn/i18n'; +import { Filter } from 'src/plugins/data/public'; +import { Feature, Polygon } from 'geojson'; +import { + DRAW_TYPE, + ES_GEO_FIELD_TYPE, + ES_SPATIAL_RELATIONS, +} from '../../../../../common/constants'; +import { DrawState } from '../../../../../common/descriptor_types'; +import { + createDistanceFilterWithMeta, + createSpatialFilterWithGeometry, + getBoundingBoxGeometry, + roundCoordinates, +} from '../../../../../common/elasticsearch_util'; +import { getToasts } from '../../../../kibana_services'; +import { DrawControl } from '../draw_control'; +import { DrawCircleProperties } from '../draw_circle'; + +export interface Props { + addFilters: (filters: Filter[], actionId: string) => Promise; + disableDrawState: () => void; + drawState?: DrawState; + isDrawingFilter: boolean; + mbMap: MbMap; +} + +export class DrawFilterControl extends Component { + _onDraw = async (e: { features: Feature[] }) => { + if ( + !e.features.length || + !this.props.drawState || + !this.props.drawState.geoFieldName || + !this.props.drawState.indexPatternId + ) { + return; + } + + let filter: Filter | undefined; + if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + const circle = e.features[0] as Feature & { properties: DrawCircleProperties }; + const distanceKm = _.round( + circle.properties.radiusKm, + circle.properties.radiusKm > 10 ? 0 : 2 + ); + // Only include as much precision as needed for distance + let precision = 2; + if (distanceKm <= 1) { + precision = 5; + } else if (distanceKm <= 10) { + precision = 4; + } else if (distanceKm <= 100) { + precision = 3; + } + filter = createDistanceFilterWithMeta({ + alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '', + distanceKm, + geoFieldName: this.props.drawState.geoFieldName, + indexPatternId: this.props.drawState.indexPatternId, + point: [ + _.round(circle.properties.center[0], precision), + _.round(circle.properties.center[1], precision), + ], + }); + } else { + const geometry = e.features[0].geometry as Polygon; + // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number + roundCoordinates(geometry.coordinates); + + filter = createSpatialFilterWithGeometry({ + geometry: + this.props.drawState.drawType === DRAW_TYPE.BOUNDS + ? getBoundingBoxGeometry(geometry) + : geometry, + indexPatternId: this.props.drawState.indexPatternId, + geoFieldName: this.props.drawState.geoFieldName, + geoFieldType: this.props.drawState.geoFieldType + ? this.props.drawState.geoFieldType + : ES_GEO_FIELD_TYPE.GEO_POINT, + geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '', + relation: this.props.drawState.relation + ? this.props.drawState.relation + : ES_SPATIAL_RELATIONS.INTERSECTS, + }); + } + + try { + await this.props.addFilters([filter!], this.props.drawState.actionId); + } catch (error) { + getToasts().addWarning( + i18n.translate('xpack.maps.drawFilterControl.unableToCreatFilter', { + defaultMessage: `Unable to create filter, error: '{errorMsg}'.`, + values: { + errorMsg: error.message, + }, + }) + ); + } finally { + this.props.disableDrawState(); + } + }; + + render() { + return ( + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts new file mode 100644 index 0000000000000..17f4d919fb7e0 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.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 { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { connect } from 'react-redux'; +import { DrawFilterControl } from './draw_filter_control'; +import { updateDrawState } from '../../../../actions'; +import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors'; +import { MapStoreState } from '../../../../reducers/store'; + +function mapStateToProps(state: MapStoreState) { + return { + isDrawingFilter: isDrawingFilter(state), + drawState: getDrawState(state), + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch) { + return { + disableDrawState() { + dispatch(updateDrawState(null)); + }, + }; +} + +const connected = connect(mapStateToProps, mapDispatchToProps)(DrawFilterControl); +export { connected as DrawFilterControl }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx index 099f409c91c21..df650d5dfe410 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx @@ -11,13 +11,12 @@ import { EuiPopover, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Map as MbMap } from 'mapbox-gl'; import { DRAW_TYPE } from '../../../../common/constants'; -import { DrawState } from '../../../../common/descriptor_types'; const noop = () => {}; interface Props { mbMap: MbMap; - drawState: DrawState; + drawType: DRAW_TYPE; } interface State { @@ -58,16 +57,16 @@ export class DrawTooltip extends Component { } let instructions; - if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + if (this.props.drawType === DRAW_TYPE.BOUNDS) { instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { defaultMessage: 'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.', }); - } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + } else if (this.props.drawType === DRAW_TYPE.DISTANCE) { instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', { defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.', }); - } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + } else if (this.props.drawType === DRAW_TYPE.POLYGON) { instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.', }); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts index cc2f560c63d24..63f91a03a5d01 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts @@ -5,28 +5,4 @@ * 2.0. */ -import { AnyAction } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { connect } from 'react-redux'; -import { DrawControl } from './draw_control'; -import { updateDrawState } from '../../../actions'; -import { getDrawState, isDrawingFilter } from '../../../selectors/map_selectors'; -import { MapStoreState } from '../../../reducers/store'; - -function mapStateToProps(state: MapStoreState) { - return { - isDrawingFilter: isDrawingFilter(state), - drawState: getDrawState(state), - }; -} - -function mapDispatchToProps(dispatch: ThunkDispatch) { - return { - disableDrawState() { - dispatch(updateDrawState(null)); - }, - }; -} - -const connected = connect(mapStateToProps, mapDispatchToProps)(DrawControl); -export { connected as DrawControl }; +export { DrawFilterControl } from './draw_filter_control'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index fae89a0484f11..5e4c3c9b1981f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -17,7 +17,7 @@ import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { Adapters } from 'src/plugins/inspector/public'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; -import { DrawControl } from './draw_control'; +import { DrawFilterControl } from './draw_control'; import { ScaleControl } from './scale_control'; // @ts-expect-error import { TooltipControl } from './tooltip_control'; @@ -418,7 +418,7 @@ export class MBMap extends Component { let scaleControl; if (this.state.mbMap) { drawControl = this.props.addFilters ? ( - + ) : null; tooltipControl = !this.props.settings.disableTooltipControl ? ( Date: Thu, 25 Mar 2021 18:52:07 -0400 Subject: [PATCH 84/88] Add section on developer documentation into best practices docs (#95473) * add section on dev docs to best pratices. * Update best_practices.mdx * Update dev_docs/best_practices.mdx Co-authored-by: Brandon Kobel Co-authored-by: Brandon Kobel --- dev_docs/assets/api_doc_pick.png | Bin 0 -> 82547 bytes dev_docs/assets/dev_docs_nested_object.png | Bin 0 -> 133531 bytes dev_docs/best_practices.mdx | 126 +++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 dev_docs/assets/api_doc_pick.png create mode 100644 dev_docs/assets/dev_docs_nested_object.png diff --git a/dev_docs/assets/api_doc_pick.png b/dev_docs/assets/api_doc_pick.png new file mode 100644 index 0000000000000000000000000000000000000000..825fa47b266cb9ca39eed58d0bcbd939a3fbe981 GIT binary patch literal 82547 zcmeFZgw(jX;_bcZ4>9YZ6+fHcz00SQIA8;PM|VCY6snn7~t4q@nS zzOB!B-silJ=e+;G_nYh5v)8P>SKaGgci;0tRaq7vmjV|J4Gmvj?u9xU+U+JZv|Fop zZUZ%R8JzvV4{1v&DOGtXDOy!Wdvi-02pSqowCSr?_vN3kbeWjEdezm>&Wh{krvCnY znEETfp0=*GFKz9gdfU>I3=HPU?#*LpenHDo`&?@+IV$O(OKLWD5c|@td1QaJ5OIID ztLX|tpXd3oEj~ss4KMcw?X8~UGc_Cx3{%{wQUfD(wAXHE6ZF!OvbQFjBsZP!cwxLx z$26h2Rg8n%UCySUnYL|dabqdx-YYY^HxzPh^||k2 zMndo3+e$N5)s0KeP|Kdbg9p{b8fY>TqrzgqV34|fTQx~a)348(ZKV2{h5xr*XB_}&&AS0{cd&o#a_a%LgQt@p{1yNvsbGpC3%jC^X zW;;3AI3GFLwb9MZ&B-zF#fg#P_tAf$O#p3|&Tw+DJ1!f5YbZoV-dsrujSVQ@LBqU7 zfrbT?ZULA0Ez18X%iLl?!}wK?j)oRuiH7;-JIcWQ=O+rdezy779U~?f4IB7%AGq8< zq5u2s+fAP^{$0Mc3Oqx5rXeLS58O3O9U%}qr#JS_hgLYkKn0G2oURiZ8VS?S>z2Ga z<1WxY(o$2$Sx4!mu&KQ*r-_;UYY3;it;5fL&_vyZfub$M*@V{J*2d0B*j z`DZnVp7z%(&eme|I!davQudAzT0Tx*&ZqR^xU{siqK;8u^RIbA+%5kzlAY6^%K{b%`uPOJ#rYKUKYasDMSs=`t6I84 zY;;~&+5#{G#t`Qc;N}(m)!?^B{~7XMO|_gLj#BovKuc%w|3LlE#(#bI-wl6_sr#QX zc?J3ZI^u~UhJnoaPH6v#JYufPZI4Pzx#5K|159;sq{Es&F@Xm*c zQ7kwgx-F~b?*Su5d7+zReZEgblHw)M7XMF8Md>BySC*cA?O>+RG7K~ncOMT&L-%+= z%gCsc{RO9aziVlp|L@mIn}~U*si{q4Vg)P65E#w^tKhJwGBTJ_>Te5IX?vA1Y+a|XSp@~hFF{{VNyi#84Db4bUAu=s z=y!6Nqe#&~>FuiRoL8@ni*>7tg0TD#3?+-Hsy7DDXTA#^=={?tm_`(iOibj)56N%N zC>_sU5ACuuGA?iW`QOkdwDl&6UJyyrHexx7><)nrm+r^(m45^sR$f>R-_*0b!umpF zLWgJX&M7XrP5v8L_Nygpu#G-^_z)8nm9+Ay_ki`yE-?uSTUJ(9MhLu0X+EAwTv-Oq z|H++Y*LjFfGL0qPgbV~sLd}@Js(-8WP}mmltdKY=@XOgT*Fnc`^Phi2+%?vw_^vE4 zFwh2ybN4RKJD8+lU>mz?%;7sUbaLXZtu|CHS4b9On)rQv8~6FFWn^1D{U<->A9zNC zw~}~v3_J#=nSVzg2hKZY`t#6u!@a!;N>|;sg$0va#fS8}a7~N@VGI2@Z@8!&KTTL{ zbEk5$lw>^XLnaP3nXX-SWz1Fj!vINfB4FU&^EnB0nD@T_qOI9Sv{vScFADj;LYCQR!EqpQ4}S6EZGE;mt4HOq<%c_aJINbf zO?$DlXIpbCMsvz5>hTckn*gIoa*aFPH!WdNtu@=R=PQamy;Se;GTmXe2fYY;q;zjv zCV@PBPh_}S7l+(i{u&=cr31USDLoo)<@f$kxEKS}^hHX7VoV3*^|vIFwI-uGO$b!y zK&pf+W=ZRVJHZ3d*d4a?|4sDTJQ$)l{1dfZErYc>g4;2 zB!{AtH9<{7d$}_!Fn!#vc>7C;z42T@g}0Z*>|JMJU$VzM+D|HX4p#K_6fzU8vA8nU zCezalEmiAlp6smPVx@N)8;~(vbyjzibt;D8kzMxVo#*9eva7pL4NUTt+jZ zV@1;Im5Fn4o2MfC8S;~^+Wy$|=>5c~b0^Jg2TO7vC9U-_!J~?o2HR<(L2alsZ=2~u zkH@dB(k16zGQ`|xIe$Z&We@*9uz#M|`=p6ZqlO9P+8)v`0u>z`#)_JNeJt_iIp5&e zWh2p0KPKd0pnJwTswPC_?$g9AL!I8x-Ex&hRn%+B!A-(gNs+4)M&3Vu3X@LG8Lt$EMt?wsjzGL3;h zc&W(6tee53_Ar0S4a;l;&T;IOjI2}-Wy=@bSCFyX?zsd>{A9~Ovi~a zfA9b@k-)d!5^tBtmsl%|b=@rB%OGEK<<{2ZJD3yzuQ2v^-eJn8yyw1BC5Y@T`2#Uf z@BNKUx6(X4P1r?&Ee-^-oici?rdGDG(s!JW9H_ijrrqs9sf1qqM%4WelxXMu!Ox#( z{~$G~uLukZlGjN(DWdb+={%?JojcChYQi#_G#{ekfL=`W#HZb9nl+$c5EFL^lEDzE zidvh5(r63K4Y;B1c5UrkS!*w8f;5zT{R+zCKuOn?RKm%u4^Ry%ftGn>4y3B zEhzV-s-)e_P2oBX-Y4GqPJH$20bw&Hv516j2HINea7ThMXW2CytTkYU^7n7hA_<6;h%BkekyIooj42e}IxN=Qs(Lv3+ zrF}vUY%dgKFKF7P4=n?|D@TaYAT@fHCm)`&lE(VBB}`Zl7aX7yxvw{;Dc~j4eWhik zNET_8@ZaAZQwdL0?#^pY-IB_jK|a5B%VPh&kN0(Q%$B8c8@~j@Ke2zu>D&2StE5k~ z-@aza#t2sYfHjg|#ZR!~C(#}8i-oLYJb+K_vVp7K8>q6$+%`ygLK^Sd znsI<0q7u?oE))%SMzXe_B{5R?m+B$eDLDoVq8YV9b2AiC^&VW@78(k~$fB8>dM%df z0yQwMbi)9%z{)CZ?M5AIuj%rzpEuaw#|<3wA~+Avf4vF3b{=7B!fj^~_+*NTo${C} zE+AS>z1GIWQiDU&lV5SGdbl91(Jy6iZNe_3mXIf)kCA-e`u&rMw{ps9;vB?ymBpMa zEDU9Wbf2FgitEQlt-tO>@1GDH3EKra zpX~$vAY|E$7i5J`hyd_sAGuPHGDI#uTgF}MH5$k)+H#5 zHS1JYWKWh{UF_FZ%_0&T^7fT4FkDBo*+SY42>(E+ozW{nRbfrQ=nlm4hko5UD+x-R~}Xx!yHz zD%J{*kKZNoC)UDLsP0?s9`3Fc_nA;`bx^ZQ!E9OE!_m6xoiCrhG%wLvosM5U54~2( z^<@SBaNQA5+rRdnJMl6z!(e^1fcsojrItiR(nwY7yv$3o& zvoo0;+z=!==CYw(!o==L>9pt(XtcxmF*?KeyrnE-Zu-upuOIC=M>)H&P2?jXm+zZK znKv7^@K*vBISIvf&sw?Q7fBOePYo#jOJv^&>>|m?FDiEiPN+J*Cu`UVa}Y($eTZSW ziN%mTfjXxk2I#RudlwcUTiW)N?E&J;{7>E}n(C+`*CgM~#z@^%Oj&b7J^(X|{}Jo8vY1&u8@8)Fc=+w!5qit5SS4G+|kE~`YZb}cVUIaVr2%Gwrp#G;2B zrzMQu-BA%85P~}L?4J8dRTY@q`@QH}i0%6DMx1_7>`a>}bnWnshpF=U z&e`n8)ZREs!_VeMI8CCn5mR|ib;FnU-Pga@_;DIN7IEE9NUmH<-Z_~po|0Q1Vq$v2 ztEpFOL*E&h83pYHr*={pYpUROylAY~q5POw5>9wP4p@=z&nf$>1eVhZ1dn~>{fov4UoiO>1nfk<8?yH*KpwKWoc zg6fG#Z!~K^4~N@c5IFsi_OUP3<5#J@zge!9)%XL}+Mh+4lbyZ%<*v12KgDPm?lw#9 zbvmU*nBUuj0SR`q;XDq<(=N3_ort z%t2?@UBgmK9D2TP=3U-56TKfapIAK5wOn83@kN-@xX9J^;pVaUMto=@E|#H_S*g%H z6TIlPb9(3Fa}2u!&a709ao5-gLAKu7vT7LLdPIVHeR=VH4&KwSD%b`t zQ`Lgh9BKKKz2H6O)~m+pS>@s{z54Hmul7lXat)%qTltMzj8}_9*#QBXfPmfhv<%%p-s*xVHqx~v#08rncYA!rF4MX=N1G2NkIdt8v~Q2K8`T}#8@-X1YhE*S`m%m~ zL$2j#TNKjM<6~hql*VLtRYp0kV@0e|m^5U9bCUk@?ByIkB;B@z7%bsz|Jv}2_b46X zk&vz;Eqhbd0CpH#yEgH=6{M1UVG)s^M_s*v?pEA#Vw-g)QMu^Zh!Y8P%398${AI>r z02kvd(K^qr?!maNfH6DLq(6IR%fUDmO)JkObtQSsW1srEl;kuEmOlOffN%*?&T59VU= zDQgwh^kK1z;jj3bnM&J=kC?Vrmb*2~qej;DG92eH2=h0#BM`Mce=uJpNgZu`?!3Ue zhu_o00ca7NOGb_!ot?~GQTf5WARdc+E2De_*8X7>v-H}uBj>x@5)8wB=ijD}uD$FP zkY0^bAv<+T{Ohw@;`P~Ee8=Jg@m>wnbXVuc{B9_RkO#BwiY@2o`UB>xu5;qexhOF= zPqE|eWy0lAkcj#nw`$dm9m~kvHhvFxx#q|`A2?>T&%kA7e%JYGbVuU z7X&uP5+eK?sgwE^I*1UA`uHSr#@F8SZd0(c4Qr0C0{QIA5T%O(MVI<*B{11a$%tp$ z9h0eflIM&6z(<__T8W7TBZ-?l;-1PhtEnJX4DD`?iDH>=QWW?)S z1RFC#adW-9#WrinlB)+H`!j~0)t|6I2r4m`n5@3*iO8|)mo#lsYmT%qul4k2N_SN; zl}UDxKNp^YR?qGpz0j@bC%!M-PQT_s5jP_|0nsQnz}G!`lN*@c&+0N;+xtjM2T@-x ziUBhpMfDhq8JLn#hu=*YY8RMl7x5(6LwXO9>o&1M>1B7;TZu^JKd^vb?H;C)B{wX7 z@@b5<=b-24X%ugiun9{jqp?qrV!p1N5(M)`J(?+OaJJv{^!2<9R{63bBI__Qbtl|- z>xZWuP07=@bVE~lz-yw8JNiS z#KFn*dDvQF8!Aby`Dh`3xDOX5=5>&ZhG5^JGr420cMGuwQGLKv(AH3Bo^@)_g_i}?%FQTa0C3-K2E*7_mz zBXl^W-zHjT6ZvElsgZ5PvL_cS`|QGc{Tj{38xw{NV&hte=O<#j?{#eO@!t%Pi)IfDx2h+QFy;X~w+g=UbHMUb?sgqO0VH?e`*2}Jf^N&?0 z5XiT45&t%J_RzGCzsstaMt`*RwvPaMx@qlP>MT0!n@?(wORU?3mXTYv&10II^`a5S z+eW0V9Pn3HY!{yTnO0_+vi0btrZU9f1v>W4CB9y*#3P^VO@ko!K`n zwxyFoTn!QWO!2`_U4c}C4ndj%e@hjy$5!pA>uHYE zE)|XLXRGbwlvsbjj5Ek>9?ZVG|0qw*z65b9sM~1$&L=Xzb3}kWAHY;f3V~?*ZHHP1 zPb=&X1IXHt7n^%hRZVA&s7VUdndyR}szQ<{R7IVWCRsivAa+3AA{%vD;I>`A-8k7( zIq|W!D#n+?P>OYof|Q-o)gGR@z8f>RP_|LWV-YZou5Mi>dJ$O3UQg~W=BL5FJ~w%k zA-dLFxq8zB@=t`o;=%7S<#C>twDkz*!CD9O8Dk|*zf2!}Dfp~jWcGH=gj~?Ms!WeQa`*L!+ecQtrjgUkSskh$8s5wwjqH;4E0m+FMRRYTWp$K?5Uo}`kSQPW z+hq}g;Q8p3yfGo)(=_&S7sGGN@bic%G0fc8r4+lAXEo-B(jT^cmKw7E&QeK8#r?ML z{jEQ7u;jql^KBK^h%wJ#PYKc}lhIh^3L&c5;LLgk=@8K7IjHUxBkT^1XT71>J27rm z)X_mh|>T*k&oBY}SidknwI=5$^BK*~}o*Q$qQ%UgpOs`h8 zxiQNF_OUq}OrzwsrH6kbqEli4wta_E5M0#tqIsQTS8oVb_|otJTTWiSX4adhvCbV@ zmuKoN(@r6ffh@ySv9lsgA!Bq{*Ju4LRfV;ZNul!ha{~&LhzSy=D_>E%-bYgR zJyEnUOd|mw3c9%`N{uOfmGnJDf(OjIR+lm{snTgg2|*#bfr=uLA@alElgZDe`LXA7 z-#;~p`sk*`eR%8BA~v<|8bhQG7!dUy-}or3mR=#ZPM$^jO_kOg7_&n+m=5K^&~$1= z`@Kh@gH}5OI|-xO#u&N_E9CB$H;5ChdQHg0Wb3OI@*&l^OwM&IBgF2;xiu_KKWqWB z^cy+%jN6H=<2MLLzZAmhb#~4QbS*eH1!g_kidqiZV>RjdqeX1_%OqdF6X|ue=Hupr z3L%4qj{{cu_^D9&oOAIt%F4-&;#Pm;M^IP{ z`&4a(ipy<(|C2uD6Z8 zh3r&@cpnhMH9Z#UY^GZKTk?58xf%JZ0Od=|dKRy)Wu|yTHpSE&AJj+>?GLPWePv#a zl;q~+9kOkOS!As(n*|}Yc0zjkv~F~2*%ZT{R;wisAJ>#QUvsOA-}Le!$M8&j3yQcw+26I02FYT@cjYw>r@e{`j)lAQ} z5$+#UaAV&4v#0kD8^W~niflGW(~As^?lz^Fl=7Lao@p;Y3FX&1^x!s7a`A9Vkr7|J zxy#*;FE2Qmk~|Ko2VLWV4QYUHF&*!|KmK3==B*Qct;x6kJy7^X7AK9bu$FH+&s+&E z>f}vrCC7}hk&kKUlQWBW-V(p8!>y}7E>XI^~#`nB$6M?2H^o!Rl`Tz_%1 zfJZj3)yVf2M?ktkKt>l*Qu}L!A7&GL_)-6+b+VO{;+~-kMBizKT(J3S)q1GOpeO9)^A3@Apl+uEe9}o}Lz158oRq(%PsWt>WIyiIfnSx;*c7yOrGA`;#m7nyp~} zX8i~Jo|(et#?EfSqw7k&-FjF*=gMTK9= zH9mA)T1pungb1xsJNeF5`=z(DmqK4JysBNmD6K&Zgu;5{ic4FRubE}2PX(cD8nqWy z_N*Y|t#c~jdz5TUAq`(-t#zK;UQ9QUkzbag3xA4fUUUX*mTCtlh|kyp&QQU_~U_k#dF<*!xjwLp3i_$Ewo6MrUf~jFOlYt zygHfYOf+7+I`{;mxrR^_{a3<2NFJ;1W)UYH`8SJwc>%toX~6fhH{MIBCI3m%S~SfIitBFmm(W zSX`Hw!L51c{EK0Gnu>kD&K)OCOrwmfctqk@RY~#Udb~6jx8o|w6lx8dJv2D7;UQIx zY($#yo3ca}1HEf6Ht0~>w8`bJ!0rY&pMtpFkCfQ0=0<%5WN?{<`BCE7PnsS0jcW*X z7wOd8?4faX$&b7cODAKL4z{Z(9!~qLtc?z9W_}L60kKL&u3yxZxt3qZf8>>m>F`=l1kS8gwq92=u94&;3(TX z(jr0GN(t5mSc(n8xAcetlHgOJAPiEisqXehU z^pKJ^i|jexRLX9Cc9=#q-lv64hj2fW=oWtw;W;*paG@4w9L;&CQ;>__DVLL+Y~>QK zuZ}-*W7K*X(Wvpf8&!s)&B<`lI)}b(IP^WlIWyf3s7k4$I(Q^LwDsyXKVUUPC&sgw zAL5n%Z?_M?gXE-WKlvx_HA8=-k=NHhWVsg)?bSCngVe-Up@X(6&vJR-?!#8K+v)zu z5O&k$3_=5cnTn(s?qXS{`U2-Pp|t654{l-&vE3%uA{(;e@miManr%x7W9Q?HZTcJ+u7+Q{pa7g08w0B! zX=LO_u_z%y3^J5>DRS@tm*S-S`{cuPsf}8@!DF9|r+?6Wetm4qCrm=~gn3b_M5%t? z(6kPgPPw^Njgcz%`F-5ey!Q(RZM$RAQVDDWWzMMwR&b*784FAs3RlAmwemir=IQNx z+nHc))NI^Z91y}0Lh|J4n{Xe)beQ$Z(zh9Lj0`E(>yexw=K^1Ke#+5Tqp}>;JM!SF z#X|ey2df7o7u!N&3Xca|u38a<6^9&7a=pB?^s@)yVe#7Ud?PE_fI{@ z-C5^lGnHZiW9$b+H=oJ$owG|6joJBtQy*K2Ku4rARQiMtqf?2DqVY{KIW8o&G2GsMa23 zXyc0VH_Vwa#^DOQh9zyix_sI{%{>#$8huOaSMEpE^80|brq8@Rm>^EL+VFEhty6Cz zh&c$p`?6HL^rwCO36at7u}%I>boaiuDbA^va6O8<{&+R56A%!<*wgt`>v}d4;MA>K zVpD*?=~Mk!&n~s8Ym3)n9*WJG62$`8C|#^`jBE5q<;MI&nb>;9nD2U(z8M7V-o=}+ z0-x)w{rK*U)BR!fx`EibyGXXZRBHdMb#TZd^rCqJTTFY1Li{O@KUmcd>@fefP%{;Q z#_!MLC=oB?O$sGm^f3ZCZm%x?0dI@DW_g?Bp}Xaa8nJ{rQG% z8ShN%D~mXT%TRW*5ub*F!M*VubGc>RQD?xXVrTNYhkVvgSc=_tLDwVyqN&+@N{Vhv&Qg4eOn@-jJPvr-icT&;!XT8Cn zpchq23qP_Q50_0unS{=hA*sL$dHUNi{izzh`6X?NkwPb+Gzmq~o4sF_`0hDY z;`ne%fjEVDwv>Z3-GgZSUzQW89}-uK zX(u*<+XH0l7uChw9S3#3XTAc3ob{ht`h332`5-v;h+MukcEjJ~ctJn8OnV-s39uCd0R>P#_mzU5%1T4v05Wph zlGSb04J(2|&+x3pD<~coYi~95BG1)Ti`uI32)iA+|xNFdBJ+=|NSbT0BxD z>c`SQSFG0Y{oOk%DMDpKN{-ql6m^U)@(>ROn7&G!(qRY_#RJ3n6|xiuhnoyXe>L3| zYmfcHHM6;E4E63Lk)9L_MJZPN{IrjN4dH4)d9rhQ3CT21ZpK+R^hp+=r~xKi`y-KYU$-+ZK=~5;Umh$B~S23lo6uP zNa$lFu&dKu)`n`qrcQt=YOoRdon9-NB6vO-A#*KPt20f1>L6Q)$4z?)-bY8Z zL*>uJ>Tj;Sa-Us2d?yLS&hheFIezeH{_z8S_0o)pi0JqzU5a!=*JOGRm1`%Dpu-== z4K2Aq$OR?l=O0(>m=!o*FNQstF7GadE6%O?B=ROC3a1@Y1bDa7=}}*qXWGoPjy4b< zXsv0oy$W+VIhymEx{!6YVpTC-#eKVp(Ge;RGa6>l(BiVB#K-okF60=fc1ll;Z|iL+ zBWM(Sdrczb;G9|A>)l-bTq)Vxn#fXZ*2jJDn-@LwthemMNowiwem~XrXZ4Q)16{@u zwEaSuxvphdgYV~nz@2HOLbmwBy4%X_lhX2Drkou19}$&&^ZBN5tEmo6EyL=b(I~nt zg)T9C17(CbFYhp0=?ST=8)cq)=G*2TzUANsX|q$}*@5cw33ow*?F->&tEKvuTX18S z@y}p87EtZT+kT}yYzZAA2C-Hh)Uilz$DA`O3sqj-^EyB@!xz1osOa3%T|as}t8OGb z>TC#V#WFgNq-i*M#3C=Vt6HnfQGb&$#WZpnf+HPT2fiw()HbrjN~k_9`m&GLgi9P2 zy-hf6iDv=UP|gr#x=YFttY9&SR4pPJn9mPqbF z`pHBmtN~tXV0j6C`dB7U`9j$@S!haBDMUJr%clsFaz{2XI>$H))S6~q1_oT~+bt+R z^E#K!@wXl~?^MiYFXI->Qw8kl?OV@MCOC^FemUIXY~5#nL#tn50knF(wP1`5mPS3M z8p1QO!HH5V@8bqkFUYYseiM50k4MseJ2;6AO!vctf){zFn{Q)L5(f1in`SxobkVbu z%Ex#zEroyY6^tSzExoOGWSq1|eRVR_W-~E+2M1rAL8+&sUMm0+D2G9pes|-QP5GY4 zJu?Ij^nf3-mhGg}KS$2WloJEBtREISHi+!NqBM_oJamp4;f!YC3#%{X-cO!<<^32* zg&W%wbRX)vC+Kx5XaJ&j{J)=cSMrDUAd{9Q1PT);X?KBj~th8?oJs(173ah{)sKEdk-G>!dJ?y zUR=rI?7SpHosF#k3bP5!m-SgV^`>K|Qip0F>*#Yt2Lh1C@bmm5Zd;FKKN}ynz|Nzf zP?4(mJkUfd8$n%}qL8Z=lzXN3(L%Mxy&Ko#s{f?F-7~#A)~Gz?+`GRF2+_?cJ8^Xb zLbqJOU5cQHniHS%i9~MmO+l}zsZecIOC56#f7~YD8`BKY^YK{i+Cn22Ban5M6d;Y^ zgG&rJ47b-4HXd1r1Uti8_5*lrhq@*q+Huxx7Fe&3Z*_)doMfmGYZsg2ch2^~^nOOm zL1v?P{ze+yy!Vqp3Xs!e(th}`4yM7{#}5h)R&aOMNoWMT<2^RA6GZNiV!5!M!qE0UcUuU{PMYW zxZ{!V*;^TfqsY;$+uCB(P5KInY$jWdUSeb|G$0f^x;r`0{SeyA}HGszoqb*KVU?NtL2DGlfWa|?s9x6o+Wy-JEg zpH}haew6+N{M_niz-+vhJVN$&9TuG(hy(_!tHX&rR}A8TAjY7O5I}h9xhJbj2miSn zG=NDyen}K_KN8I!ykSu|3cd&zYN(o&DTXz1NXfQ5nX1`|0bC@Ct+LJFb3qK)vn%T8 zNm5OL8~<;sr6PSjukq_cA1aHPW!IwCp9WtLKH@Eo85U6~PH=hCPPTM?(Bn8#pg!DF z7sK?Ck{Cf$Xs5_tMo6}!!d^sA&(L?)a!3=hJmNS%|7@b!3-(apOBo!}TNm_xedgV4 zcq=ba?zm$>Mk0G0UN)O_LMwY zql@yx1bkwN_Li(ZdKq#j_^X`&BljHE<2#y~mgr|o8)!W@QvQ~~A3`@$U+b;4de0ro z)Kigmul!e3FOF@Q|L?BBQ&3c?f8uPgwx{JT)Y#^>;~+?i?+30XB+zxAw zy50SUCL;M*zeu~IUIyxK!dxuxnt9Db6OlsAF)=ZHH*gAX%kRYZ?>ip1mJCd+besA_ zuheJS0vkdLyK8oI*B#||1D}j+Rrcf1<)raD4nK<|Wz%{Bs+}EI>xt(`UtOngKiUu( zvElioLRhGqW0kTc^j`84mgD&e}4g@ZTqS(hz z6K~Fq0n9EQwfJNVIFit3K)=FtCOD!-KlS`Fea6o0#9kA1Dc$l_an#7PX81jbrv=NA zcXz)DpK9fM7ktRz(lXW@n_Tn_pk#a=eiH#~#d3qB_Pe6kioNW21LoD41|jpe?{&%C zx)OPI+xD@hyg-$ zy$Ps)`E|IY3)t5^y9xWZ^Vk)Mj%j3+!KP(oq&gl1Bt2=<-WxR>OC6>&TneL`I{fJK zYpLJM*}stOYlPhUnZ5ISC;C7q;u*3fz6;iuAMJn^lC+?^fn-j$(&WZ%*q(z{B&AmJ z`~jeBm?UawuAcGJpmsTtvt>0yJ6VRJN`%*MHa;Lg{%0~5voaJ+A^7Myn!f?*9Ww~H ztd-pEZ^r+X5Y`m{&`Du;;S%uc7%qF+A@aejmS4b`e)v6WGG0>UhCH~?0pC`T^$QFxd-!ypmJ0$*+nntS%0L4Ehm*zKc{pBTKVL)_R z53ws}=kJ8=M{J~6Ii|F7D= z&g$m_S}*_%$Ds6&zh$xfubE;h02}qMoTL8(YEkR}FzUY`q<@!7{yGVCCdqkX7WwnP zL$d$OuZtJ}hJndi_Wv;;02O~A)ebj~@#-&N{0pr9y@=&d02pQEPHO-6Hc0|U4oe}% z_wUa4f9f2i0bqc~>skKqZTf#`_Mb!i|Iq9|ut4+w{}r0$4Gy$=DfbMHzp&IQb)YB} zj6-5B-j9WhO-ijgvFp)3fz42#0J)6+c+4$+#-@nlww4JoGUVIypUe)`RUO)C4CVyw z^s5cmcJ=XL_up<8(Ms&KCXPq0ZAQ&zmhzp<_`MbKILT;e5t*0EAd-72&cJkjZGJo6 zQ$wV~fQ>j*#{Bd;*tb^xdfY)r;cH+wc!tva^ind-0olk7K}HK&s#TYVHP@TdHslii zGtt-wh-_l#()giq$%wmEMt!Y=X2BzBq)b47D22yFm_U(#w$}g&zJl|_&EkqW{8gR7 z@#5>`#)bFwDi_*=L;@NZmVdY*x2nNh8a-tEBnk)l^Cqlh?J>n1=1*m zZ%rX4N5P=D(MAg&-<1O8%lxoe6MdS+06^?zv8f0w$5fB+6q zpSE09h%X8`QZlkX)Mtwvfn2O4$oKa900P9?`r7YKaBrci1^xI%n&ew2P|pcMDa*4|XkC z!HmxX#Ck4)yvt-fOQu@`9EI2AxtPcV8I9JoI+1 znGoj{lv8dR)24s^uKBR9?D8jrH_l+(!T2k4`s*5KB>+x)<{nCAY{t}d6LZ`v{$U7x~!qRu6}zblF<|a2PJIA+FwtAD38-)PIGzGo56#HZ~pyX0A_Ip zCU~gvts&g|4~Q14i;=6FQPTPm3RpRwR$*^%rU=vd*_VB zLy{lj)qq;>J6%OJ{i^fs1&LF*)MTx2VXe&pf(G4P*GE;;W@h)jt+|g^Fnm)KGX4+ z142#Oo1!M7mCfAtOGH8r^v{YC-p3KRD0X3xCJL-kIH8jM57a~40uY~uaD8g!raRpJ z(v-Tn>>18kivhqPYgl_H6qxD|EgRBN(hb9mD5^6|^_vs< z)i4S9zLEmVs>jP(z);I#8m?L(S;sC)aeVcde6QWSvObtg3|5e|F%pV#Q!bS}m70(+ z(8wn@nl~H>*>4Yx%OYD7n|R4tw%u@|87}6!DMrGKFMG*)5!vbWNPc3(s`d;cMgD*- zNQ0_2;1jMiL*GTi>Djl!$sZAV(7RnRbR0ZXo-^mGj%S|hzA&F!m`0y-qvh+KY3mFr zHNBG`X@12g7S_@sO^4M6dX?`l_g?T^lvhSdW9ZAps`;g~>rdh%%6^ENu!mH~gi15a z5FA}NHa7w4r_oJd*BviMYmB5w@x2rz_8KSh+?|sixu4-W z&74OY%^yuHHam8Tu$Qi`T3_vR!A)*hFD4s?D;;e{w;!ncoj$K@zAeB*WBda}Hd1b@0jfx{0SChxZycM=v_$21jDj|Ju^$_w;N4-2iLLuytxkgi>f zsG{E3WP6mD1O+&9i+3Lnr$cdrdnbuXzX>7rX)!>(6AkqW-{|`d|GxUvJ4r`9fDXntf#Z)@Ec{Nl;riNK(PoK)yI&Q>3| z7=&+5eeE7zZP{Xl7478yRU#Z!E9~WCO#I4@DsASw3EouGWo_S#!`Er2dM@t;fHx|9 zNP6ld!pWt3?pJN(EGKEm+jZvVZapaZT`%of)HWrg4u4bi8$op*Oqf>S zu8**>gkF_wLt3esdgcI)6)kcMWHM=0cf7TKHFRcs@@X&=aCpebtnI=P!vkIKQN>dO zR_n*6L&qyjuJ?|E-e%VFvD)$pNa*_)?ZSnxjEA}R7fbn|UnZ=$@mXBtl=1hR3k@IZ zUGo)nU*b!Y`g;7cPDES#6R1%)Q+Lo(!i~&ozGq!KcS#ID5#?C$u_Q``#Qq#ku2rc} zN9!2vd6&=M&sM#)JOf}JUz`c@**ig87b8WV{4(q;Pv)+2_<&6M^wr}uU!{JJ%93|u zkIWE7)c8xh4izL8SR+U=ik|*(1>KM#Zx@1-0xd;-Sz|V7+`xwP9Dl>gHIfNBg`47{ zu7i$8nq!N2P0n2%Na6gxXb2S&Oa)g9u^WLnA{!iyQ6v629^ZQj+3`lCt^?yIIROW0 zNSILnY*S-|QIGAQ5Wqd+xJ$ATH%CLZ)tgKHB|s9AHJOvQWfC#TeKTVzaFu$+{-=6Z z$SjQtZ@i*4iHmjn?SGcDJhT9{CY^aH4I9;(SXN$~bXcWy^GzWGX`hl4MBaE-!f zDy+?V>hrKV#GxnZ9Th4*YQ32fDvX>|J6Jzk{q9C#^bL?rCT+Fg#Z4A{4K`f|fPL4DR9 z*Y17`cJ(LCqpRDpVL}U!$(-6N{#la*{xkzH5FW&3T)nNtyW)iQylEn05i`eB9NRyv zO*LCS>|f0b#-O6$|@ zMp+rXDUgbjAT+h!fXK`{C|Hk~@lS-E6e2nIPaiqyNTq|Vr<@WuIGu2u=i|r3-t6Yz z`ox*>Iki5P<{#zqJIiW-hVD$=SH|%FvG<-)O>NsBs0T$61Vlsxq=|rZ>C!p(@qPG@kq;RRvi90@ z%|3r~E(bNkT3R!LZL{5HA zT?Pln&0G5OizQV@zW9=+fpaIT0diBqF8U93!Pvke-Zl9p@5QD(`n$l_joiXbz8=Ge z1G?`tx(?T@kr#|Ka(@mqh=gVKjKv07tq&TTDI~$N9oOX?h!UWK;qQU-pKdEx|zqkHP%yD=2<@|aXT<;+hl5~8R`%U7kz&Bt)h~hM}EolQAG*{ey9Y%DALfvxD#9ipL~}E z?616Y@@hqXv>GC-;SvZB$!$D$Pk@KbeM<5IP@P(W1mK>cw>}yEAMDe}7tzXEy=HYt6 z>Av>Pw_{1v+lHUQ!5iW<4*0W0^632DIXxmo?#)6?3{Ii#`@_xt{W`v7#Z`A935Dc= z;ECm-z~9D~lAyE3(O=IKgB5ttuX?*{@q_tHgeem&Is+jy1Esf&-b+l}0p1u0RDSZq zICxbmirgvo_xuhfly|`feLX=Yt)<3hU)F6=mAw#o`6lR9TFN&IJkw>=-<V%uA=7Bbw!M2KSKuL~nhwm%QP@QtJH zUEAFbocjbl5r7R>D|5%~yOEx~?vnV{E6@!@@_|cgaC~(WVtd#-K`w#dQ@RHg5U7P; zWw;Z6*ar-NkkT3OauU23g9Ir$DCAgi4-o$NZI}b3C+`3>*!*McFDJ8TyL^=EM_^sw zV(#9_`D zm;6ndbP-{cya`@;?4`p>mS&RexlFlc{FM0arz{JBr#*D}qF^`L5I`=Do@0h{3MKLqS1#vD~klC&h+l zKrM_qr)`0%Dlp&#p;A=}6GCKgj&hYL>f|dmW^?ih-jhg_ew;z>mJWew)fDz8Iw`}G zU$-yq$X%U>IE@kITp$MJKvHUuX=4~ zRBlEM<8xJg(0pbIiQ-(`jwbQY_cnAWy3YkTWIlTCwL}r^GRif!6A&84&{bw zCx8sj7_lv<_4~6zm(}N+dPyodPJKMRop%FNa-uS*zP2N4Y>N)5J)M-?Hx^lUN}ZZ` z+c$2+7UvOJewU`w2`YQhE*vwlq@}RiYGarGD8ce(-)dW%qyD>y`EY+MN38LAOvMU&;)qn~@X#+N!kVDC6T~G=SK} zW`C5BdZBq4X2Ot;sEf^64kWv#rf=Y?hx+nf6{0C$>AeSY7ftm;{a6)j{XnZ0;8M)_ zCECN6*w)bI6Y$DZnPhHxL+GN;8kbIYAw`3TQ0|BWH^QhP;jFFr!JpMz5*7{3Nc zGr74K+AX1{13@7V*c+=pb=%hBU*I^t03655#7#oMNy(+)WmC0$xu@}BY29teYugb4~sQH14+NYl8cqvy+a3brP z&+NyeKB4DIv}c%2#!v~_=AJ~T2tP}q)8b(F#^um`5ZRS|O@<2JP(R#E*sRMI2@`~eS4e!m7#nN)aFyENtj z8p}(;sS?*`BMqLu3i;DQmy36OK479~;1)kxrv#jqXKq{30$KB7yu=&7w?{zXv>2$@ zB>RYt>6zhz5{jg0j;-^&D~b4r08^Ny%PF%%B`ssMjT)(9UisDKoXy#gpJQfMbk59~ zzZDlc^pYQ{Vl%hA?*=|#IiDpT2$=fh7b_>ZX;EVwwEc_Y?Qi@$mGS>hUjCU8-1q@( zh{osfn9d-s3ynBOL-pi?O#zzE9YBO+v_bLl27v%u6{QyI!!u?*m@_W)q4zjvnSW;& z{q#tIf*`*KXCf8O`p4E=G?a-{(E0e^>lV6jCE%cjuPBsV@IQc8C&U0GHF-PrE*{%S zmjEzl;q@5Xiwkv*GJs&4Cste_`@n7_1%RrVDJlE-?-Lql?&fSTK*7$(PGrAN69d%o zvj*yUZA`B!e;>%7RZ@OZ0ID|8VV?D3YMf2Tj8^*Ld`^S+-;4o_^W+6cEA%UIk=492 z@p(xY^+k(H(%}O5dYobX99R+Yhc9eNDy$dn z;*y{96-Tx`;nmArZ}XkStxecwZgLo!X7W#dNLj`8lJ7{`qySieKW5 zs!n8W&=+Kv%KeUi3)+7zdd6ey__I&fAQQMCcaG&Y@NQ&N7klOf^pK>v`SW8q3xgdm zd=-trM>sO*wS|DqW-rU=UT`|VywpOGHycdxNhnb}fLZa99cL&f#Oap*Yfj*Q?y-J_!&9&=?Mxr#d% za3$RoOtSQfhLqF^XaeT4FT95D{D^Gfv z_1X^3i{QGs8@GxB%MFn8@X0(c@d(Um2 zC_vt8nU`|jO}f_6)u z)}bUN8-?XkUV;|@8f7{v=ya&|&~^QWamWdA@-;(U9@a8j^*aZr|2N0b`2q_+bP<~u1k42>78vM8(Vk9!2}+IzX~MU#lansh2SVt}T)vl?ncpP;678aF ze#FhK`t94dM5T*~dK1uQ0s@*(q@_DFaFT4Fy^W@R}yw{oHPUxBs`b+NK4h%4wU8N*@sxOzwEGFQ%s!mj1i%O9_$3^EI7 zFJ3LEcKugNN3B8w2O%u+k2`))@<-_W_A8*y9}0)}*Bn0;UdV1HdUScfd}al%Eyt@S z6g8DUg+v0i%<4ID<=NIg1qDSu@y9o9{_OM*U;XztzfH}lp#tMuBqWAcb(6_JpPm3? zv{OIF)grU!3oDBcx^F6?t|QnfE+`HG=He6=Xh>?{WE*qTv~ee>ORN$Z72Z9B`+#1$ z0qto?;@&#=XrZAz<<)`hF_{|n7oNenvg4C%~EKu=kOrA=FNDQ?k&A^)mhS+ zPCvEuC->8m(XhPG>Ze@+4Tc9)<8VPXUbfDGYidf-CjL+|jr~KWk|apz_TpHkApqQs zXl!XAbu+yZUHW^8rhD^-9_Z4Zo}LD;KY07p?XMwhmZonG+lnuCe8*m_P0hg`R z)qJU0Nk~nYh<3|gD&-F(Cj$}jXawx+vfQ%sQ+5>+`+wOL-m#Q;O=`9pbs zY@kCtN?mQ2Do@ODL8s=c%2Qt1ClaB)v4*+Yg{#*YBX0L*NA2}}(|549@@aJZyHNI=^Ai}pkp*bKUG zCcu&XX!q91IVPpAzNBn%t8avPZ6ozL)$gAKfcp@V<+Td3Hrfnjxhm-4PGNL)QOM-+ zcgkr~Tn!&uPCd^YWr@7wNTD_P=f_$Rk?Kz+)CoyT7f61BkG_3qO^`J*Hg4})sBpsm z76eIGcIN!ggy{ScJ*ocJ#w4^a!x|Y{BcdV}p^+{^ujmXEK_8K{3GY_Jb${#p{Okan z@HjWPrsNztO3o(F5(V#}apt+`!<4RP3X6#&m!&QCFgh-AxBNOPM?q|=o;H@^X)d=R_IUnDi)2U6R zcYQI`Op`=rmx&i002((Sk<5+p!2`LEw-^hpD9F$l9sNMk$Zs>Ciq_ogw}X+;Jg^5N z1sK7|m`TZS|5>oZXKgr3=aZ?scy~`f@|`fS+yX0N;v@{(xyy0%&v!W982EV~?mYLy z!F>SZriwf1>A)`l&f0a?d_Xq^`^yIj?4rN*wi|aPaJegRYEBb~LTzRK}s-u8*-$ z(}j27w6bJ1<2Byy>B*t=ATyzcTi?kLIhQ|=0H)b@;?aGEUr-Mr$yzDEL-38sbDzZ0 zYt{BV(p*UQbpR9Y6YP3v=ZU!O$!V(0As*g(sP@SB3w+=bJvU2JBBg4koQX7DCc%P6 z9;HVUg*ug$Br@Q9!EMn3N;R3TX_we~F`q}wU8kZWBJQ^~r^um#oP8+?bYqXMW;gd1Qt_d z+(Zj@ymji~B(1+pH&tN5px{FarW`94_kNXp<|T z7GtD2kXGRD4XRyoLlltuhJ2BV-WIz)()AHKMWoE+S6r(QT+22TmCPJT(RD6*S<>aY z2|v(=3?6zmMK-T=Z>d&6nwJoc$S0jIE!0cr(w2tFVa&~hOWy{!CrXblkm6BlXir9d zIewaQ&-LD!)b||il)Ik8Cw>;>Kv`n{DlaLgdr3zq35e1Kg9rOnfx+V>eLH0LgOb}z?%DdGvcD>=yK-BnPFr!#chdYHcr ziO-Ykv)`Wd_hMsNR7oo7HE>O@B6(;1+N*?%=Ex5{iA=zV^S%kZa{-I?JZEg}H437S zffK+=rGP}Ov9s2O1I@UpSjx#Wh;^+hNHg4UyVV?f``<*ERUUs0Jmy{XiAOqO6MI-_ z*dQ}}fHF`^lOxH}DJj(RQYEdxR%)stUKRWxA4a$biSjrUy63uYnmH;h#TbtI=Ol{% z5_0!gIllp_F*4*x! z^3`7ZIQk>_JUEb<$L{M#(HkATB`zIesiSi1YR{G(ZAXP7!XHE))EG%#G_!?GVp zAya0H7pC4SZ+Cue#qybE9F=<{CAwmVdzX?$5(AY#9p~wuMktp-BC9-i{YCBa;`!TE zLF?;*PZke%DzvcWRmO7q;ea)c6cgBI`S>y9vyZoje2LY|CkNsn|M%~&SdPu&Z#oiE zCNdNyC&k-9nbz6_aKR%<5z=ekl;ch8vp{q|o321+lGTfrnCW=G_8KU7&tK9n%0EcS-) zNu@N=a!A>8tNy$PxCH1jWM$vMU{KnN9vpgz&QP^GEEiOuT53JS0xw;K-b()@KSyag zo4c7!kz(3c;;MO6;FI8Wh~X*(kNWI;c1f{#$x8{9EVDQ&M^dVdKAr^k?HF+CaEJsi ztxl|$6>#c+ShP!HqjL1r;`v}~&w|MDh4iI4sarF2UEq; zot7nRat+9^fZKyoNkk6&GyH|Ek4B4*?Svd3Xv@b4J(G`SGb@F?$cq}_TQl$etUWAL z4?c!bnI4AfZ;TOaE~6J6M?2yL2Y$!*|<8 zwyBj4I%3 zRti&OUhhYi`ov>st4F5H!d_u_EksyUE5Cb>-F?WM!m$Su_gJBVcUC#61{J06cZ58! zQ=ABwrhMEHx8--VvpE>WzAfA8li>6@rqbTQ?sm>&b`n6I`F!woFZfBy zap3Id39ga3U>Ck0;K`-za5HetvHHAN)zgSbwI_>7{(VatQM!jEk1`Soyv@w1Y7T{1 z*5Z%)oi@ga!Sxd!@Pz!&FKjTYkh9HB= z>7VB2l0`&5`3RTM`sjc`nR>&$y>MA`7LD&;HtmOw#l5c~TT;^eGcF&DGnY+_h+B)~ zja7Mu>3JS=7%4fFPmQBi?As{ZN(2YjSix6RBWZnw^$m8nmpNTss>ISxLIfv2_ZZXg zEYaNCeN${pCQOFbGRO-*xg0o*f+UaqN*3wBkTob}9W+6arc+-?47=bRZk^&)o^pzc z@y9}F(898SiSNRcPj9~dXn1A&9bn8m8_?T$p3W^)IdinqwLZ*Y>vK5#P#=jA;_inc z+8fRCyI=(^TYC&eZ^lwO{kBg-seC3BX!6%1cg)Y{Hw)><7rL2Y?+sO$Lm+!)D5;MQ ze~6BOZDTd3bH@Bop&Xubt<#$gf-VDtHX8c26=iWb0-H^Vx7|2S_6T}ZL2Q~dgp^cC z29sXj=e6|M`N4^1p>6SN7CQEQndam|WuT<+K!MCF2)pmx9uKuU?Ov#0;UXOE1vs$# z{shRnz2eUSO+iWb$ylSJx0ie)ZyU2pjo;6s+L_%N-ss4t?GT_W9T1mB_3_9|9hY^+ z3@Z7Uq}}G0iPRqbLPBcN(m8Hcpbi&sj9$M1N(l9_>Mze4pMAGG8NM-x9Grvb6?xsIZALkH^KkNkTK)LqUSLNc(VN@)6I{G%W8$fe72(CQhx?ogBSk?#Jtl)0H ze3?GbnX;LPx~dDl6+vLy_KTEJC0W#Qx=Bk0fp#!ng@Sd;ZeS#eaB*PmpL%G{aSefq z89=3J!xHniz=^JXlCXcZgFR?5IR(;w`^Mt}hh$^UyK`#wu^C+zFTd-w(Uhc3_cT^> zJ-3qAx%qXa)6OGfLF0q2&nQa@mm&de?&d1hq-L82yWt)6U?&-f`=IcHeS+D@(QCzg zVW;~`F79(5o9G$7Ulkpc`V`G>U6#T#%kPB$G_U)!u}8kLL~KOjeaZli*u(U3O;{GH zw6x}A$R@WaidDF3dif(ko>i8cj-@38yzo7`zIiJ|_~q9U4lfLw6aKTDu|jE5ORqU? zvrB7-BQ+*iah4 zeNS$_=GafWy2P8xa&YVZW|D=!a?)ORTGl+b>#LpqU?CSbRE?Hkme#Ifk$xJjaP;e- zQ+SBO5}K}7A8%mc z1g5|k=Ve=igx$9b8h(|}@DVQ9F~+ju8tI)A@(n^-ffxC<6N;a1v#?80`DywZvo=$U zxWs!u&NS-Z%&+-cMg4l*!Be&(%Z5s5yZ_e4J~`6#=)LTUIJ)|gdZ)MuN23WLS7gNb zXuert0QHpu>t}0pbe$fEsbzDUn%f#AQK!74oxuC0a4|7OIAl6=m6{skq>A+D{ibgW zZ-oZ=MEG-Ylp(jdvRMQt2Kw1)Lsg>odBsV%GoJRZv|oMM@q<0@fmuzbRWbYG<1&IrBXTuV9M2ASX( zT!K4skmC>xDMSa*Fu*+81NwJ|@`o;8PeIm4$HbOS($|W@*z(I2X%hJA#!^n3MSu|! z((~0_GDwF|rjAr}GMPpnO zF7Siq*K8xh5>~VX+~k>A)Rq@0<(|t%m@1ZO@2nR_%gc9N>&tK|Vhf=r%rCVWVYs%_ zjP&zfZ`N>SDTMsu)UGm09bb+g<&2Q&=qyC`wIFOl?-#K;-`0AkVD&s&XD;mtzwzyO zaMh!nq)lEnKUA-vR%?r$H`rDEyWAg^ZqID8Tx5i@^ip#hDHsL{J%gHS6*8~)V{jnw zOkL)RFVhvkn<~0)-!oEH`OZ-6oEB38jLM>rk-J0g$|qMm;#D%E8@JOQWcGS{xnH*Z z1rt5JYUM7anE-`^N4N&N!Jw6`FLta%_9_uDZJm~q_f;E|&6pP1-nlz@`moHkdQ`ND z;;#F4!Q$v@x&o?LRbQCnEAXTQ-nDF#D{ht5P|w3(VM>nr2`*;arzO^$q|ISGyGeC1 zPJ~>9d3=c86=WBALTYpS+h4K?R3dH7AvFo$kJ_kv{2p%+`-5<^92C0ab3DI_Yog(7 zugp;Sam{DL{mnUE8J}prFj6=FN2>pnP5KI6&CK2?LLrpkszfKw&$c+9wlE-3VOjXZ#y+TT7=4QGt{un0edO zRhJ$%10+hphbZo9xApSDG!a6;Ziv1s*td&jZqABxE<2Hx9Hg(c$e%^RebgpDMRuz4 zR+QhU`l#qM{4m0-Vkqm`tf+BSAwhmN11`Ze&zeEpZ`^CWp0om1-wFZ-U9!~SF*g=R z$Gz_98!MnjdTnOjHKZwG)H}S74RJx00VwsbkbKY1<`&37Jvs+E2tmg>(&l2JVn* zMK_Cx;e7QLU6ZLd6Z6%)YoBf>V^)i>U?jrkHcxAc zM9SkX-3p^P?)zN^_6k#`&o|D^7)oMq{lI|%`=Ru0p)nTMOBR(q#lBo(uo>wb7Elv` z9f-=jS)07#0P0XaDrg_e(9*?$ky5!SmAi)C6OO%ee}PhOSEYtw917m}qGj3@dfDZ8 zXn1`2STIHV4$pB4MDksMiE$Lv$>|!%LlHIOf!^;jU0Q`FDoO7sL~*k22HTPfcmXGn zp0qS#N{c|~`RrLq|13$w)z@0iKZNn4aGeItO(k>y)H zUm2iv=0MhczWn5IZf%ZpR>a1_-ZXtnZoS0mJpiZPCnIP1A;WN_*VKT*Q2U@qbx;V~ zB_doBjpVYK$=`Vy8Wsj6;Hqx3g)c|?133n6_&wVeA&+d8Rhr)YP-Qb-^QnRP1F%W| z&#%+UP-H?$A*$=(WR$D@^u>E%_=c5S3p*CIH2ZS(mC6XlPYTx7Bd4EV zYnd5kN@wO5nsw-G2DdOvuq$yXQ2CMN2hGl!ILX&@G*=o|cDNg6^ozhlD^_nHD3d28 zljHW&v{#QvXnlX;4dwIYEcRz{aCLYOju8V;yFbuXmb$Q0`NrBcVKw^{1k`FaBIdtu`To=amtOUPXK_RmP#j1A0bMf1(NSkBb*tzJGT+*Z` zJtMXYE^&#T4jefTB{)u%=9ToVphZTf~;!9F0oG1 ztv)VfmTcd3dKU%ra2^rQ5$8?m0q&gQRgULTcYdlBviNfD6OuEEq6j2FJ5ISi=llcc zoAO+qzLsY4af=~Se9s~daWo?G&2W8-xp#23_eB;stj9zGc_Gl*t=$r@P2<(j^UFgFurA*2$(v;0;!c#W1_V2M>L z>j9eoyB+rbv(W&OjelnG1`mZv5<-pE;mXQshe#|x0yrjSJ26+j_q|^-Hm5XqoZkq4 zV+kt|SDXHZ+pc@C-O{m9pGLeq+NY{BE(>C2L?#+39}3!R>F!V(j4T{*LVvnL)BQ;d z*pF1j9}TV+mImn#M&^w23k3O6fqgnW7OKLtZt6lRyYzZDB(&@5O>7hZTHsL|sNbpX);o&E+o7&erDW49N3% zxJ`Zt+g{ntnOn~^k843f*%*MiNl^#eoe|Yy@C=a_pi9Cc9T=koF~ zIzXmn&2M)k@LEVL-Ps*p*)Lh!E=zmFrN%73YwlD554MhJO++|#Wig zSTWCZi_iCEhN33(H%Elr*1qUDjdSeqy&K3$VdBjY<4jk87OM2!wCxEiUx$hF zbS4{UvAM2JFbIx2CPZ-=bl%82*i|6tyMw5a3m~MmN!BW{B6HtwLO+#$4ECIhMw!+( zobm=$Vd1)!E)^YPDAmFcNeV?h!4-$Hy6`#*))-PbPw7wSZYG=f;YP_<$pF zW%KW(DaJsaIWw)C%U!Rnp!vR?aP+Hi4xkH-T6d>4vLQ^F*J51P@5%n0oKYdg7GD)s z&(ft~e~_Nnt@lz}g{7zYhqM-Fj=XBp(c#qGGyj9PnT9a0IOM9_{$@*CzRh9D-9r7I zdC`iagJM-?N>Q<1G~-7J`yt!ZKE}m<{{!#gA0A-2Zq1eUjb4Za7@p^39!fHFY;?u7 zQz`R)e|UTRCx`aU@#Vuo61^NP>*56Ps4zN(>7ZRVgulsI{uy`^e4;R7>wMTd~6R%UKvMxwqWk zHg6keSGR_Vz7CW;xN*QvDH%p9CI<{#vVmas_qRVqJ8JEM=xwY};?nVY-7lOE>WL+U zg@skA^e47%#T^Yb(60c!Sv1z;mHs_DYP_)?=<(f^EK-Q`5JD>%3JfYs@P3MqaK>-U zAIFld=$0vPRlLwCv!bVLJ6=aKxzDs>T;2?BPGEaiI!8NPDDxrk9O%(rpH)$AAM?cP zTsBQ0v$wI0Vl!=SFYZ<&u0EYuQY$dRElN7EKz%Lpt|}D-F?(QfL#Sl#ZJVXU))Dz> zU6ni^s5f=@j9(i6odX6G?*4O`g*gb}?y-ri_USG06acwbT&*mFYQq9GZ$aN?=8a?W z8W}`!s86rzi@{Pml*C5+p6lEjZdSJ548s@X4EC%BjLudy_J-9_M;O?*Jca+U;!c~D8fAhPu}3ZPiYLT8&MA9Vj@ICEetDT0$6dK7R(P!Y8^g^P`tw8dAWcm`@;R1&mw$h@Z_cbr`PpNews$+lKAp{) zQ!f!iEVq0Y!l@%H@6O%(NP!Wi<=k(-(UdE`EjpfvCR%oa_KPju$oa@^WSzJ?oFITZ z*CALOp~(>%db^nF+0gzBa*oHo8?Pt46%5CYX!3aNNdm=hVYOBYH+`4krb6psGzmQJ zbVM&pmBD8AtH37!yiicpygBCKDah;A=UASn*+IEczl@QQ)9w~W*iHIioJW(A(S-N9 zNAG52^LG>e;HUTfo{V5WORr!5EvjWz)e%Wqi6qsUPu$Vd>GOEs!y|*=2FNN*zG6(F>Z1}Cq|O^C zJjR{(c%@l&RjVO0BSfZP(OD^v(Jq${MbhVKZ!27x-9uIaT;jW(Zn2)KUL-X7>QGkz z8-dE?Afz&SDVkU8yxb?6Moev3YJ}0*-^;U`>#nsTeMpYphjB0iUJL zN!88Ia(F+A92yRKQ%CbKH2b>}g2?aJm=m@;VC9YXyrc6!s{(O}Fvql*t4qYkNb@1X zsM0Pdmu9}*xSmd#C}nx&;l2e1z0o{MFxNfZz5C63V!R|(}{~pBf^&EkbB`+ zH@RPr{B1^Ee*)pc+>S6BWK{Ri)^hT&aLyf#_}W=mRKxa}hUk~&FX?qgQrLIWc` z8uL95>k5fMaw^eUSTzVafnu`yQc4I%?xq5_c-^Dyl{F&(z!GGsV&|0iF5wI_1JgG8 zdjIn1M1YErSQ{H$#x<)CsI#;C5<+$;m$Vq~E-fsGJhW95j8(NyLof2Z6J|7Q?almf zRm-Qezu(uihFfhJ$eg!3IUh8%+XfObwb}lvq;$sY4+<|KI}M3Z}~oFHqKQ zEzG>zr`+u3@>N|7JG@?YPbl$0Q^-=X+P4=&JqpQ5CiagquYv|`PQ7dQk{>*1%aVz` z)d^lXAlv{FX=a<(n~VLh-K~p&nGT`o>B^m`GEcFdEKqmuhbjv$m{YzuuEvg3Y3-bP zOXA;Ma(ww{FbCh&U=RYyZup`eyvPq^)bsV>9VM`~yXrS9Wu3KA$OWgsb%cm3HiE}g zKEZd(HNewzZeX8h`s;JE?L7yE^p`m~<4Wxtz*V+{HPjInh?EVPA#rsU!Mus@n5C6A_kx^ z4wS2@F%VG!>S3}iuD5xIqA@^tLMX0+kW?l{@Z9pSpHbgH!l*iG7pDuP?+XCV{4;B> z2-<~F-|y`DmF*g_V&M3Gm;o2+BQW->QmgIqIb#9wo_+>^2VKy)f2Da7^!=b9-eEjM zMA&+NfRya#avMCv*!_y4dm(k{jgl87QvOB&ACocViwU*#*}k zvq*eSi@K`$GKutnTRz8M??V-y49*l1KivmRK~#;T?Rj`bx@@2g%&S@)b`v@7uQj@S z1F=;p+^ZnX1p`;EcByD*KIr=a@Suw}>cLv!1IJx+zN;W(CQ;6s2;X}iqT{!x_<1pYwXd4ztgP023O{5;6c2cYcFm14)v>lXn$ z9H@Hklyu$x73SliKiZXQ)g3Kp2l~i;vS99F2cR7|;&~5)27%gG536=zIiKk2Jxj>; z97Vp3fZd7b#&mC2nr6r%&v(1s#C;y+HT@2sX-42K6LH$RD=pS#54gqvgxIvvu&XPq1TBF7kZvJp$!UBXq z6)JedEh%N#_)Iedb?yKW!Cu4j4cNL$x6e2AFnFk@UvVv4*lo^OE*)CG!}E7-_2(%H z|8AMfnmqlNnLi(K`CpF!k|_+foNW~^=q#q9qZ03be9qFW<0#{H zQ`RkJi#N+JQ+~TGfP{0<^_esm;my04XwPbf3e0n76X?}qN|&a)VEQ}3K^;nAE$25n z-sC?MYJ=#1Wi3`(Y1|ko)qNN#e5KlBJaIQD)NeLZsFPeBR|~l$mHDABi*WenPYe#F z3xFVU(nCFM$1si2o~x-_)XxfLrT?spd5D*E5X7;Izi|7#Fe@8efLbgvDv>qb^B8>8 z5Fn{c@{^%iqFGB{mUdn+gU952G#!9GJ~IFl|1tnOCd7#TPpTlSog$XS9JP=?#!w&m zTO{~B;xj>N`zsZB1!Spn{bOrHVq1Rzc2$`VY7CJV9OU{mul2L)cMTJ&Y-i^%@Ygxq za-$wFN<*mzSxg$6XcgB;Vb9YZXZ-!a_Uy^4UH{rOP=wG>KyAqj3|jPdY3QeY_YXcIotwOP5Yh;RPV&e-DLQPQpcyJ)>$iz^GNi z6{OPCC6}?-+D4fdw;Qq8og#ST3rf|S&oP`B+Zy+I9wcyG$FOIon8Rw5xlfH}SM&1n zvahST`Fz}N((wtu)Q02-0Kn=1F#d>||s`*n|e9&z~eKHA_;eW2__X6iO z;>r5*I7JvI`5ovTkY}9Z$18Gg{>LXm>djlN;M)yP*?n&rI#8Gj%GR_0Z|MB@q@@0yQq2>_@GUe$jJ$#Uljvn*>ey>Kn7i*tB z8^nH?Q@PL1iOVIykso>Kdicd3Nuw(d-I{)KDK>5}liY#61)yFtOX^VD2#BtW{Gl)- zsY)S?&1N*RaYnWLUrP- zhbTx6wuKyWT)jHq*43vueC$eVphaZ!LMNHJ@&kJL=Pj%oBe+VbG+opcfwi6E zr}7mEJ_S!09ZS~gI@R)=KbP~mp|pxjhuZno@}ElWr2LekBl zRuc#FD{zzkWr$0aRFjcfs^X8kE>q*Frfm@+YH6>JU(MUY?_GL)sa~WSev+LXnHyF8 zzlHH*?l?kSe)4g@(L2-5gUm{Wx}%S{3LokSagEn`cru`?nNQ0I5h1#L*+uX#M?}#dmX!^K-{{tF}?HoG}WM$yTT? z8(r;*3KQ6~yX@}|yu^`Mc(E}VX7ER_Ya%GN%qmEf*W|0BUa{1jcw?(*4%Ta0lLoI1 zU0z4u63*85G48)bA?jVC@3R+mFo@qudwrjBW0w-XzoGo{vOX^nw7$a-=c*3V&}QAt@A3U$qAV#G%iD=x-;B`{qcgQ#`9 zQW3M*sCm7u38vru)&G*GA+7JL?Q@LnD{54(S3e&PDbdj5v2t23NTX3)sdc3Fc!BQ7 z0E^{@mQ1IBrdtpSTO}cQPg7cFK(tn&ZmuB8!E$eTIAqzS{Cndhj8_x+jeitTUcQPK z*gs@oyQtbXigmU8fc^uux7j~1FD&?+)Css0jh&-~S_8yN^WvvSC&!KWQk{u=Ne;d= zLM7E2oZ3SxpZ2OYk;6PFQR!{A+mR>YWpO7$h!dUSp0OfR9-~1W?Fxs$eon2Yp;kJ| zoGY&eI{EK>ex!A=B8qr&rnT?>t+h?N4;*JC9D6r?N1}hlM@Pb7meVZ@d9H2?f)rxr zN&2)tU@%4l7FD*&%wT(OzuyghGD|@?=fRM@*Kj=96)jTz^JWu(IP|e8TiggI@&80N z!bY=w>QQ>*$SgF3hg6bN|aFQ%V+y*8)k7c_fE&I?9an)XazHPl*NQX zcIU;^O(p|Om&%@R`Wc%v1*}wyQRMt^p>o#+TdL=nWE?BwWouUc=+J5jY%%@xj*yV` zPNW3Z0&3HX$mIu zYU^*t1Wax3)`r>^9S^)kZI6S0y2DgLhgLP|l!>adOs9?=9rhG1v|!s5=smW}0Y-pkEUIM}-^`7&t1r=?5N0(r6F%nD5{xN%(0;_2=;Xo+7e6nl6 z;-lZp|D=ETsNHZTAzNNH>{UQ{@5}@@4|10DkC{6g`ajDZ-$%#)3UFrLU45Nb?Tn+0 zS1-^g_ zZQWs2#rbaL*$5u5g7LNaXG(MRJ1=*zV?|e%s6O1n^A5}B*iF%}>q-tzc~nQBY50T4 zY5G@k2jGLwpYrHLLA3EupKeaxygE6gRw+2LbgN)YsRpl+{=;qEWRdoh%3`{9y=^RC znU$}hRN6R0C^y=!27T1SUVH!6P!Wz`z&OpRen6KVxa^+Q7`abxQAHG+YInZfXoarr zu}MHJBOT8B#4K0Wn|N0{N?NR zWJs1NC9^Dd;oGP=oAwC4*))$Fpm$_oXBM z2Z0v=oAS(@w18Q$@zVzj%`-DLqOj!3aH+W3H+TG!maqLjnmFVzECOR@`|!QcE`(>3 zo-E|UEhaa91UM}ThJWp80`m>0$J8gYv;s=6aF5;1MYxWWR}~|nfn0@4(ss6{=nhAb z@p~pC+1HHjT_Gng#_iA45`e1fXxPJ__?EzBUvYH6YXESoCf&?kJeNG$yGtJ*meHsx z&1pJLmPuF~aHfA2FZ!4~cf! zHv`6d+se(^sYwFaF7{D@vH{@My8P|d-nYcK|2T2a%`tC!|ElP8`2O}m$d7BaNs|Vy zC1IjNxz9)u^RN-t65B3&A-65Rxmoy@L8(%jlyciMs(wb(fl-PE+dCT$_HLR&JqNYZ+^5m$EzKbWyj*8LaCH zZ`?7%V=#wBD-(wmAq}Xc@ZIgPxb(47kKeI9eI-TjoNpi>Fdi}N(E%_=<}*nsNRki! zyk*>KjI&;!;q4uS3!=NhY2DlvyI(fk?%*%fd~+&vEdiVw^wyK8Kw>0m+D3LT;+o1Z zSUcmFA;o5L6SaG~hO|;|pao*}a~z?D}OLSlwL>L7^{lX}S{207b+Mf!k#* zyT%Lz{O+C}Tj_N`RP*OUeQn=*ZKC8ow|ZqO<007rqlOv zWoR3AyHdDegN3GUsg8DDezMDaI8D6#h^{X2Wlw#BL}%ovc!H&CrrH`lTsd*9;+9<5c+;Q{E(zrG z5s%`hAZ2kWR<+6dSv)D+2X|@+>Rekmqc=e**2HdeM@){|nb|Y*SHX5;=nx*;ExLPT zy!Xl82eZL7BFQ^#*he_%^$4Hb_{hJE+BBysQB9xWS-}#04b(r_x$W*fdyDECCoMhY z{WP_;w*%CN5 zVH@-e{vY<^FM3do{N2vyP0Ilm_y!oj4?xsD^qO>afX92T9m3`yybO!A6`7?JKEim zRNZND?bN1+1|S2xO{8PO*kXg;u3)lamXNd=Q8nvu`udiNj{^th{#cp%Vd+P1OVTZn z%;0cv8ZiUad-msn__1xd>2yy&S!!J=ODYH0_S1VTEBvDqL?DWUZNyW9Od!Ih;1|7^7*{%nDx8YEA8ib zdA+8Kb9)cfrvvvFS`%IeJw+~SR1{>!1QH1PUhHaErrY2nrm=y4RrdLPuE$Nhsm*XZ z%Utbn^@Hmt(sizYA2IJ)Ng*P3k4Ca)`D+nXcYVF)o#N>RlqR5u8rak^n8LOIX4<*r zdTC{#5YX|h&=Bu-`}DJav*z*M)h2uMQERgROxD4M6+)RhPl z`*_dS&A$1u;Nv8BD(q)-{^;7pLMW-rUJNmze75^HL(RuJoPl!G%BnV6MBpI0P5m`s zYy{@vPJPx=vhS4m&zlqU1g-MP3+b#sI`RH5q=MjBS(Su(g6j!%PLW zRWw@WECO6d@=@}bzd&ICHRQ4vI*JTIU>1dQk($@Kwuz1Q+YUD#Q*YFjY7$2}y-+Ky zl?7iXA?5FX1H#h2nl#pV49VU1y8wmg5r;X5nKFBYnbL$e5;E81;(SDX1aD9o^SA%n zn3J*^7TN1)&jH*^IgRh`8SVwV9{1!mu=`}#n)_1(<)4s;FaHC6)qmUh7!72@ad>JN z5(sie)5(2SUp_r(6t>Zeh3HY)}xc&lg#?C|AqeFioB7m%zQ?3@)M7KEE7-$Rp0e!w?`I zeeStk4(l6s8IAA3V2Av-IaXUQ@3tyaGR6{c5nE$fKK62O^D)cjYJkn_uL${hZ+*Tf z68l9!XM2TyiVlc4u@|+nlgysKXNP=TZ%o;5yvG*6p9qt@Um8mxdyb$3>&n^r$pYS` z+MTnkRcv@)Z!MA`Iy)4Qegk~Q`4b#Cke zE~`Sr;?lx7zP5H`0$*2!-1Wg)X20C2gVKi3m`;ge2$FxShMmzrMd-#A%J%mb<@1RA z7EB$e&Xl{oPX8Dz>}HbF&$K2EP`Mfwk|Rjw+UPGZ9j&T^0jA+P@A{5gZNS19j%742 ziGVzscQ-oCdxCLujz4^N3b?Z~u--~sf{y5-$Fv~AI%H@>nqX3`G4PqO>OIDg57Jc` zl*hf`XtMqpG=QX4jQbxE_Rr7$gE9~PFUK@MZ&yF!BadyLg>FMYtJH{cX!jG)@J zLn~sHW&YViMST@s9#_I@uvCz7siEPCd4a=nh>^o>S#Fjrnv{S7UoJd zoMKoSCch@}kHHFJD{NWQg3|b)xF|dCT6DefxY^jU4VJ_#`2&Xwx@^7aZfX4NB z*5m#!=QapKxY}VQvdue({Yj&L9EcZQo8JdAb^i(CMxfuH3;u+sMR?3;&Mrhzf5G;t#_` z()RUCj+gY`7AgMUMQ%$>_OQLa`n0BpZS-1Z$n4+#5sa9se7Ca4!6I|j{bGZCRx+od z2v{t;WDpar^1X(6=D(cHms-RhYaV}JO{qPiAOBYd?>{&G&j~PJ6P-By%cAH%XERH6 z#{>h=Dzp7DygmMPI4S}O{Ek=-TX$My#J@6pzu6Jm=ReZ0FFIKJufv}y1tDgFI7zGy zIkD`2^fM5#ExqO6bFqX<=lm-lN-vGLyniDOX`2;;&Mszd*NS)6FvK{c%d3I(*C>b{$El5A3>Y{ zE9(EVVg4UP{kJYM4uJj3!IG!aLXCOh``x$Rmm;Sw{oeSer;y>BwzHd|8{aPK->S%d zo~K*>rl4u{k6@4?uj8SZ@4C%w>FYJdu<2Z^oJ3TY$S1jv)X3x0KB!s`X9?x{jYiR= zY|m55-XorX_>6k#{@**i2!^^C(fzrV1(98{S1VjpbBECP7 zUa>I3_|IB0o`~L0Jr!GZYr{a&XkxWMO#YKXU*t0aX~g7F@jzds-Xs1nAv#?20R{E? z{t?5$g|kx*VTJF}d3;$B=;D8@N^-lAO8Os7{#N%#)vSL(?FSXI4}2~a5uNd{!U2jZTAkR&OD z!%0NX8zqMDJpW@XyiaG%i~cAgsozl%bSWR$v|m?dIVNM1vK7!^A=VHtHjU+t&&#kD zE`)h}6NN&S|22rQ%Tuv?l@TkH;A2D}CiUUsHrDYCblcGx)G38n!UGvk2rD9p`_B?c z+6{JO<2U=pGHNr3bk#B-3g3Uzu{QAvwi1B|W??j;#gRt_PX*r@Ik!3w3y+>rQ_J*>0&tb;bLP z15ppxC|fAq8~$2YTkSc5z?&{oNkKc?8lD+?XQzc|J;TDGN6R=HckQonX7RSkX-!2} z1ua)j z6b)!9Dvj#ZuS;yEa_IkVZ+x&!u(kPh>_M~WYdv!B8~&8)HpWo$st2b)X_E2Jg{QdR zQ1@7(*v{s-qa%QdHdR7U@W}wYgsW#sSg(t4O9CP;uqH#Lyk7;bh)ru4(hsL`CLyC? zE7eoZ7W%|dPUUH$+?6G)xr`AYuGp)q_Z^~=kH`5Hwe$c>W2YPvHZ69Wa0NUnhUsFp z`a4^21seoklHFz*DC+s^(I`;a<7kB$BblAGIM?4|rm5Llv2@XCsHNWwJWh0Q{CEtx zGT#WtojT7$DKs3wyX2L8ynZL0Rd3Xb?qbyl8frODpQaH1RG^I^d9;p$0)yx>lUlY$9a9p{VYPNrW0_tenxk>-|qwHnZopa$S;IeD_dI6 z6gbyGyls4IhTG&&E6qCbhnfgY><_%AFw*L6=ae#P%=s<~A9%eYM{DZuVf8*QV-1hz3J@=?8V|!cOp1 zpv!7d;t4eo;njD%59X^;7mN*?^(oG0b3+Y?@LeS;c&fl9D%moy|5SBqR5Gto||UfBP5J@6wiV7s>F$_8*S~& z9EUg$K8z_GH;u021}|o68zfpF7e&PD+>=}MZbP>9V3Eo*{V%yo&ys>&uhe zTq$-!s&{a_)+GCdHr^BgixQh_Q!=-qG zkK8!uTXHyuS>f<7Y{1vn!)8&v_Zi~04F~Kps81(Ep_usI312@RZ_|*gu*hOt;Gwji>ziZXP?AMpe+Vu#?o=Mcko>R15qP;voJ#A?Tba!Gxh0wbC8| z41g5_k|2*3W51`|m)hbsW69rh2TC|XI1DB_q79}w99uogotG*vT@VRNtaKM*^M-oA z0x&npLZ`rQqc`-)O^lpLh%*LY;P`TAwsEi@=E&Tz8k8f0YKs)prlbiZEf zIa8NpQzqkR2-Z#?JcEOwzXsP3t(rgb7gG_UPEsAJ8cKy>%Y8i!i)H?bFrpI zH|^+99vb$l<5SBEV%Bn^oM?sK4iEfKoA<`C1KTXH)|@?0a^Y*sK5XyJn>4dp!ghlo zSmK)xFPFk$pWfxYoNqe$KDFPrL9PYN{3-Jr7 z|Ktq21)wN7ITEUgT##7LHq@^GZPQ*Qm^G|@b?+Vmdm zTFm%034xdo99PJ{(C*02c3%neRa*tIOEJo$>e8_|180S8deKfN7 zWFDq`u_Gqj&H)Stg!c8r6?GPD6+fVcMBX=ouq$};5z{%m@TCHhUFH$xmmDTmlBkO5LQ)s{@V>Kmusdd4f-8oMy)J`QA5?DOcVmtWek;;-uz&aH z3oh%W@mxR2VdJDp7~W;*@?^0cEAjK3Flyc&LlPifEM3ZF17b8D{n&_g?{NM=N|F<> z|N3)^n9PyrEoEak2CDpU%F^4(aOJtM=dDS0#{0+W#j9uuq|%2X;Bq4-Q^kZ-&0a+x zD5Z}4ijObdYYV7UO-@?dIchEggV7sAOn!9c)cMTx}X|S3= z4dE3-HKq~bDG4GlP*U%>RHHgL!}rM8d+VgQ4Uazd+|7aLUALt%v3?JwarMf4)f=xG zs>#9W|Ib}WQkJwb$fM6Q^4JzfX_H2KSRUneCFy`KW_h@7miA-AZ(;YEO*zjI!9fi= zaL*INJ!x||bs_e=`Ro!ihBQrdM4#9ClLBPMzRYy%q@`fn++p?((&69wbRP@_VKg(H zGP*E0AP*frTLOsQyN0Z8I_y*OVc?d>gDdqd1x;x4&p2DU4z?V#QNP05=K;BoW`0~f zQ+cZ?27(|E-Dgf*CY+FLk~Z{=o7O-Iu^EeSNem%=(0>A$pE zU8e_QHMY?p*-mWcxvy*=uA_v&L?Scfo9# z`4fo?!bd7lYZv*9X3HK7mSlV2K|3c%Fu*sJF=}zMb`&o|i?#V#FcaZ%L|LD&RGdhI zQyEs)_)9+@Fr3VsWwyImVPRA*^S08DN4VJDT)64hsFC1S%*Xqol;dBucdXrK0F(lTw%BaB($~-$vl8AUDeaQQu zRA~*_*N}J2phGu@Mg&@PaT7{vl*!UKm>yX2CF*X_8n^wx!Q`h3U#a7vNJf=Ydzbvj zl(%6AZN_Qtpy#BQDiYOtn{U9Zb0dYBu zo_;*<1nies4Jnfo-;K5z2XZbW5`-THqAk*6aL=4+=9SJzbm=Oj#ILn^fY}vG^`rdKqGMP%YJS1X)K7$v6snt@t~C{Xv{1Oj&a&B!ng$PB zDvXUc6h%x)YK@O&LWcrXzpi!S)jB5#-M%Q%uLK#6ankuZB?k&KG<^Je3hj@5<TS_DD zQowH5CO3zu*2PG_o6OT>;u9$Yc99}A(O*N@pS2RkxfZ}^yDowQ@C7olW?3Fz=@zNv z*oo?{PSq4<$jV?;k&9Zyj^v7}ca=vvPii#VeBcAW>BaQSx+Q<~ZlKTxP8X@wLZz!f z#+@DfHj6qoGv&K94OVM1-HkYbODZf@h^{i_wq<*-dLYZ|;y>?@ZL?F@w10tsts_7C z%*&OQTkNw`jzq@ze15+0U3rcnP*2?Fn{mvs1MBj8^$=C(?BKSqCI}99WqQv$&e$>T z<*?e*M|XO9mS4%t)!BPCQX4BmjeN+q#S~4Z(P66gzA202U6G2M0o zg*MT$Lc3;{Dfn?OvU(wuuZ>9_8<3h=Z@b`G5YUUxlM;2fRNt$TZ8+)IdQ3g%I3u-M z%EP|nI&d2gJMS`CS#GYG&>5-FZ`FE}dG@>+MCk-JDA1@n!!d}Uvfpq;sa*RKO; zzD_o~?qw8XwB)p0fm#plka;MGfA-qH_|?-&*CaSCF296n{|wdOF{(J5#QA1{)pEk9 zr&ecbWZU^CkK%&xZIN0g`*g8t5&{MplutAKwC6#}JB#Caw8+7CAZQgX-XRlq`nEVg z+}dgnjafM*b`YS|v*`37R{rX!Z)BHG<(AK^u=(hf&1{{$gx|Za7wmFnWp&ojIoi96 zZDZ_3vkS_ZgeE$Knj4P73-mM9?94IU^1j>74Ycz=3PO3aSVIWCDlbQZ2vXXkICs7Xdfr68hPW9YY0^H zn2x-<0dKnvKh(H&-=h&Jd($2?JMFFm%whAI`R2P{3yZ{z+4@GQ12#vN7b@cPAvUvR zupcWmoL47yX*~AelBA+}kC`cEWxLj$-_z>#Cs$}VT>6sK;{7U1RfBb|S_=~Re)w7Z z_Af{E_{T4=*vc9jFqYYy#PV{YTHHPdqZQ`d2y9S2c~~HAyC2sQgYJ$?-l(jmYF#}h zb7;@`I&cpbn`aGP@`JEe0{XK8o;Q6^gkWBnpYzqjCK#%6KAWz?Q}ORMwXFk=yEUg} zGrc+f3({k(EPA1G@#9S@$qC)my=zD2CNp_MN9C50K2J=;(ef$tL(7kI$;WfP8viAa zEVE)+(Ix)isVm1`s}aIZyv9!ylti8oU(ZxJ(E`HC)rsuJGckxdf4_>^3)} zcdn3?=Z+@B@er+hM3Q-6oBczWp2Ebbozd;AA1FS69zSNXXnBjy^xPrH<@k&5IA5}2pMVP< zm2SXIr0j;}c`Ex*X(P%8jl(;ScvVFB>b@Dx*`2TAkzs0cHA?Vq%<9AI%nwV(w-iY1 zFX#^vD(yINUn;2mE}!19_}SaYDg0vRxT(=xablsxyi5#!owV{p>G3sBBeDn^!vrI$ zs!H7Wpnhe$iRco1z?6nYa3I?$yN-v3mvf(+lt$$}B^jvg(5CYAPr#cov|zcg? zO)x4SL7QmKR2oQb9x#Ts{VQf%%(0R7Q)tXPHObMK#C-*RA~mr1^#c`O#B$EnlccfE zhto2n0qj#G=ncX&hs#qc zYakYX5%<~ivhjlB`l@qnE{jioXir?0cKj6+JY-pa4dQoqA($V~dW@GHaH*>>kMp2l z>3(Q+B*ck4jw}*8nL*9*@la3jqwIimF>`2qo(s~6P5RjMaAgPYcZV!6lxC(~(&rIH z6k2TYEse($E3_c*3spzpj-B64Gr;aLc)6~UH$*qbYe%z{?h~E}0mDzm9iIfq#|Qtj zO>GM)s8y@aLmwdXGfCXO3*%<2YholXIg~#(8r&V%Gk3%liuRZta7aG$2ogvI5)8Rv zA}TS&5P`=+_(IIBh9iYJ@>KkDpKpAZFs_c%br>4v{)<*>$3R2o6*XR<$An9Z=siPt zfQ5md;$1&u!laz`eu&O(G{yXr7F-Ad9wPTV93=p68UNTzo zhXs0g*7A>)A_N42IYJGC)xM5;`pOD8`)pP0Ya5GA=-!ckq?&%ctWf=1D^IYZ*l)TQ%tE4(_V4_8|J zZUf;VyEUa{8`q|)lJD-+XyC8=IJ}xA(4KxgaQ?lv@&u-WwfznE@7Mq!Q2yk?5`$FR zQWfnHGKMS{$RC0&tW%yx07l|;Z^%mYtNP@Olpn~|cK)f6@`UHII0n6|wkUrf%8+Ih zb2&dja!yoW5&5O~&Wk?x9lYrGm`1s(I-G)j+z`YIb3mIEwwG*kjtKZWrn$6S zNn}U%K_3v6MLDL3O1qj*vf-Fmng@*?2L)!6$Scl1YrW3Hzvt8+8nS#mN;o~K^%b=o zmN_GqlO~;?NIU8r3EUDm>c)H^IMB-UZZZfWUHYXe+6MdSf$72IU~kH?Y#;_y;6@F?s`Hml+P?K7cao2yX!fGLr#IpSa*erQFKHt6?l8gQ^N4;ghoZi+ z;c-lbA&RIz&V0$z?g6R`f;_h;-iZb76u9qN72q-W2O@olUy zqSy!|xR1WGExA!Xgi9J^$5EYSjrLeu=9sV)8dm#-QOq3YLp;jYRslh70U@c=?)Yws zNGrsi?dfcwFV!z5ljJM;^GhblC*qzuE!KF)3;@8vp0F*6Mg^VQy)&UytMvE0b2w$A zjX214I6M4(jd)1AOgo+c7ot=A2CnWOi0Pf=PL}=mczc=;)3| zb{lB%#DfvgPwejClY4tU2Knw=j1)U>cOe<|0-&8oO4)fLd53qQ z4f7`-fksYnj=gUn(%$Dyguw>lsNh48Zwy7<#xm+BQFNNsMfX7tsN`>yBfLYxoS`L* z<&s+Kc1^#tO|S=*LNil5>>7`QVs+ zMXL(ze`+>;g?~ZP6r)%Dsad;&{-H0;_$7g^+kBHrh?os~p@I05yWmB1P(U#Jp+TB= zrFq}&y3)AOh_>Sh$?WwTsn+4l@T z@D3^?VB5SuXlZ9Uk<@X^Vhh_kETzvRcCY5z^#}7iX|wi^u(+Al|)KjlL);ON`L*wkd5?yT`TS&uy3C|YSWb8)18uXf#O+;V>r*y*G; zy13cxKLEL+Tqe}$__n<3uSf@&eF(n*w^-s3LfT_5MB}7KmKq#mb{?fEt+pAdr%KI# z(p!5T?W>*PU&Im>t(+vVdP3?yLWKd@mKcXEUe|F~`iHk929#Af1FMeNT$Sk9MlTZ2 z{wkDr-yH9g-Jn@oo1&*}bn||qU&PL7ki~#$wwSnuYl8Mel5MdDC22wgOx{9&h4YTv z+kf*0&|bhDjY<>ZKteB)VeETCTFK)CQ3WHYQXzW17AKnHYzo|?mvsjs9>*dj?Zh`^ z8oR-F#odx{YE7&@jBl!U9xk!Fa?W&A&~Jog)zbaAIe+&8(DRDRY}Xp|5zap&#%l9f z`|Ki@6sx9DE=wW;FN~^jhN+eR0uIck49DR}0Y1*UU$nl1l!`f9FbOZ}gpe+o4%NFs z1ek>2Yf)nPcXcVHg)v?ylDGaD%csuaU)*HQuuoS@a0@~Sa|s$K1S7N`97~_(>r*&Rz6>+v9-Q>b#2){ zEXTCM@U*?Ke&!X3mQc;lA=tS$ouLJ{qojVOGQApdxO}D_^T9!MS#HJ8i-WauAZ6Y7 z`2m$gwgW_(5pN6gZn;U-O}aQyrF*!L4#xE|=|>Zzl$lU(Q&Fh{DvKMnoxq`F#3S#B zEAGzJxuA_^qEkM#W}?Vd&zXHbrSaAnT?)nfqUa3GVUebI$u=AW2cdp+nrcFL zx&z1eJ|dh74tstYc`91oJEM$v-v;z4nWmd9XI~2Q4nZ@u$Ms)S@kZs+>U zm0d%ot^~$up5X<W^>Ptc$Zik1ti@r7`&-oC~#cuc~V+;ZNT%Wrk#&L4};98*Sp zn(Z~~7ih43@D0+MiMPmfAaQ?|Tbx2jtR{3LcURF>i`OW!(f_d3Hz5A=o^z~^K|wj- zVX`=fITyNzWZm{K#nvCUD3*l|LRqiz^RF9DxQcz_!-{iG<0^=eW(V0}x$bUGDgPEUJc=iKKFQ(r@h)4UI`t}e5A`VM87BZrU6cxak1Aw)q5tMLn3-1&8h8URf}+HPG;RCOJ2!!X&`ShfaEfvmuc6)Dh__b7geT}ux}oQ* zZioi*e!TMWfxV3KxL(4ETBG~L*>vH$N&E9SDK5p|d*jm-nmy1tuS7A&yyH=1rv!BZ zD|+(I3u>_Rael%WjZ8}9dI^2VYrAG??eW+jf0379^Si^lXK&+IbzOo1AS!Fqs&ZnS zZvUM;2HxM>(quRt6c645^%_AN<041YqL_rvnn%I5*2zaA*RZw4HzX$-p04iRzmZTF zHR78rABjwvp9;osUPG6xE_48rx}Cc|4~bBOdXHD{ug!|(uerP5pfos6E38i4p3I36 z?sr97#OKT##RvT2>_X|X-9q4rzv3mvo|5s}y93;s6YQ3C&6KI z#PD{NOW`lHw>T!k`zz9QpO4`#!C2^U6M7sS`n-gil#I;2FtfUP=XgaG;hn{e7feoE z%TMHWk^)aoih02^Km#SYV;RbmfMVy^hEE~&0?Wj=y%W+Q6@}Cd8{UY>i)}xdf-@)Z ziPX4lWet?AQF6t&&N%S)0Yg36Z6F`o*lIoA+EBBU0|;RCT7#uy$cQaGje%j)8Pvom zcC*3prOJZ=Di0>N2|;*Kt%pNa%O-|_{BT@W@jXJ7U(5O8>{r*7;6v`>7&hH=0%MFNr!L8Zd7ePmg2fisuh-siMTb1_ z@wQqkIRkV^-wl`UuA(g#zrs*VZ)SZHHo?p^Wxs-m85PADIXJXTXyzQDm%L}A`w{i+ zNFW-~%sZsUC`>)6Y3tv~t6`Ru$X{{D{mZC+I zT|LYS7=zQyapDE!V;L<1sqW#%etm@*A}SwGLM;iU1-Ezxe*42Q77wm5z(6(bOMy%@ zIDC53+m`$RD}eF1r5fX|n9s)IG>0EQmcc{udoj40Rx<}6Wv|dp>o3%5b4rk|E#9lVyLmskFKxt zkK!}HvjBj+v^_^!+n8+KW$GcS=w|FRNTpzJI9Uv+MqE>7MZ=XW>4+3nE6VMf+{>5$7SO{FLQ3?ObE7m}cW#+Vg+ z%MDDvi;J})BrDabq&3-%YHDGRs~5xf<#c5Y8&EJ>0$b9XYvM)whYKF8Y`SrhtRya1dj)7;?*1spKwA!sWgKus zq^yXgsXw@7rB6Cm?YpGBACYzIVP!#MwpMh_3zD-8xeKCal-MKhgH9UAYVzBBEHfqQIrn1-O7l(qJ+{7Q9Ran6vY_K^Wn;G~JFAezt-)Mwxo~cXJYeM{F4u#+` zo{MpB!|B6r{M;fx9QW{e;WNCKU7p-*R>n^uY_kQ%{m2G-jusNbDq;Le^H*C>ksMnjEVHGQR_p^G0ENrh{=E5-?WFW4`E0CL3D( zFS@=5PWP*NbW+7Vw(dq=uTPKLx!Hf-k)+i!8zUl0s-n|4eL~gOuD{?vFiCN+^`>0E zGKUm)za8HIs-&7rAm2}X#~|1)x(F9$=lWUe!lJ|{N zh%7O`yAR>!RUVavP{&95XMw48OIC*XU~`P+X=A4$`3Ko{cNUdZszQ#@|DcE$L~o{K z91sa&kZm)u)*&U7`HM5UVf$lnqjM~$vMI-SzBu4m4GG+XDFi_Oh2D-bfAo`?T`?;j zHPwW`ewx%Er^W`Om`QTS*8|k;Zfst&xad=*HU9+1o-ikJmGsDSk!S49aSsWmalmDB z9Ifj6UbWvqf)U{~$hzwOmWr@1{P6zCIjZnuLK@uaeIukpnAYm&BO zi_Omj>H@v%dw6dF3BlU8bWb?E*7jcr8BAOa+T)b&DA=;AW?rXl6FtdanKU6+pV;*f zsjF4FHVxi>{uETGwmAJ`%6eT_W?NDuAe%cJUiBs1z|giuJhz}WnZZr> zt8X}qGT2Mpk(k z?~U&$2QCf-ZRePbP47zIHO7zXPe_8BBm7%5%fjN4)J12njmGQ;M=kMf5d4<6@J;10bq`^YOQN@S(-jB$|L7r=Ulh5#Kty-M4JWE!Y&x zzRh^__BUa?2R}@&@TiYnCxSyAtYG~wx3Ghxh&ikn_-BDigR(7$pATO&UG~Ec{6quL zefTq7XV(%o=1wYtl{HXD^>VinOvnOZ@FDqCnWDBWjJuJpQ?2b>KUm09GBRJZrRorW za((Z}4gnaCm~~UQPON#^vtnw86Ob58>~^=X>2--ISyeF<+)}(jTz|KUyI-QQrx=Hs z6GP-}`*#Me;(YBUm7<05fpabsd29cGSoUdyz$O^z^~t%_eQ zZI&CX?`a;FJdEv#yw|}f81?%4_{}ix2B?+gi3Xe_FJDJ*Uwxl$TI-Y;E07E$>A%do zX|IBWS=on}wYdsK93JEEVL06!KknToHgKK{^!k)vCR3duti0dfgqs|;YUEAD@ zG>ymp6%RqbT6b{JG8CKvPFJPN`Ag_sFE%B6Xq~n|ivC1L^H$c+Ys6V{1AmLo-DSsG z3Q}0pkKf)5h}g>l^c!n9o9^?K&K2JY zrq+oDS#hH90_E%{c1uS+c=V7;Rg)k*1dFRlf)q3adZAGkngtb6-<$7Ty|e65%N-Kp zYs&~?jKnIVmdw$B-P78`t){7iPJAr$YJ68(b*q3j`rKTb^3H(VM$TylL~;vZ6NOl@ z>ZTOVE9x(ZVsjz&r7^s@org$GlLf*ygBFKp&qU_d)bL4(02LiX6%W=TEUxf0YfeV)0xsjz$(RuyCyl{37t z)zK~vaQ`9rt(I`SK$7ObWx%+h$rS_0w%G7qP;L6+e)aYN=ZIU*)9o-$z(E{hrEDPj zoKew#EKDcI^(U^2^^a|#$gP?YAj^(+VC#DQ6QzF9c(Lve(OF0G+k{8NlzUF@+O=97 z?Zce;vUIL#)?CP(L@Oz>i1hMNRSMu6?eIgtT|^3)oh0=frjhPcyrTHu}$R#2r*LSMrvl$m(WQOJOftnD}Ud z(_HPN#yp=!W8)xp_`-jL39#j$(zxlqL{D)LT z?!Y_s1hy~ncq_qNE0$0ki0dl!JtsKmricejEu+)y$W(A_+=9lnEZOv_Uy56td?heS z@A!?DK9xD?FZNDPyvOG1!*xXKiPONP3*qnn#fI641`}R;dp?I&2b_yQHr>vqT(49& z8nKnnu0y|+%Xc;Y`0(XFU0xXdfg8%GlsIY8D$k+s4PPtt5r&oH5HwsOSp784cZ&kI z`R+dz@2vLJLb*Lv5B(mDfuKOTm*axz-qF93IjE|Kj~0--QS|^f z_xHe%%-6!=(rflo)^IjcOk%{8xdTPO?v)IUw7N^3!q2;LFmN*)ezh=X*O-|W@mGDb z?d3haZ&odkTe~N`O zpn@8f5y%yd;+G9-L5`B4ppCKLrMDyMxArRX$8UU!`eM&zk6n;aMYuJjWj3%GGL>jd zwQ6HiL3omp`srr=m*u*jy?c~Sz81crxKbV7f0*lstVH=5RNAB|>7I~_+SGkCdl8H< zyv;)AR^n8XSNQq!6Kg?V_SBwbGZ|jdr^W}Poo!QjhHx?JJEI+xGNhh8WAwU!LBLz1 z?m6M+?>$Fs8e@d|U(r_YZ%IvYs;^4mG5vIR+MZjg(c{d*#mOn3nz=(M-Tq*>w<6W2;_7Q> z;2XW;UvW@LxAZ1&k_0|Z$u(L)2-5Kw0vTYNe&K%*PIf5DbBv}%kNF@hOw4Ne#fVo_ zDbc6}tx~>!5z+ZKXP|@wY{j`#9oToedFfM~z1Q+aMD(5~f_U$sWgG<@8RWs&X~H<-+bU6j6}7?7IDgnzK(?uEkXiK^B$Cp>nBAJ`wc6 z)OC-Kr<8cxad%5ar48J#Q&PcCw3k=fseSaExbXw zl17ui3IbmhGtTMN6#qv4lBe@s5u zOsnl;H!O->Uf$NC;?T9X2hW{?TjAc_EoG+L3f;=!*?inXH*i4(_Pb4Ny3Khm+3;WU znx<_>EMt`rtJeG9MaI}hY8OMQp3L-Ie@dc3Wy@Z2Sl=T9< z2WX&e!K7oE6eh(A3yg1@q$G3^y|(HCKgPxpKlAd&$O_YC6BvGB$}G@>X(g#<$owdS zyF?sZKJlsg*kxb?03uC%_@QgNNyX$I?rUwImKF_|JU0s;k(}+GA zw6jzbw_+S}n&_HBn|d1$Jel`-^|r_yUe;{-VWXZc2wQ*Kd)v=(FG;|2g`SP|NfJ&F}w* zy*CYq`u+cgOGp%LC_+-nz7s-HT5Q>68Dz`8j(rJ95h~e3W#88^%-F@0H3ow*hU_yK z+ZfxJ>!a`Y|GVx7KONVT`#$am*YP=yc``oZJm1Ufy`Jy0HwG;F?#A6Kh-1lm5!DJx zlzyL)I@dNbGF;%fU$49R(>AldA>%CDGd#7RhEG))ynQbp)Jj|JBLslIqnZ6Dv2w#n({MTvn=1+#=y7{ z&8&~AORdt&PYjb302lcr{)9{!U*ojf+i>r5_ui-Rn&l~{QLlWqTz)<2s&Yfe@UDL> zcqHHXdo+=(0R^tvQ`Rpsj4vvUUXf#Unf~mS3}`P*Zl72cvG?lSf%6!o>K;FC;zsvN z9cm2iudpOx_A+TtYJ*cLJiq*m?)v`|0^8f>wV5{w90cGpoU=JxWk2Rm=5*z`D8LwY zdB4%1#Nx;>&eFPXvDk^)%9tCU_{}=;Rd0w5F*h*2zq5hlS;qOgzEpQ_v>FiTqCNmu zI3pG(jjm_iL6IHZVz}mXE_miW=Rfg^r2}_`GwG}s7rK$(_p9*h<0pkJfD_gxR|JPN z%pj(9>lu4S^XCm1K(OExpkW3AsdSiMJWd6Svl_tiPy+j7%l0Cg>As#*PD*nLxz?17 zWV46+>6#h4$@?w6MMP>c{Qf;RBXa1z&div-nHv9Ru0h$d*e}hXPkQvAU8}MD$5t&r za(%-TInz5Y-GDperTzA&9yavnj0L?`ayBvwr1bb~WtEaF2j|Wqt_UrBek=3+n(vvk z3f10#Ws$Va$0rTn=k-kpY;E_E=JMM;P)U~)ugPrY^T}|4x>OerMs5q}Sk7(~nLO$O z1`aOoV!1X@T|fKmB&&f0)}}iiy7cM5yyv49LIj$YrYFp;H2!|-cVBz7A)u{OtYO0^ zJN)rmeRlq7^W)eD^fTDLQi?S-v}+>ODT(3I)C#%l8$AByo4N6u(q%PGfk7CMfUuAO zcSRFnS?~eFN_u#2hDD!JP_Hgy!7Z(f6mCJBq{oxh-JQrx>J|LiFgc?w(=ZN&*bfNd z1yv!{tIw}ryXQR~36lgCH_3KugNYyb*Ds`}WZQjDOb4@o%stPGJhLTfJvKA5i(Tl<+8bl8I+bckxg7c5>uX>!Zn3=B$4$_D;n)8^L9g5j+gJGHZYGlPu#Mm2k z-tnNQ499Q`_rIMR_uv)|3y!Ul^XWKOK*=nfe`p>63Xarf0b0s$@BiSb`pm11B6-P{ zn5EoLd#D2!9^}y{2E6tYT7k{4)N)=V$F`6q=bpt#EqL0IIGE&F$N1dPku4{Mt)(i5 z+S@)G5E_WFc8lmbM^@Q^L3v%~fwh$d#y}(SHVv`by>QN;Qkt^yU2=G|MXkuaXQN+r zAC|?Q5IKL)q5`YBv=Wua#vtazfcx?!`}1*KQqUvSlY9f7J8OA)Xyb#}KBM)sJ^CLf zVmw~!dgL<^bc!x7aID0`g+HI zpj}a^?+f|W;Mw=FX`hIQnXUU}<89EF=`DgPL0GCzE?a69AXk(24({9tz?}N2uj4e8 z)AbZMZh_@`nsK|xMacI?QOR;6{5GM)XY<_Y)AaQIlh2<9-9ur?;f~M!YBou?^9g#; z6JfMQ3-jVd!8e`V19yfD$NZebOJy^h&t4UKf1xxK+&p=%+w2U!z5ehd)8Qx-#(zhl zYz4J2q@TMa`!*m(wKoQe`Q0P`jmy3p9?{90!AfU!sU+;|veju6>x(ZiyIVycl#*Yt zwX&)%<6bfe$z8<_7Zu4w3L~!hI!mn3S+-CM6q47Md9(bXAApVq>nE%(4Dw3*X@4eG zw$x+Z^ZCd&C3h*a~dt@N(ZCXQLlp zDYxGfws%^8bBy#xmjKLo#whUumm`*rEAF>`V^}O3tyU4nn`!`^1aQUYb27$t?WOpg zUSZ#$kNfK+2RMgkqUB2Ei{qtqX^fW^pS>g78tI9vpRG2EAO<6b69iz^y$l1lS5nr1 zN$<4x zSv8NmTmj8x4kV{DX|A?bC|}6L?eM!f0cG!o{wU=zR9wfv`FRyO4;~wz>x<@ca{KRx zM7hu_I$rJCeN#E?FDHHC-P4b$>4&^b!0E;*PB{aqY+#5jaAYV}5A$__!-D1M!C~!y zDw^h%p3k+ZD~qs`u-_cR+J7o!!kzmr$3B>|&h@0*MTD!`Hgd|~*!G^wYF!Is9mh2{ zFy0eD`uPtO@S<%fN(wqK7lrAJ)u>0LS8vK54z@&4ug6XbHZDc&l7JpMwy$JX`0T=T zTtaNRd_hA1D{vafmDf4a!tCqwhz5C`@=vAncIJMOmdbG-8tH)cUI(F(=~QE9KC#rhy{l1tXTZtLsXF$Yk-huD^i3}Wd|uGu{R&tH zb9RL9U97;oF@55$n|{pu1^L)@_u=l88Y}5B`N_GjEjupaM5EdPRkG()ScEMj;+cm< zRrtHErEUW4IOgkA+^?DDTq@}T`bA%#>S$Ow;ce-|g?VMA@Vdplye>;G)!*n-%1p$x zFOxVqVd3A+%-^s!=GSIV54_8{z_9jwN0U1P@4MkstLo@Dl?-6W@Q2)a^2_8i+GsA| zY^~*(mzJM1tlX|E#L0UYIW$G-n*7=Up3stx4)#eB#-?)AEXvkz1}TrefyFlU##$X* zBEg$yD;NN4U+g2C!5CtCp^^6%CLxndt|L_1dW{qX!vZ zU+=S8GU`c_Fa%+EUgBlz%atxWFLS9=rr%h$EiZ=QP6>034amsQgJ7kx;vAmIv_WyA zTDNd)boq|2&Um?_IJcq|%}<|>M6hwZKiPemX-!y!h8|>zdnycz|KkMkpAxS1&A)Cb z-z}$>q?wMs;xqAi->lXf%Ua(|if2+>9}lva`A+Ph;ikXK>IMuDcs}s`bo4FBOKZwN zhtDNP66>3QpUg>*(*<7%=rvA0w?3%fx&57incv`(qf4!Dd3<|xwH-H03xjqU(10JP zXc?hU2n=fN2rEpuv@CFK^T7Ibpr}g$#-PYa`?BpTG1gfBb*2=UR~B>kP0Wxo<1JXy z<=^{^hHpePHcc-$v^c(X2amYK6{*m&C*Dbo;dxlm3Nl>UYNblWxn{%96Xcg3w$-`MHk3A&lk2`R%zWjV z8@qLTUXo{pSpc{S87~<|Ez;dr;T-V#>f4>AFvS(=uhf*qCzmJUCHfS)U_Vq&oI8L% zjJdR!5((E`R27=YI!UO*Rs}AN+Zm%me*Xk@fKYNnaKhLP*|4J~lqvtiaHh zgi;d*&~1>6&=|*+ zb>Ehp4%U#i*G8MIz=bw5r;rxlKHX|r-s-(u1dA#+{W3?VXWyWfmsxH-LeVHdU!h-n z%*G^{Z_q|tyrdJ<2X+87MF|jATWJ_(a_r2@p(kdWX>7l!q{G1C7|zrD{_{Q120!}A z-4j&h3~uMb`nTzBp~|%wZeP-Vdz@8xn?|I83o*_9^0wT9=Cf|Qi1|y$`$pPG3X`Wc z#%5#XInxca(vBbV?KoE*(0m{3KHo{ZNZVj?i;J_4qCDE@3@?GV`k$YLtekF#d|NB) za?j3_uW!3=^IbzSdm7WvlTmd4a!Sd3eT&qNe7xs0Nz11jws%(A0TjpPvlc$5{kZz` zW80H_>nRt)l2)#(aEjio&_@p&TyKUqS+6~}A|50f`aJlohteMynoXGw=K#2K5gym777&lxMv25gulw z$j+rU?d~&3ynJy((^&-=Ln0b(!w=JcTz#bla`T!mHxJCILk17@-a|Gdf6LQ zN>8?Y(>y8Tc_8;=J~PnP^mJU+XxLphD}qVA;A>)q-v^!Q$+5hC+_|4}C7fPGCF*9o z`Rq!R!K#d_3#|1Y`-g|aQ{5+`h2huZr9L)kNdWM40F$GA^A%KdVpjw>OMFURzMwP8yOhCIilM%dpOs@`0^@fJMhn4!pkYipC;n@4q!{e`l zB(>cGSAOQvL@80xyKnckzgPpVjZck5Pl0$(^QVtJzb7*?)`q)vcVe=-`hcN|GjDiD&H{PQ3L`ZG z%$wXugAuDBd@R`@Y@J&1z%XL1wjRXF%*x9Z}0^ zGR`%v%>cJ}Hz0l z2y^2S9Xr?OGrB%YeW61HR6X*EZ@Pg%^_yC1A9Lr#NSxa$w-B{wjnK2o>)%RW1qHV- zpf^8mPhfO;oR8m%E|GjRlzpj&k~x3`Mh>u@s~{ltp=yF2q!~WDL?R1%1yyR5(&s zpR1K_JHOtl_-evuQ;J~am6UV2qLQKnw1d{n6+J7;m6RaWrPi634=D3iu-DI(ei!kb z?I3WCeIf5{MQ^)g0y7haJ^IWd4<6(l-~(Qrh6VwL?tQ`6p+fy0H%B?LWx0JrBUrw3 zuy@-*Y&KX3=_oMz9{`N=XUh;^eHz>xeQFLks8_6lNb~S}%a;lM6~LO#v;IlModC_U>IVD=!QnM<9#8lS^$IROF16UC{>6E<1e6f2WRN$Po@x9#@@m;QpDQ=mBvl0 z9(5^M+)CWN5v%6?2(=Lx$m@~m9{9&mJ8X0^%NMX?b{HxZx->q0ud>T?FzM=WGZpO= zWf^nw^TpYzj_;NkI4&|6yu0;e@M=_Qe#Q0S`_ove=L8ytYb;L%TS~vP#8lka znvmZDJ=JcxI*=wgyGl_MsSl{~1Rf|H8h~(B*aP{U@G zzS-s7rXAuA`IWAwCj6lRMnO7cBEq-0q*-7)f+468j|vf}3MNc!dD^F>Y%W8GUj&XN z3AKhAHXaN!(#pV41$~RS%Xi~f3SOCrJRmQ#_%uoXbQIX$%fD&HhBGBT_*rx!a|qn@ zkaT;q9XncO3vaqi4iXG9P+CvUIxlteDMmYFoupin)Jlbo2Q`q9 zi)arGWY(~wu6%}}Xz`f;vVrYD@r?=)+!gj3-}@_AR$ht*|AxMPU&aff^3~NysFDz0 z1?wlhwrO)* z2JotWtMk6)8IyrnA*g7;Sn1d%ciH=RibXX!_C(JYvw^0^X?@fD-S7h(e&FC1BdUzO z^+3hk?X-xw@V>Kflj3&%{39zjdwR;(5->9Ov#m@07L30yp=RfCh=mey`+`fwFPRTs z@5lTZE9ASoxbJChxB!eoCzl_$}nViDi~qWMT+nkah6~ z20K9d$q8ocXU1%JtH`nP-l_SN{&mUtK}Xa4wtzpzFaii_wb5T zcIPF!x_SiB<^tp+66!X6Gom??QSq8k^^D)Gqoeie$R_Iwax38>6pj@eah13SGpTk`8iX;bPx8 z^3qR8M3Ux{CYizY(MYPP3qu-~b&4-ce!hs>y;8czh6^?=E2w>JzKcw{86I--Mi91R z$Ryqwva>6afMtb{cTpSX4`dSwne%e0A5obg&xaf#PK5$0%XtN`04ISb%yX16`ufeP zdxW*xCd%f9=g*HfR!DV6|EftdO$696}v&LL`CWGW^`L-wJ!56Rr}i zI+va94@C?DwE~2+a|2vDEMLKb_;vXd>l-49qrV}9GhVR z$&O7OgsqR}Wfpi$-|x#&Qc8Cm&rT!lrpzjtr@QxxBEQ*e5|S(w9LPKS5`7O|*E*~b z^MXhf#A><~o%cIq{^W%jJTky2^Xt%qco>PnI|7$^UBQP9s(nHp0TEB##Q1Ef-Hodg zIlhB>A52<|(*R2;tdDPq(}Ls@K_Oab(x{2+JFNyvjVVtTj~XCz5$1(x-KodiqZ3;VS;O#%42O+|EU}wIXUw)CxtaRA9?>pO^5s1>Qi6RF#t2PN zr!QX&qEZLRsW8jvv_a>}`8&OT>xAF|-iH}Z$G z=~sws!xtO1mkj3Cn#Ss<%8WG~HpDAYPY+>=%4o%`WG}_4~ zH|)F(%4mys$)yC7%zEZbHqW>|xuRz{Du!SN0I6(o<4S$EDG1(}VT#LNArj)w1)iq# zBmfX~VB}y{Ai@)2a>^dw`0i_8kani>$8oC-#1E2{GYv@2HAr!oRcQ$0vAvQ0`q_vb zh1@Mw!^__w@{LMl=tEo+gb2-3w8PB@ZQmJu@+71RuQp6y|E-s2CD{2RDp8SVj|HJE zR3A3I$>|K=BGgwphM0x;4%l&FEw0vUsF*D|z~$7}p?1F5)k;M}JV+;q4z>?D=eskA zi2k%-C7pNKPzG)q_iWb<*~wEbdS>a!}_)Eu?MF)_PTxcfC6lBrT%Pf4htyrhEV za)gnC1|-oreW=kdOT8UAOwm~@+S!cE>}K*zN-nKYyAO^V`QeR8@){819bHuu${v$4 z>2Vs*sk8swez37zYl1~jx}0JE$ajlDOHOI1OuT56#gW~|-qN@%qd38=w$v$6B0^sd z>%-gU6KWJ!qtRpRC@FPj+0RCPvF+!Zs$MWW0xs88QyQ4SG-3U7^I?fnVygvMt>$~I z@;4?%;OiH+2yD_C>;BalO#@`cpa>Mx>B3}Ae0vEU+T)tloSTjEXa<7hz z@UCdF%k{yW`vWYwjf^pwS;H1ZCBwg}p&>8ICW=$ApSHE9>wC)khAM5CUEcA7Q*_@I zTQ$KCDgKy`?ND7ye{~aDk$w4rtWbRq={(DjKvS9XvoH} zO8abGLG|dFus^LXnG4F)z&!3`nI`IDC)#$MN>*k!A6B{C^1+SE;$3b9aloGCl?$%e zg$!;KVmWp(S*~13NlA5SUq<&$^|~%CmyyCjk+2V`UY)ZFE2N>lFZIRaH!Y>2%Ak_LJZy-2pT1&n@An~d z6qLsGRwO*MmjoHwIujCduv;fhj4W80PYo7E1no2vE(8wtsRSA5B?X*Wo_`RbhVzz2 z^KlXCoZ>DUDsm}3{Eh3gNejCl^z4al?&<`pEHiw!AxGp4|FI>&>h}s=3B>FK3J&9y zxcWR5LGYb?p3J_7jku45?i7h}GGzVr>Xl3lo(F}6YEOj zIaK8D&_YGh7R9(i9Pt|!jiSYCmWI$ag9Qd@$j&n{BxK0C!T)-ylxHr(>R_r`W&pcP ztSjBq`02sCAiDZZ&EgZfheDp%TqFsTEIuJ3t9dT;QK)*Tq4gA7@qSBi)qS%&_G8X* zm}OrB^x&67YxsiQY?nx*H4IExX?%7GlH`(XD)~HnFEL;7O3alYZM}+bCCFqQ*fU_r zN=K<%Nlo7K@ztVbM?vEB+Lcec5_)6v2z*|3CRcB%7mY9B(~4(RVP=ZUN`t*g5Y7af z;jmXMNjqRB~2<$RhHe$bvQ0@Ki&wY=Ydarwh$$n82Mz zv=)cA>ez_l&dfkFf=y)xQJ{tc|v- ziv-t~_%0h6(;(-OU=7x#>9Yt#nY&mz*>vQHe`u0-__*XU}#4YP$O017FYkCs=!?y4#$p2f%5;rl(*~ zIrQF15YtF#6c+L|X?IkKSS&%$?}aFV=EbE3X9heE}4LgR+;M!Q`mf-~=N{LMKF`MaJ>^1ozsVaH_l2Na?zzI#4B zP2BZV2PqNf>t>!f7``NFF8g~{&rq3bCQmA259%ck2osGaE!ug&%2$3ShKPtm`Lc;EV99BSj|&|t#ixcAjWnmeDKcCq@kduiH;&Z(O}Q z$(+PEd;GS!{(pF~PE5__q{d~vq+Cw1e^1!;?Wr?G0)E8;EM(lPO3`*1{i?0rbwYnp4mVah&atO0|2m%B`+2lwYw}m6 zewVS*Nk*eoDu3rs0GQAHoLwyTCC{Jw=-?o@I?9`wD#9CRuQJ<9EF@aTD37=&oV&Uj zr4|WD{revKjs&8NC)mF6bMPrg@<5K72D`ksMo>m$rrM-_W~O=Rnd8)F?;iUv9~MIw zudc2aXuVaUjDEH?eMTihRdzn4z|)`Z>dRwP|MkOK=nrLVdY;vC{0VQUsiHN09y=zo z`}n@H9*7zg5s;uG@!#J){L%J*79O?n`99~rqVr&qd56mC#?kM9js9o||5sk=e}wJ9 zr2ah-M@^dlvZa5v8Cw4j{P}VBYTfwH*7obK75wF$fq!W_?CW@M=X_`9nI8)s{m!S@W1)toa(Z%o7li89H%$YTl9~myKIw%Y zLGi0E@BRdKbIvyP|xkpd(#<)E?)| z!qJPm`abJeC@b(7#=2hfe)!*ne$#{qEy1nLU* z{!;9b3j`#B`ReL5;Nf2hc%Q{{l-vPSh^3+i3^v6k8 zAO3ACSN^4qN8p7D`J=^;?xNw#=4jw4swF7L{%tF=|I%i5`{r|>BXsVNIJZjwCAK>@ z{L0_9;sNOAA#FO>G`_F=g?mp;|0VYC#*?ygf7!~+t$%2fFrD@W^B3-Ysr{GO?LkZ) zUVquj5AA(p6{s?Yu&mMQl`mATVAu`xMxl)ZP>XA9KUXZiX0-wlhH!3L}C zkvAdfm;WLpcaMeQ09`AT@wgFqJ0aT_ury`Y!#*h=4Kv57KLResjp`_didOya2{%^h z9V_1b1w`m(Zr+_t_$4Czo!;;q;L8gXw~PNp4X(aV13>s*Q|(y+(+7vEDQD^Wi+a7h z>jJ35De~`o#cs@p{${JO;9q3@7**XVVDX#L|9@S)K%&y#Zvlz5w4b(P?^0|L%OQ@# zxPBqnRmXF;#6m}_=2PVfe!r?+Q60riAQ7ZbRgBuwX<3dVIQ-?{kX3=P)Bi)A0Z5`S zdDJv;6fy^YOi^>b6u5BQ7JXono0w%=jb%2Z@Hpsx12N^;v#Z2GVLe! zRxfM}p|B~gO=(VZJth`%UN2mSmxqwWRupGQyKU~{_>Lk$r z)LE)G7WM=vY|M$cmHljwPO62u*6%(?X0H{Z*M$hpdT)+U)bEE**(9}0dxTMoM`|1p zHRu=UYinCF%HzDC_l^%@57zTKV`RI{f`@Kz^z)k6vUQkxB`%h$!ml_8V9XdU`v%#V z#F_anN)<#aS!61uDlOFqIXsh~_jPF`b8bu_GT}sxG-)xEsiL^Jg*sZSD*|@jcd;_V z??B&k9>Y{Ca0Ps@NaILMyusamgz*dY7Xiq=$`|`Tx?4DyD(e_4Ycmqz(yn}R$=Q3% z5`DNxz|VUL^+2V$kfDCdF+Sr1$M?p{9rKoX1cn^ROB*5*IPa&(a^2Y80BAUvZ*_O3 z>XdCa*42Y0Fa7p+r~0W~mki4PDesGP)T}tuW%tiR?@e>qk7os{OP#8>?f&eX5ilhs zvAD-)yY;!w#k5$HLq0ha?d&`ZDhhz)L41Fn-x!ZBLzj{oWstc&l3mVc2WvD0kR+M3 zFtuGU%_gu{dv@ddfU_8pi!uDj?P#RTHx~QBpN(^NaAV zW0GErNB97~vjrz$a1}`zcE-?B;|{{cFq|tDk?G$ev_i_mbiZyu5z_`ZrRGMu${K8P z#~h7bAS`rA2?}r@sS_uioKs~S6AcWkUSk5$m|iiQoW5^ngSXT&8sffJU^DdHl{dw< zNCsk6^#FQN50&CGPBfR{9Gwf27AMz?2!hp{LeSDBF{R3w=Z%U799f>jm(13UebO^* zdF8iEs%(q$wix=>?tZsolxV6zSl&CzJl(24G#=CcF&90V^5eXxOu-{~FdQ=&Mz)b0pxl%5$B*RcD1r~1hw z@K0u$Us)y{XEI>37CT4>ZZhfvOKPE-*Wb=1{r!!E` zcsgngYzy&Rs8TxZHu<01o1Ut5Lk=4G1`)yQdji>a=6*D#^e36SN@|)8US#b(@B83w z1>+Hr`dBjK7aVNKZaE z(U>|&W-&yyBuY}setg8#zh4wtXu|+4rm&0}Xd*K1+!4BkAIphnfDVpz5#SRr$FwRf z`Htm0V(`2Cx84(44F)y$95Jv2*@Jw_Mo|ob@mJZ?FIiUV2L~r5P@t@v>1gJp&v@g9 zJ02v3)mp#JxAWt_Q-`g#%cqN~Ti0!h0tRoJJB-fqM(A)1zsSVV_Lp&a07f9~z@)a_ zri;J3-OAt{Hw!qOo-2-hH>4oZPO-dGYE{R=$N-c`4^Kr8o5rw}%ozvnYz$6|sW56f za?e4}`TCvWJPK9DfFS3|0tf5!h@v-taF3Xjc+#pcp#QZ8EXb+UxklMsn7d*$-q_m>A%{6}i^-1a7YZ zw{rgq-;zR8TpVQx6nVfjxYUsR8@kJN`Z%GEm z)KL~Zn_o&z_U4%GRC}Gq(u0J{Y<@MXB^s|@S!ed$kqhu5JdeGzUzV$uCxzixK#|BK zcZq{#5~dPJc!=?I<7yb%Coi6^AB1GrCm#)(hsgcw_Wu~nA|1S?Jj+lBXJBi|8=OH| zrM0)`;}ara*LnE)aUX(N&q~NhMQ7+>NhXZ6fz&XJ6OUn+@rZT^CF1ae!U z`QSaiJAAk04hGM;5WS`MZ(kE_G+4cZh^?5mj_4TBAzPI-3>CYY_}afKb^E0U6qZDP zLTTCmb$DKD^^N-@f^xqC^-?TL(71aTO=GfXzp3|-M!~RVaGJ3Kanwr%S?0=B^-~p} z1~}hMe9PKGBe8O8SD>25`xidgjfg?VaufRRl}nA5!cR-?QT#~N;t&qsMN<(juqJ=) zCa*4FL0_l)*yE%&P-K$0JfSunyTlpJot07bRa(;O{|v=|5eTa~MHz95JFPKHId@g# zjaN*;J8#&0QdqZ`Xdhb%%XN8YqJ&Mqt7nRJefHjJcL_pBq1-IhbH!jCm|fQm96O=5 zl3w(p_4V|NAFp^1E|&Tx^>XyI^JuCq`}f2^)1X-L4K)urq*LsCri)F9{V(iddv9yXAM zG-kCJSnF&c<9PVaP1~$9!)kbOac5qZBjw411<=_?m;TXNc;QJO|&uh7X8+(M+K{-J%#`Q@Hmc4Fy(enw4X9*#-` z0@4+bRw|$Y`0wOeGhQEOkFJ53r!Q!uh+6?%u$L=V>_$Mdenw5EagdpHTfKz_S`!c)v)lW6$P&lANxtkJF6m84A2QqCQb&>DtK1f< z3r#837j1hpBt#%`$V+YoRNY7B0cWlQ%J%->c*|R#seZzbU?n4+AR~Y1m?@utO5s$6 z&2>LEt6=iI8=_-~dpSBF@3|2Nnf&ZhLu2ET)LR8Ig` z;75nicms>sd$(5y@H5}UP;4)@Dd)A{DMGmXj12$?H|aw(1#fV zPGi|G9VE~~Q^!mw{`lgAni3dIIuzy~xR@Z*lO}z7GrqM@P53ra(xLiW7eXwgiBBpx zMI?S^t%hC1(<*SM5}*qv5R+4rhFEZ618e#VEl7Rk3oJFx6J4!i1xZj~^U6XnzB{U^ z=6yUzg_U=MfvUuLxsC{?;mHHif^nNQRoKZYv0?!mIMohlGsxw7W+8>Mn#oyF=yv+W2^n1-M`7OYp-_&3` zX~>(Jxlg{BW#M=bmT9{4W%h@*l`bkHzcL}B!%TvoIn`d@`6LL6_3~R>t2Al!5w$j&;~bjmx^_FPixbCwM*nx6J$|us;8_ z*|oW0d4eZGTR;MQBiVj;pr%{&G2()@l+>is6`_15)u27<#Q?Hx=wp z6lG&AwvWH*f)z z+ZZCF>Z~Hes#kRk!+9Q>&oXkYq^=jAv+DlD73_sBy%OZ&loY3;bsIfeAm3}^aw*n^e+4WEqZ>ENu zP`uab0a;o`C2res_HD#mM$nYLC!&eV&5<+~WLHJvgxBuK6M}!;+1M9w0pY8K?(kK! z1#Vszt}hf`rC))+=zs2+w|t%ek(qEW8q!$`o64h(zK$CZb&hNf>&537cBG*x#LU4l zlhUR^pvnMW+2stt(LjTjGcetZCxX*}c@0^@91l%5eW3Qpn1D3hSgxfx0*u7HF<2!6 z)McB63rq9CkBxnI#K+9JKegtsUR>%B7ha8WjA3T1?ueBYSr(-={2DD(J*Hiz;4J?vyDrHF>67Bwqhv}?eEltGmWE%NbSHZn!#CAogX;O7k5e^OcZ);HFH8_EF>MI&;#2te|D?*$$49h z_dIznA_?mT$?)uOFU@G?9$$*6Nyy+55fWY!u$;i$E^p7c4(A4m(#CHvOI))>2SNZ@Z6~4b;_67)N0}k_OKsft9030<2G>raY zcR5J%o9-cvufe?+t%a+F^4TO`OeT^;WGEY;jrAf|Si6*bwM-J|cDJi|%NM(mf`>p# zgZRW5*c)O+S9&ZR8DxoPEZVaKYNjAEwgPO4RFJ;}Z3gb1C zA@uJrUbi1BC&A``oD8i@n#D^$AWZ5WtHJ+R%@(VY^iI49=9T@ubV1kb3GU9|$DWhV z`7f~g+V8cTJotHFRqm&fTpKqPwd^>TMOb7bJXI9i}8ZV-bTj z&iFwZY77p%?XhbcK0=G4-^|jkak2jV~J49ewb}X z_vjosPEpFWfvn8`i0wJz47wwhi{*2el{I*Ayx$2!kP)8IPR?3vVp6c@^k2FxQ?|ig zO|G#;YWTA^&?;>&c(QLb;5%4!Lk~S)(%M%CNe78&+Z5hD}?xa zA)Op_p60Narh0M86Ux)mDAT3krcd)C?2&e<$iB<7KS~zOg-Phy@Z*^R*Vg z;-U=zZ(43pAv!2q>ziwg7^q6=nQ*e9`oK(bXoTe!}0eunhe2$ z!jN&D=<8sP%QC>kg%+T_6X;0Z!^CwHr&k_D+;Ioz2Q;ZLO+cUcyc)Cn=FBHOP;;rs zg+4ZpMX)H*BQH~-6LO!SlnGj{<5-nuShG%_h^)_O%Zl9ZF%4>ScG1mkR%@}q_DG9O zV}r2?(YsngGyq>LhR!+P9C~u=O?U(aicv~yqZC!4|Eb@ zydyFk!Oq*8RU(F`UJpXB@7ow9=VR-~X&k1S)rbW)0~nIhdB3IM664f{gW4*`9cHjo ziLOk5!`oTJB_wR}wiSKDN(gYaY(Kh8UseechI2KE;P7gm(1)q$DWne4Yn2o=rfoLy zhsNK_cEQM{9Y?#&8beD)cy>b|zI&t~raa?n8npt3RvxB3HEY9&#?SZ4ib~;9B@agb zH9vRFzbh0z~-&t5FN{ZwUPHUU!^GT$pJ89J2MeKrB1Ty+1lhP|N|G%km63ZzW0p#2DiAVL@;6 zv%r;IOPm)HxLr3l0M%te$8ArEq4pc;cXRuaZN?z>=?ijwY+@Z?=K>ZG zrPjN>)0Borsx)LI z(d3%U(Q=a9vxow7n)@Y519!fnb=0Z*hHAp|kNY^OTl&b74R$ZLOS?z%a zj@C1?!AFX`yT!M3O83Yz^xae-=g9VOE&|9p)*JpQSsBcFL@cwptc?4)oaQH1{CsuS z`4q5NdsDF($6S}wN7Fy*k9|`3->U>_W$moB#NNH`?4P>o(Q`fBd-bI z4l7(U|F=B%UqIpitJ>nX!XJ7W%>qxN!AEil_~LF4jRTvn)Qexu9D_>3o2S`EEEZ6y zE2e3CHUX(AosJFIIWFd~MeN5LYU*jy&JG)uw^@z~aSu29be`s-uay13cc5Hc7XrJ= z^?b3vFH&T%j1)Ju{5T1(KAFR?eq8@!beEn1o-9pyG?{S!llWg(SPh7Fwc3m^*V6|s zr%Ss3=RM|Dt8+&rp#QX;{5aG$20^Z;H%4#hJ}V)i_jy7M&ue!c6^H#8o78!ITb^~98G6OVxR)0PaUz`oUpFqODm`c$5 z;PIDLzPx)_F{#%|C{ZjgtmJp~HUO?8ei0EL_Hb^4Tt9O)H@JCH4^L>)5YU-4cSGgb->7ia z4A4>xPrpuig26WqC+?=Wkip>KV4#Uac=KC?p^ir9U-_RvrMRkt7X~}#^Nl!k_3a@# z6%4y=jJbr9?EMm60J|YgC5D_X)t$@<0UG0MPM4{dT^#;vZ^mV60KZ(uZ%e&SilR{w zR3dP-4O##B!8%P^5G>ikEn)F@D;}cdrleF>uC}3;D9F7^pY`|l6Fr_U%|$>buJBCf zV5@-eU=-S*=PSmRN~CM@ga~BSVYJfbyv*N`iLUr!vQkJRz@f>gW`XFW2-oz4cHz?e z){@C=|II!h`$v<^gO;3sL&SVg^GOgr{Gxw^iPO$mL0yfh^^{HSv2P0 z*}^AOBJ6rL@;e4*!Ae{&g-mL?C7s6x%jW)SA?{ucZT{UJtafcc#^}t1e zlQ%x4Rsgz+Ka>v*Jazp{qIkjvK0I!A zcD7VMDXcX0l^IOh++3rqD)fv~rRz0)WES&_rc~1~EbsOuDXu#<&Lo>KyrW!ZAI!m@ zap@>`^w4e*b+4k%^5uVneZv8PV4ou;KPZg2yYs)c%56OEu_LKyga`ykR`xp52 z$2(rr_sbuYt>xDLU9|OQ1lRL`+1F2R#9eSlh_V*(HhoGIvuppbsZTKP=F-x@r(5i@ zPA$uM^3Tj-`|sJO73Z4XRFA0N z%LQ@`@4P>C{|q~(ysfc&b>iEMDR|5HETswAH}2d+oYUQe2)A1}AY?EUAT zR_FK3mk?Egm(&<74TrhnCrn*d@hw=hX3Z|8&TY>h+q{nD*4LkY-T=5S|L5n&c$4dj zZl<3cEB=%|U%d`ghilz;4Bhwov%gwD-Y8hf{X~1tr|UD%|9VyWKy`ytOGxXA%l12M z@Hmr!X{pMD^9iTo!}q;j^SyEU11`w`(M@^(mwd*xbO-FyE6W`AF|T~EQ+(08P^Aw- zNx3Y+TMK(0a5q)rtpMVgS~(USdLFhSd5uxybcb$157Ean9*XnhUvU?(AmCu|2Z@J2 zo#cwAa_wP>&SDH%IoJQk(c}0Q_Fb`UbZYZ9y{Pf!=bEC%>GN2emd)BDa+4ijsd*x9 z!G*->=Q5`9?AcVWb%Pn7T89N8z`-T2uQMO+!JX5*#dCm#BaVM9bkp=&JKP&s*^| fRTL(8JowKj6dhXR9Wq;x0SG)@{an^LB{Ts5W;>S= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a6b2f533b3858357d8c80d59f5a4d6d1056e1d84 GIT binary patch literal 133531 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 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. From 32212644da19833cd033d018af7ade17f1a1a029 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 26 Mar 2021 08:11:35 +0100 Subject: [PATCH 85/88] [Application Usage] use incrementCounter on daily reports instead of creating transactional documents (#94923) * initial implementation * add test for upsertAttributes * update generated doc * remove comment * removal transactional documents from the collector aggregation logic * rename generic type * add missing doc file * split rollups into distinct files * for api integ test * extract types to their own file * only roll transactional documents during startup * proper fix is better fix * perform daily rolling until success * unskip flaky test * fix unit tests --- ...ver.savedobjectsincrementcounteroptions.md | 3 +- ...ncrementcounteroptions.upsertattributes.md | 13 + ...savedobjectsrepository.incrementcounter.md | 18 +- .../service/lib/repository.test.js | 28 +++ .../saved_objects/service/lib/repository.ts | 58 ++++- src/core/server/server.api.md | 5 +- .../collectors/application_usage/constants.ts | 10 +- .../collectors/application_usage/index.ts | 1 + .../daily.test.ts} | 205 +--------------- .../{rollups.ts => rollups/daily.ts} | 126 ++-------- .../application_usage/rollups/index.ts | 11 + .../application_usage/rollups/total.test.ts | 194 +++++++++++++++ .../application_usage/rollups/total.ts | 106 ++++++++ .../application_usage/rollups/utils.ts | 11 + .../application_usage/saved_objects_types.ts | 6 +- .../collectors/application_usage/schema.ts | 2 +- ...emetry_application_usage_collector.test.ts | 228 ++++++------------ .../telemetry_application_usage_collector.ts | 72 ++---- .../collectors/application_usage/types.ts | 40 +++ .../common/application_usage.ts | 23 ++ .../usage_collection/server/report/schema.ts | 20 +- .../report/store_application_usage.test.ts | 115 +++++++++ .../server/report/store_application_usage.ts | 87 +++++++ .../server/report/store_report.test.mocks.ts | 12 + .../server/report/store_report.test.ts | 53 ++-- .../server/report/store_report.ts | 22 +- .../apis/telemetry/telemetry_local.ts | 11 +- 27 files changed, 880 insertions(+), 600 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md rename src/plugins/kibana_usage_collection/server/collectors/application_usage/{rollups.test.ts => rollups/daily.test.ts} (51%) rename src/plugins/kibana_usage_collection/server/collectors/application_usage/{rollups.ts => rollups/daily.ts} (55%) create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts create mode 100644 src/plugins/usage_collection/common/application_usage.ts create mode 100644 src/plugins/usage_collection/server/report/store_application_usage.test.ts create mode 100644 src/plugins/usage_collection/server/report/store_application_usage.ts create mode 100644 src/plugins/usage_collection/server/report/store_report.test.mocks.ts 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.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/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 37572c83e4c88..ce48e8dc9a317 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -23,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() })); @@ -3654,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( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index aa1e62c1652ca..6e2a1d6ec0511 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -76,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'; @@ -98,7 +104,8 @@ export interface SavedObjectsRepositoryOptions { /** * @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. @@ -111,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; } /** @@ -1694,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 @@ -1706,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'); @@ -1728,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, @@ -1757,11 +1786,14 @@ 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, }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 73f8a44075162..cf1647ef5cec3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2735,11 +2735,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 @@ -2839,7 +2840,7 @@ export class SavedObjectsRepository { // (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/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/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts new file mode 100644 index 0000000000000..8be00e6287883 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.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 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/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/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': { From 02a8f11ec88cf6940da0f886d6ca9a5816314de5 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 26 Mar 2021 11:50:21 +0300 Subject: [PATCH 86/88] [Timelion] Allow import/export of timelion-sheet saved object (#95048) * [Timelion] Allow import/export of timelion-sheet saved object Closes: #9107 * visualize.show -> timelion.show Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/timelion/server/plugin.ts | 1 + .../server/saved_objects/timelion_sheet.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 66348c572117d..226a978fe5d88 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -47,6 +47,7 @@ export class TimelionPlugin implements Plugin { core.capabilities.registerProvider(() => ({ timelion: { save: true, + show: true, }, })); core.savedObjects.registerType(timelionSheetSavedObjectType); 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' }, From 2af094a63d93da906c5a60ee40c4a8372099f574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 26 Mar 2021 11:32:46 +0100 Subject: [PATCH 87/88] [Security Solution] Put Artifacts by Policy feature behind a feature flag (#95284) * Added sync_master file for tracking/triggering PRs for merging master into feature branch * removed unnecessary (temporary) markdown file * Trusted apps by policy api (#88025) * Initial version of API for trusted apps per policy. * Fixed compilation errors because of missing new property. * Mapping from tags to policies and back. (No testing) * Fixed compilation error after pulling in main. * Fixed failing tests. * Separated out the prefix in tag for policy reference into constant. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [SECURITY_SOLUTION][ENDPOINT] Ability to create a Trusted App as either Global or Policy Specific (#88707) * Create form supports selecting policies or making Trusted app global * New component `EffectedPolicySelect` - for selecting policies * Enhanced `waitForAction()` test utility to provide a `validate()` option * [SECURITY SOLUTION][ENDPOINT] UI for editing Trusted Application items (#89479) * Add Edit button to TA card UI * Support additional url params (`show`, `id`) * Refactor TrustedAppForm to support Editing of an existing entry * [SECURITY SOLUTION][ENDPOINT] API (`PUT`) for Trusted Apps Edit flow (#90333) * New API route for Update (`PUT`) * Connect UI to Update (PUT) API * Add `version` to TrustedApp type and return it on the API responses * Refactor - moved some public/server shared modules to top-level `common/*` * [SECURITY SOLUTION][ENDPOINT] Trusted Apps API to retrieve a single Trusted App item (#90842) * Get One Trusted App API - route, service, handler * Adjust UI to call GET api to retrieve trusted app for edit * Deleted ununsed trusted app types file * Add UI handling of non-existing TA for edit or when id is missing in url * [Security Solution][Endpoint] Multiple misc. updates/fixes for Edit Trusted Apps (#91656) * correct trusted app schema to ensure `version` is not exposed on TS type for POST * Added updated_by, updated_on properties to TrustedApp * Refactored TA List view to fix bug where card was not updated on a successful edit * Test cases for card interaction from the TA List view * Change title of policy selection to `Assignment` * Selectable Policy CSS adjustments based on UX feedback * Fix failing server tests * [Security Solution][Endpoint] Trusted Apps list API KQL filtering support (#92611) * Fix bad merge from master * Fix trusted apps generator * Add `kuery` to the GET (list) Trusted Apps api * Refactor schema with Put method after merging changes with master * WIP: allow effectScope only when feature flag is enabled * Fixes errors with non declared logger * Uses experimental features module to allow or not effectScope on create/update trusted app schema * Set default value for effectScope when feature flag is disabled * Adds experimentals into redux store. Also creates hook to retrieve a feature flag value from state * Hides effectPolicy when feature flag is not enabled * Fixes unit test mocking hook and adds new test case * Changes file extension for custom hook * Adds new unit test for custom hook * Hides horizontal bar with feature flag * Compress text area depending on feature flag * Fixes failing test because feature flag * Fixes wrong import and unit test * Thwrows error if invalid feature flag check * Adds snapshoot checks with feature flag enabled/disabled * Test snapshots * Changes type name * Add experimentalFeatures in app context * Fixes type checks due AppContext changes * Fixes test due changes on custom hook Co-authored-by: Paul Tavares Co-authored-by: Bohdan Tsymbala Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> --- x-pack/plugins/lists/server/index.ts | 5 +- .../common/endpoint/constants.ts | 2 + .../endpoint/schema/trusted_apps.test.ts | 72 +- .../common/endpoint/schema/trusted_apps.ts | 43 +- .../trusted_apps/to_update_trusted_app.ts | 30 + .../trusted_apps/validations.ts} | 2 +- .../common/endpoint/types/index.ts | 5 + .../common/endpoint/types/trusted_apps.ts | 38 + .../common/experimental_features.ts | 1 + .../components/item_details_card/index.tsx | 85 +- .../hooks/use_experimental_features.test.ts | 47 + .../common/hooks/use_experimental_features.ts | 28 + .../public/common/store/app/model.ts | 2 + .../public/common/store/reducer.test.ts | 3 + .../public/common/store/reducer.ts | 5 +- .../public/common/store/test_utils.ts | 15 +- .../security_solution/public/common/types.ts | 4 + .../public/management/common/routing.ts | 20 +- .../pages/trusted_apps/service/index.ts | 36 + .../state/trusted_apps_list_page_state.ts | 9 +- .../pages/trusted_apps/state/type_guards.ts | 16 + .../pages/trusted_apps/store/action.ts | 11 + .../pages/trusted_apps/store/builders.ts | 3 + .../trusted_apps/store/middleware.test.ts | 36 +- .../pages/trusted_apps/store/middleware.ts | 173 +- .../pages/trusted_apps/store/reducer.test.ts | 8 +- .../pages/trusted_apps/store/reducer.ts | 32 +- .../pages/trusted_apps/store/selectors.ts | 56 +- .../pages/trusted_apps/test_utils/index.ts | 4 + .../trusted_apps_page.test.tsx.snap | 5573 +++++++++++++++++ .../components/create_trusted_app_flyout.tsx | 99 +- .../create_trusted_app_form.test.tsx | 318 +- .../components/create_trusted_app_form.tsx | 332 +- .../effected_policy_select.test.tsx | 167 + .../effected_policy_select.tsx | 197 + .../effected_policy_select/index.ts} | 6 +- .../effected_policy_select/test_utils.ts | 44 + .../__snapshots__/index.test.tsx.snap | 18 + .../trusted_app_card/index.stories.tsx | 24 +- .../trusted_app_card/index.test.tsx | 12 +- .../components/trusted_app_card/index.tsx | 142 +- .../__snapshots__/index.test.tsx.snap | 638 +- .../components/trusted_apps_grid/index.tsx | 27 +- .../__snapshots__/index.test.tsx.snap | 102 + .../components/trusted_apps_list/index.tsx | 324 +- .../pages/trusted_apps/view/translations.ts | 18 +- .../view/trusted_apps_notifications.tsx | 29 +- .../view/trusted_apps_page.test.tsx | 403 +- .../trusted_apps/view/trusted_apps_page.tsx | 12 +- .../security_solution/public/plugin.tsx | 8 +- .../scripts/endpoint/trusted_apps/index.ts | 7 +- .../server/endpoint/mocks.ts | 2 + .../artifacts/download_artifact.test.ts | 2 + .../endpoint/routes/metadata/metadata.test.ts | 3 + .../routes/metadata/metadata_v1.test.ts | 2 + .../routes/metadata/query_builders.test.ts | 9 + .../routes/metadata/query_builders_v1.test.ts | 9 + .../endpoint/routes/policy/handlers.test.ts | 3 + .../endpoint/routes/trusted_apps/errors.ts | 20 + .../routes/trusted_apps/handlers.test.ts | 307 +- .../endpoint/routes/trusted_apps/handlers.ts | 145 +- .../endpoint/routes/trusted_apps/index.ts | 41 +- .../routes/trusted_apps/mapping.test.ts | 83 +- .../endpoint/routes/trusted_apps/mapping.ts | 80 +- .../routes/trusted_apps/service.test.ts | 140 +- .../endpoint/routes/trusted_apps/service.ts | 84 +- .../routes/trusted_apps/test_utils.ts | 33 + .../server/endpoint/types.ts | 2 + .../plugins/security_solution/server/index.ts | 3 + .../lib/hosts/elasticsearch_adapter.test.ts | 2 + .../security_solution/server/plugin.ts | 3 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 73 files changed, 9574 insertions(+), 694 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts rename x-pack/plugins/security_solution/common/endpoint/{validation/trusted_apps.ts => service/trusted_apps/validations.ts} (93%) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx rename x-pack/plugins/security_solution/public/management/pages/trusted_apps/{types.ts => view/components/effected_policy_select/index.ts} (70%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/test_utils.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/test_utils.ts diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 1ebdf9f04bf9d..250b5e79ed109 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -12,7 +12,10 @@ import { ListPlugin } from './plugin'; // exporting these since its required at top level in siem plugin export { ListClient } from './services/lists/list_client'; -export { CreateExceptionListItemOptions } from './services/exception_lists/exception_list_client_types'; +export { + CreateExceptionListItemOptions, + UpdateExceptionListItemOptions, +} from './services/exception_lists/exception_list_client_types'; export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export type { ListPluginSetup, ListsApiRequestHandlerContext } from './types'; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 90e025de1dcc8..d9f67e31196ca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -15,8 +15,10 @@ export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency'; export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; +export const TRUSTED_APPS_GET_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; +export const TRUSTED_APPS_UPDATE_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_SUMMARY_API = '/api/endpoint/trusted_apps/summary'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index e9ae439d0ac8c..326795ae55662 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -5,8 +5,18 @@ * 2.0. */ -import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps'; -import { ConditionEntryField, OperatingSystem } from '../types'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, + PutTrustedAppUpdateRequestSchema, +} from './trusted_apps'; +import { + ConditionEntry, + ConditionEntryField, + NewTrustedApp, + OperatingSystem, + PutTrustedAppsRequestParams, +} from '../types'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -72,17 +82,18 @@ describe('When invoking Trusted Apps Schema', () => { }); describe('for POST Create', () => { - const createConditionEntry = (data?: T) => ({ + const createConditionEntry = (data?: T): ConditionEntry => ({ field: ConditionEntryField.PATH, type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T) => ({ + const createNewTrustedApp = (data?: T): NewTrustedApp => ({ name: 'Some Anti-Virus App', description: 'this one is ok', - os: 'windows', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, entries: [createConditionEntry()], ...(data || {}), }); @@ -329,4 +340,55 @@ describe('When invoking Trusted Apps Schema', () => { }); }); }); + + describe('for PUT Update', () => { + const createConditionEntry = (data?: T): ConditionEntry => ({ + field: ConditionEntryField.PATH, + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + ...(data || {}), + }); + const createNewTrustedApp = (data?: T): NewTrustedApp => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + }); + + const updateParams = (data?: T): PutTrustedAppsRequestParams => ({ + id: 'validId', + ...(data || {}), + }); + + const body = PutTrustedAppUpdateRequestSchema.body; + const params = PutTrustedAppUpdateRequestSchema.params; + + it('should not error on a valid message', () => { + const bodyMsg = createNewTrustedApp(); + const paramsMsg = updateParams(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + expect(params.validate(paramsMsg)).toStrictEqual(paramsMsg); + }); + + it('should validate `id` params is required', () => { + expect(() => params.validate(updateParams({ id: undefined }))).toThrow(); + }); + + it('should validate `id` params to be string', () => { + expect(() => params.validate(updateParams({ id: 1 }))).toThrow(); + }); + + it('should validate `version`', () => { + const bodyMsg = createNewTrustedApp({ version: 'v1' }); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `version` must be string', () => { + const bodyMsg = createNewTrustedApp({ version: 1 }); + expect(() => body.validate(bodyMsg)).toThrow(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 6d40dc75fd1c1..e582744e1a141 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -6,8 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { ConditionEntryField, OperatingSystem } from '../types'; -import { getDuplicateFields, isValidHash } from '../validation/trusted_apps'; +import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types'; +import { getDuplicateFields, isValidHash } from '../service/trusted_apps/validations'; export const DeleteTrustedAppsRequestSchema = { params: schema.object({ @@ -15,10 +15,17 @@ export const DeleteTrustedAppsRequestSchema = { }), }; +export const GetOneTrustedAppRequestSchema = { + params: schema.object({ + id: schema.string(), + }), +}; + export const GetTrustedAppsRequestSchema = { query: schema.object({ page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), + kuery: schema.maybe(schema.string()), }), }; @@ -40,18 +47,18 @@ const CommonEntrySchema = { schema.siblingRef('field'), ConditionEntryField.HASH, schema.string({ - validate: (hash) => + validate: (hash: string) => isValidHash(hash) ? undefined : `invalidField.${ConditionEntryField.HASH}`, }), schema.conditional( schema.siblingRef('field'), ConditionEntryField.PATH, schema.string({ - validate: (field) => + validate: (field: string) => field.length > 0 ? undefined : `invalidField.${ConditionEntryField.PATH}`, }), schema.string({ - validate: (field) => + validate: (field: string) => field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER}`, }) ) @@ -99,7 +106,7 @@ const EntrySchemaDependingOnOS = schema.conditional( */ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, { minSize: 1, - validate(entries) { + validate(entries: ConditionEntry[]) { return ( getDuplicateFields(entries) .map((field) => `duplicatedEntry.${field}`) @@ -108,8 +115,8 @@ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, { }, }); -export const PostTrustedAppCreateRequestSchema = { - body: schema.object({ +const getTrustedAppForOsScheme = (forUpdateFlow: boolean = false) => + schema.object({ name: schema.string({ minLength: 1, maxLength: 256 }), description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })), os: schema.oneOf([ @@ -117,6 +124,26 @@ export const PostTrustedAppCreateRequestSchema = { schema.literal(OperatingSystem.LINUX), schema.literal(OperatingSystem.MAC), ]), + effectScope: schema.oneOf([ + schema.object({ + type: schema.literal('global'), + }), + schema.object({ + type: schema.literal('policy'), + policies: schema.arrayOf(schema.string({ minLength: 1 })), + }), + ]), entries: EntriesSchema, + ...(forUpdateFlow ? { version: schema.maybe(schema.string()) } : {}), + }); + +export const PostTrustedAppCreateRequestSchema = { + body: getTrustedAppForOsScheme(), +}; + +export const PutTrustedAppUpdateRequestSchema = { + params: schema.object({ + id: schema.string(), }), + body: getTrustedAppForOsScheme(true), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts new file mode 100644 index 0000000000000..fcde1d44b682d --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MaybeImmutable, NewTrustedApp, UpdateTrustedApp } from '../../types'; + +const NEW_TRUSTED_APP_KEYS: Array = [ + 'name', + 'effectScope', + 'entries', + 'description', + 'os', + 'version', +]; + +export const toUpdateTrustedApp = ( + trustedApp: MaybeImmutable +): UpdateTrustedApp => { + const trustedAppForUpdate: UpdateTrustedApp = {} as UpdateTrustedApp; + + for (const key of NEW_TRUSTED_APP_KEYS) { + // This should be safe. Its needed due to the inter-dependency on property values (`os` <=> `entries`) + // @ts-expect-error + trustedAppForUpdate[key] = trustedApp[key]; + } + return trustedAppForUpdate; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts similarity index 93% rename from x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts rename to x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts index faad639eeacb3..b0828be6af6c5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConditionEntry, ConditionEntryField } from '../types'; +import { ConditionEntry, ConditionEntryField } from '../../types'; const HASH_LENGTHS: readonly number[] = [ 32, // MD5 diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 87268f02a16e1..0b41dc5608fe9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -62,6 +62,11 @@ type ImmutableMap = ReadonlyMap, Immutable>; type ImmutableSet = ReadonlySet>; type ImmutableObject = { readonly [K in keyof T]: Immutable }; +/** + * Utility type that will return back a union of the given [T]ype and an Immutable version of it + */ +export type MaybeImmutable = T | Immutable; + /** * Stats for related events for a particular node in a resolver graph. */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index a5c3c1eab52b3..d36958c11d2a1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -9,14 +9,22 @@ import { TypeOf } from '@kbn/config-schema'; import { ApplicationStart } from 'kibana/public'; import { DeleteTrustedAppsRequestSchema, + GetOneTrustedAppRequestSchema, GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, + PutTrustedAppUpdateRequestSchema, } from '../schema/trusted_apps'; import { OperatingSystem } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; +export type GetOneTrustedAppRequestParams = TypeOf; + +export interface GetOneTrustedAppResponse { + data: TrustedApp; +} + /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; @@ -39,6 +47,15 @@ export interface PostTrustedAppCreateResponse { data: TrustedApp; } +/** API request params for updating a Trusted App */ +export type PutTrustedAppsRequestParams = TypeOf; + +/** API Request body for Updating a new Trusted App entry */ +export type PutTrustedAppUpdateRequest = TypeOf & + (MacosLinuxConditionEntries | WindowsConditionEntries); + +export type PutTrustedAppUpdateResponse = PostTrustedAppCreateResponse; + export interface GetTrustedAppsSummaryResponse { total: number; windows: number; @@ -76,17 +93,38 @@ export interface WindowsConditionEntries { entries: WindowsConditionEntry[]; } +export interface GlobalEffectScope { + type: 'global'; +} + +export interface PolicyEffectScope { + type: 'policy'; + /** An array of Endpoint Integration Policy UUIDs */ + policies: string[]; +} + +export type EffectScope = GlobalEffectScope | PolicyEffectScope; + /** Type for a new Trusted App Entry */ export type NewTrustedApp = { name: string; description?: string; + effectScope: EffectScope; } & (MacosLinuxConditionEntries | WindowsConditionEntries); +/** An Update to a Trusted App Entry */ +export type UpdateTrustedApp = NewTrustedApp & { + version?: string; +}; + /** A trusted app entry */ export type TrustedApp = NewTrustedApp & { + version: string; id: string; created_at: string; created_by: string; + updated_at: string; + updated_by: string; }; /** diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index c764c31a2d781..19de81cb95c3f 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -13,6 +13,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; */ const allowedExperimentalValues = Object.freeze({ fleetServerEnabled: false, + trustedAppsByPolicyEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 6fcf688fff7a7..c9fb502956053 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import React, { FC, isValidElement, memo, ReactElement, ReactNode, useMemo } from 'react'; +import React, { + FC, + isValidElement, + memo, + PropsWithChildren, + ReactElement, + ReactNode, + useMemo, +} from 'react'; import styled from 'styled-components'; import { EuiPanel, @@ -92,41 +100,46 @@ export const ItemDetailsAction: FC> = memo( ItemDetailsAction.displayName = 'ItemDetailsAction'; -export const ItemDetailsCard: FC = memo(({ children }) => { - const childElements = useMemo( - () => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]), - [children] - ); - - return ( - - - - - {childElements.get(ItemDetailsPropertySummary)} - - - - - -
{childElements.get(OTHER_NODES)}
-
- {childElements.has(ItemDetailsAction) && ( - - - {childElements.get(ItemDetailsAction)?.map((action, index) => ( - - {action} - - ))} - +export type ItemDetailsCardProps = PropsWithChildren<{ + 'data-test-subj'?: string; +}>; +export const ItemDetailsCard = memo( + ({ children, 'data-test-subj': dataTestSubj }) => { + const childElements = useMemo( + () => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]), + [children] + ); + + return ( + + + + + {childElements.get(ItemDetailsPropertySummary)} + + + + + +
{childElements.get(OTHER_NODES)}
- )} -
-
-
-
- ); -}); + {childElements.has(ItemDetailsAction) && ( + + + {childElements.get(ItemDetailsAction)?.map((action, index) => ( + + {action} + + ))} + + + )} +
+
+
+
+ ); + } +); ItemDetailsCard.displayName = 'ItemDetailsCard'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts new file mode 100644 index 0000000000000..2ac5948641d7d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; +import { useIsExperimentalFeatureEnabled } from './use_experimental_features'; + +jest.mock('react-redux'); +const useSelectorMock = useSelector as jest.Mock; +const mockAppState = { + app: { + enableExperimental: { + featureA: true, + featureB: false, + }, + }, +}; + +describe('useExperimentalFeatures', () => { + beforeEach(() => { + useSelectorMock.mockImplementation((cb) => { + return cb(mockAppState); + }); + }); + afterEach(() => { + useSelectorMock.mockClear(); + }); + it('throws an error when unexisting feature', async () => { + expect(() => + useIsExperimentalFeatureEnabled('unexistingFeature' as keyof ExperimentalFeatures) + ).toThrowError(); + }); + it('returns true when existing feature and is enabled', async () => { + const result = useIsExperimentalFeatureEnabled('featureA' as keyof ExperimentalFeatures); + + expect(result).toBeTruthy(); + }); + it('returns false when existing feature and is disabled', async () => { + const result = useIsExperimentalFeatureEnabled('featureB' as keyof ExperimentalFeatures); + + expect(result).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts new file mode 100644 index 0000000000000..247b7624914cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { State } from '../../common/store'; +import { + ExperimentalFeatures, + getExperimentalAllowedValues, +} from '../../../common/experimental_features'; + +const allowedExperimentalValues = getExperimentalAllowedValues(); + +export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { + return useSelector(({ app: { enableExperimental } }: State) => { + if (!enableExperimental || !(feature in enableExperimental)) { + throw new Error( + `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join( + ', ' + )}` + ); + } + return enableExperimental[feature]; + }); +}; diff --git a/x-pack/plugins/security_solution/public/common/store/app/model.ts b/x-pack/plugins/security_solution/public/common/store/app/model.ts index 38ecedc0c7ba7..5a252e4aa48f2 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/model.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { Note } from '../../lib/note'; export type ErrorState = ErrorModel; @@ -24,4 +25,5 @@ export type ErrorModel = Error[]; export interface AppModel { notesById: NotesById; errors: ErrorState; + enableExperimental?: ExperimentalFeatures; } diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts index 9a2289765e85d..d2808a02c8621 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { parseExperimentalConfigValue } from '../../..//common/experimental_features'; import { createInitialState } from './reducer'; jest.mock('../lib/kibana', () => ({ @@ -22,6 +23,7 @@ describe('createInitialState', () => { kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], configIndexPatterns: ['auditbeat-*', 'filebeat'], signalIndexName: 'siem-signals-default', + enableExperimental: parseExperimentalConfigValue([]), } ); @@ -35,6 +37,7 @@ describe('createInitialState', () => { kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], configIndexPatterns: [], signalIndexName: 'siem-signals-default', + enableExperimental: parseExperimentalConfigValue([]), } ); diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index 27fddafc3781f..c2ef2563fe63e 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -21,6 +21,7 @@ import { ManagementPluginReducer } from '../../management'; import { State } from './types'; import { AppAction } from './actions'; import { KibanaIndexPatterns } from './sourcerer/model'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & @@ -36,14 +37,16 @@ export const createInitialState = ( kibanaIndexPatterns, configIndexPatterns, signalIndexName, + enableExperimental, }: { kibanaIndexPatterns: KibanaIndexPatterns; configIndexPatterns: string[]; signalIndexName: string | null; + enableExperimental: ExperimentalFeatures; } ): PreloadedState => { const preloadedState: PreloadedState = { - app: initialAppState, + app: { ...initialAppState, enableExperimental }, dragAndDrop: initialDragAndDropState, ...pluginsInitState, inputs: createInitialInputsState(), diff --git a/x-pack/plugins/security_solution/public/common/store/test_utils.ts b/x-pack/plugins/security_solution/public/common/store/test_utils.ts index c1d54192c86b1..7616dfccddaff 100644 --- a/x-pack/plugins/security_solution/public/common/store/test_utils.ts +++ b/x-pack/plugins/security_solution/public/common/store/test_utils.ts @@ -9,6 +9,10 @@ import { Dispatch } from 'redux'; import { State, ImmutableMiddlewareFactory } from './types'; import { AppAction } from './actions'; +interface WaitForActionOptions { + validate?: (action: A extends { type: T } ? A : never) => boolean; +} + /** * Utilities for testing Redux middleware */ @@ -21,7 +25,10 @@ export interface MiddlewareActionSpyHelper(actionType: T) => Promise; + waitForAction: ( + actionType: T, + options?: WaitForActionOptions + ) => Promise; /** * A property holding the information around the calls that were processed by the internal * `actionSpyMiddelware`. This property holds the information typically found in Jets's mocked @@ -78,7 +85,7 @@ export const createSpyMiddleware = < let spyDispatch: jest.Mock>; return { - waitForAction: async (actionType) => { + waitForAction: async (actionType, options = {}) => { type ResolvedAction = A extends { type: typeof actionType } ? A : never; // Error is defined here so that we get a better stack trace that points to the test from where it was used @@ -87,6 +94,10 @@ export const createSpyMiddleware = < return new Promise((resolve, reject) => { const watch: ActionWatcher = (action) => { if (action.type === actionType) { + if (options.validate && !options.validate(action as ResolvedAction)) { + return; + } + watchers.delete(watch); clearTimeout(timeout); resolve(action as ResolvedAction); diff --git a/x-pack/plugins/security_solution/public/common/types.ts b/x-pack/plugins/security_solution/public/common/types.ts index 68346847eb8d1..f1a7cdc8abc60 100644 --- a/x-pack/plugins/security_solution/public/common/types.ts +++ b/x-pack/plugins/security_solution/public/common/types.ts @@ -10,3 +10,7 @@ export interface ServerApiError { error: string; message: string; } + +export interface SecuritySolutionUiConfigType { + enableExperimental: string[]; +} diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index cbcc054e7c6a9..bf754720f314b 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -108,6 +108,7 @@ const normalizeTrustedAppsPageLocation = ( : {}), ...(!isDefaultOrMissing(location.view_type, 'grid') ? { view_type: location.view_type } : {}), ...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}), + ...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}), }; } else { return {}; @@ -147,11 +148,20 @@ export const extractListPaginationParams = (query: querystring.ParsedUrlQuery) = export const extractTrustedAppsListPageLocation = ( query: querystring.ParsedUrlQuery -): TrustedAppsListPageLocation => ({ - ...extractListPaginationParams(query), - view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid', - show: extractFirstParamValue(query, 'show') === 'create' ? 'create' : undefined, -}); +): TrustedAppsListPageLocation => { + const showParamValue = extractFirstParamValue( + query, + 'show' + ) as TrustedAppsListPageLocation['show']; + + return { + ...extractListPaginationParams(query), + view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid', + show: + showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined, + id: extractFirstParamValue(query, 'id'), + }; +}; export const getTrustedAppsListPath = (location?: Partial): string => { const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 578043f4321e9..5f572251daeda 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -10,7 +10,9 @@ import { HttpStart } from 'kibana/public'; import { TRUSTED_APPS_CREATE_API, TRUSTED_APPS_DELETE_API, + TRUSTED_APPS_GET_API, TRUSTED_APPS_LIST_API, + TRUSTED_APPS_UPDATE_API, TRUSTED_APPS_SUMMARY_API, } from '../../../../../common/endpoint/constants'; @@ -21,19 +23,39 @@ import { PostTrustedAppCreateRequest, PostTrustedAppCreateResponse, GetTrustedAppsSummaryResponse, + PutTrustedAppUpdateRequest, + PutTrustedAppUpdateResponse, + PutTrustedAppsRequestParams, + GetOneTrustedAppRequestParams, + GetOneTrustedAppResponse, } from '../../../../../common/endpoint/types/trusted_apps'; import { resolvePathVariables } from './utils'; +import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/services/ingest'; export interface TrustedAppsService { + getTrustedApp(params: GetOneTrustedAppRequestParams): Promise; getTrustedAppsList(request: GetTrustedAppsListRequest): Promise; deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise; createTrustedApp(request: PostTrustedAppCreateRequest): Promise; + updateTrustedApp( + params: PutTrustedAppsRequestParams, + request: PutTrustedAppUpdateRequest + ): Promise; + getPolicyList( + options?: Parameters[1] + ): ReturnType; } export class TrustedAppsHttpService implements TrustedAppsService { constructor(private http: HttpStart) {} + async getTrustedApp(params: GetOneTrustedAppRequestParams) { + return this.http.get( + resolvePathVariables(TRUSTED_APPS_GET_API, params) + ); + } + async getTrustedAppsList(request: GetTrustedAppsListRequest) { return this.http.get(TRUSTED_APPS_LIST_API, { query: request, @@ -50,7 +72,21 @@ export class TrustedAppsHttpService implements TrustedAppsService { }); } + async updateTrustedApp( + params: PutTrustedAppsRequestParams, + updatedTrustedApp: PutTrustedAppUpdateRequest + ) { + return this.http.put( + resolvePathVariables(TRUSTED_APPS_UPDATE_API, params), + { body: JSON.stringify(updatedTrustedApp) } + ); + } + async getTrustedAppsSummary() { return this.http.get(TRUSTED_APPS_SUMMARY_API); } + + getPolicyList(options?: Parameters[1]) { + return sendGetEndpointSpecificPackagePolicies(this.http, options); + } } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index ea934881f6220..1c1fca4b55abc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -7,6 +7,7 @@ import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { AsyncResourceState } from '.'; +import { GetPolicyListResponse } from '../../policy/types'; export interface Pagination { pageIndex: number; @@ -29,7 +30,9 @@ export interface TrustedAppsListPageLocation { page_index: number; page_size: number; view_type: ViewType; - show?: 'create'; + show?: 'create' | 'edit'; + /** Used for editing. The ID of the selected trusted app */ + id?: string; } export interface TrustedAppsListPageState { @@ -51,9 +54,13 @@ export interface TrustedAppsListPageState { entry: NewTrustedApp; isValid: boolean; }; + /** The trusted app to be edited (when in edit mode) */ + editItem?: AsyncResourceState; confirmed: boolean; submissionResourceState: AsyncResourceState; }; + /** A list of all available polices for use in associating TA to policies */ + policies: AsyncResourceState; location: TrustedAppsListPageLocation; active: boolean; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 66f4eff81dbdd..3f9e9d53f69e4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -8,7 +8,11 @@ import { ConditionEntry, ConditionEntryField, + EffectScope, + GlobalEffectScope, MacosLinuxConditionEntry, + MaybeImmutable, + PolicyEffectScope, WindowsConditionEntry, } from '../../../../../common/endpoint/types'; @@ -23,3 +27,15 @@ export const isMacosLinuxTrustedAppCondition = ( ): condition is MacosLinuxConditionEntry => { return condition.field !== ConditionEntryField.SIGNER; }; + +export const isGlobalEffectScope = ( + effectedScope: MaybeImmutable +): effectedScope is GlobalEffectScope => { + return effectedScope.type === 'global'; +}; + +export const isPolicyEffectScope = ( + effectedScope: MaybeImmutable +): effectedScope is PolicyEffectScope => { + return effectedScope.type === 'policy'; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index aaa05f550b208..34f48142c7032 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -9,6 +9,7 @@ import { Action } from 'redux'; import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; import { AsyncResourceState, TrustedAppsListData } from '../state'; +import { GetPolicyListResponse } from '../../policy/types'; export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>; @@ -51,6 +52,10 @@ export type TrustedAppCreationDialogFormStateUpdated = Action<'trustedAppCreatio }; }; +export type TrustedAppCreationEditItemStateChanged = Action<'trustedAppCreationEditItemStateChanged'> & { + payload: AsyncResourceState; +}; + export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialogConfirmed'>; export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>; @@ -59,6 +64,10 @@ export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> & payload: AsyncResourceState; }; +export type TrustedAppsPoliciesStateChanged = Action<'trustedAppsPoliciesStateChanged'> & { + payload: AsyncResourceState; +}; + export type TrustedAppsPageAction = | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged @@ -67,8 +76,10 @@ export type TrustedAppsPageAction = | TrustedAppDeletionDialogConfirmed | TrustedAppDeletionDialogClosed | TrustedAppCreationSubmissionResourceStateChanged + | TrustedAppCreationEditItemStateChanged | TrustedAppCreationDialogStarted | TrustedAppCreationDialogFormStateUpdated | TrustedAppCreationDialogConfirmed | TrustedAppsExistResponse + | TrustedAppsPoliciesStateChanged | TrustedAppCreationDialogClosed; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index 3acb55904d298..ece2c9e29750f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -28,6 +28,7 @@ export const defaultNewTrustedApp = (): NewTrustedApp => ({ os: OperatingSystem.WINDOWS, entries: [defaultConditionEntry()], description: '', + effectScope: { type: 'global' }, }); export const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({ @@ -48,10 +49,12 @@ export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ }, deletionDialog: initialDeletionDialogState(), creationDialog: initialCreationDialogState(), + policies: { type: 'UninitialisedResourceState' }, location: { page_index: MANAGEMENT_DEFAULT_PAGE, page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, show: undefined, + id: undefined, view_type: 'grid', }, active: false, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index 064b108848d2f..ed45d077dd0ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -21,10 +21,11 @@ import { } from '../test_utils'; import { TrustedAppsService } from '../service'; -import { Pagination, TrustedAppsListPageState } from '../state'; +import { Pagination, TrustedAppsListPageLocation, TrustedAppsListPageState } from '../state'; import { initialTrustedAppsPageState } from './builders'; import { trustedAppsPageReducer } from './reducer'; import { createTrustedAppsPageMiddleware } from './middleware'; +import { Immutable } from '../../../../../common/endpoint/types'; const initialNow = 111111; const dateNowMock = jest.fn(); @@ -32,7 +33,7 @@ dateNowMock.mockReturnValue(initialNow); Date.now = dateNowMock; -const initialState = initialTrustedAppsPageState(); +const initialState: Immutable = initialTrustedAppsPageState(); const createGetTrustedListAppsResponse = (pagination: Partial) => { const fullPagination = { ...createDefaultPagination(), ...pagination }; @@ -49,6 +50,9 @@ const createTrustedAppsServiceMock = (): jest.Mocked => ({ getTrustedAppsList: jest.fn(), deleteTrustedApp: jest.fn(), createTrustedApp: jest.fn(), + getPolicyList: jest.fn(), + updateTrustedApp: jest.fn(), + getTrustedApp: jest.fn(), }); const createStoreSetup = (trustedAppsService: TrustedAppsService) => { @@ -87,6 +91,15 @@ describe('middleware', () => { }; }; + const createLocationState = ( + params?: Partial + ): TrustedAppsListPageLocation => { + return { + ...initialState.location, + ...(params ?? {}), + }; + }; + beforeEach(() => { dateNowMock.mockReturnValue(initialNow); }); @@ -102,7 +115,10 @@ describe('middleware', () => { describe('refreshing list resource state', () => { it('refreshes the list when location changes and data gets outdated', async () => { const pagination = { pageIndex: 2, pageSize: 50 }; - const location = { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' }; + const location = createLocationState({ + page_index: 2, + page_size: 50, + }); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -136,7 +152,10 @@ describe('middleware', () => { it('does not refresh the list when location changes and data does not get outdated', async () => { const pagination = { pageIndex: 2, pageSize: 50 }; - const location = { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' }; + const location = createLocationState({ + page_index: 2, + page_size: 50, + }); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -161,7 +180,7 @@ describe('middleware', () => { it('refreshes the list when data gets outdated with and outdate action', async () => { const newNow = 222222; const pagination = { pageIndex: 0, pageSize: 10 }; - const location = { page_index: 0, page_size: 10, show: undefined, view_type: 'grid' }; + const location = createLocationState(); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -224,7 +243,10 @@ describe('middleware', () => { freshDataTimestamp: initialNow, }, active: true, - location: { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' }, + location: createLocationState({ + page_index: 2, + page_size: 50, + }), }); const infiniteLoopTest = async () => { @@ -240,7 +262,7 @@ describe('middleware', () => { const entry = createSampleTrustedApp(3); const notFoundError = createServerApiError('Not Found'); const pagination = { pageIndex: 0, pageSize: 10 }; - const location = { page_index: 0, page_size: 10, show: undefined, view_type: 'grid' }; + const location = createLocationState(); const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination); const listView = createLoadedListViewWithPagination(initialNow, pagination); const listViewNew = createLoadedListViewWithPagination(newNow, pagination); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 3e83b213f0f7e..7f940f14f9c6c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { Immutable, PostTrustedAppCreateRequest, @@ -54,7 +55,15 @@ import { getListTotalItemsCount, trustedAppsListPageActive, entriesExistState, + policiesState, + isEdit, + isFetchingEditTrustedAppItem, + editItemId, + editingTrustedApp, + getListItems, + editItemState, } from './selectors'; +import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; const createTrustedAppsListResourceStateChangedAction = ( newState: Immutable> @@ -139,9 +148,11 @@ const submitCreationIfNeeded = async ( store: ImmutableMiddlewareAPI, trustedAppsService: TrustedAppsService ) => { - const submissionResourceState = getCreationSubmissionResourceState(store.getState()); - const isValid = isCreationDialogFormValid(store.getState()); - const entry = getCreationDialogFormEntry(store.getState()); + const currentState = store.getState(); + const submissionResourceState = getCreationSubmissionResourceState(currentState); + const isValid = isCreationDialogFormValid(currentState); + const entry = getCreationDialogFormEntry(currentState); + const editMode = isEdit(currentState); if (isStaleResourceState(submissionResourceState) && entry !== undefined && isValid) { store.dispatch( @@ -152,12 +163,27 @@ const submitCreationIfNeeded = async ( ); try { + let responseTrustedApp: TrustedApp; + + if (editMode) { + responseTrustedApp = ( + await trustedAppsService.updateTrustedApp( + { id: editItemId(currentState)! }, + // TODO: try to remove the cast + entry as PostTrustedAppCreateRequest + ) + ).data; + } else { + // TODO: try to remove the cast + responseTrustedApp = ( + await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest) + ).data; + } + store.dispatch( createTrustedAppCreationSubmissionResourceStateChanged({ type: 'LoadedResourceState', - // TODO: try to remove the cast - data: (await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest)) - .data, + data: responseTrustedApp, }) ); store.dispatch({ @@ -268,6 +294,139 @@ const checkTrustedAppsExistIfNeeded = async ( } }; +export const retrieveListOfPoliciesIfNeeded = async ( + { getState, dispatch }: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const currentState = getState(); + const currentPoliciesState = policiesState(currentState); + const isLoading = isLoadingResourceState(currentPoliciesState); + const isPageActive = trustedAppsListPageActive(currentState); + const isCreateFlow = isCreationDialogLocation(currentState); + + if (isPageActive && isCreateFlow && !isLoading) { + dispatch({ + type: 'trustedAppsPoliciesStateChanged', + payload: { + type: 'LoadingResourceState', + previousState: currentPoliciesState, + } as TrustedAppsListPageState['policies'], + }); + + try { + const policyList = await trustedAppsService.getPolicyList({ + query: { + page: 1, + perPage: 1000, + }, + }); + + dispatch({ + type: 'trustedAppsPoliciesStateChanged', + payload: { + type: 'LoadedResourceState', + data: policyList, + }, + }); + } catch (error) { + dispatch({ + type: 'trustedAppsPoliciesStateChanged', + payload: { + type: 'FailedResourceState', + error: error.body || error, + lastLoadedState: getLastLoadedResourceState(policiesState(getState())), + }, + }); + } + } +}; + +const fetchEditTrustedAppIfNeeded = async ( + { getState, dispatch }: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const currentState = getState(); + const isPageActive = trustedAppsListPageActive(currentState); + const isEditFlow = isEdit(currentState); + const isAlreadyFetching = isFetchingEditTrustedAppItem(currentState); + const editTrustedAppId = editItemId(currentState); + + if (isPageActive && isEditFlow && !isAlreadyFetching) { + if (!editTrustedAppId) { + const errorMessage = i18n.translate( + 'xpack.securitySolution.trustedapps.middleware.editIdMissing', + { + defaultMessage: 'No id provided', + } + ); + + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'FailedResourceState', + error: Object.assign(new Error(errorMessage), { statusCode: 404, error: errorMessage }), + }, + }); + return; + } + + let trustedAppForEdit = editingTrustedApp(currentState); + + // If Trusted App is already loaded, then do nothing + if (trustedAppForEdit && trustedAppForEdit.id === editTrustedAppId) { + return; + } + + // See if we can get the Trusted App record from the current list of Trusted Apps being displayed + trustedAppForEdit = getListItems(currentState).find((ta) => ta.id === editTrustedAppId); + + try { + // Retrieve Trusted App record via API if it was not in the list data. + // This would be the case when linking from another place or using an UUID for a Trusted App + // that is not currently displayed on the list view. + if (!trustedAppForEdit) { + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'LoadingResourceState', + // No easy way to get around this that I can see. `previousState` does not + // seem to allow everything that `editItem` state can hold, so not even sure if using + // type guards would work here + // @ts-ignore + previousState: editItemState(currentState)!, + }, + }); + + trustedAppForEdit = (await trustedAppsService.getTrustedApp({ id: editTrustedAppId })).data; + } + + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'LoadedResourceState', + data: trustedAppForEdit, + }, + }); + + dispatch({ + type: 'trustedAppCreationDialogFormStateUpdated', + payload: { + entry: toUpdateTrustedApp(trustedAppForEdit), + isValid: true, + }, + }); + } catch (e) { + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'FailedResourceState', + error: e, + }, + }); + } + } +}; + export const createTrustedAppsPageMiddleware = ( trustedAppsService: TrustedAppsService ): ImmutableMiddleware => { @@ -282,6 +441,8 @@ export const createTrustedAppsPageMiddleware = ( if (action.type === 'userChangedUrl') { updateCreationDialogIfNeeded(store); + retrieveListOfPoliciesIfNeeded(store, trustedAppsService); + fetchEditTrustedAppIfNeeded(store, trustedAppsService); } if (action.type === 'trustedAppCreationDialogConfirmed') { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 5f37d0d674558..6965172ef773d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -37,7 +37,13 @@ describe('reducer', () => { expect(result).toStrictEqual({ ...initialState, - location: { page_index: 5, page_size: 50, show: 'create', view_type: 'list' }, + location: { + page_index: 5, + page_size: 50, + show: 'create', + view_type: 'list', + id: undefined, + }, active: true, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index aff5cacf081c6..ea7bbb44c9bf2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -29,6 +29,8 @@ import { TrustedAppCreationDialogConfirmed, TrustedAppCreationDialogClosed, TrustedAppsExistResponse, + TrustedAppsPoliciesStateChanged, + TrustedAppCreationEditItemStateChanged, } from './action'; import { TrustedAppsListPageState } from '../state'; @@ -37,7 +39,7 @@ import { initialDeletionDialogState, initialTrustedAppsPageState, } from './builders'; -import { entriesExistState } from './selectors'; +import { entriesExistState, trustedAppsListPageActive } from './selectors'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -110,7 +112,7 @@ const trustedAppCreationDialogStarted: CaseReducer = ( + state, + action +) => { + return { + ...state, + creationDialog: { ...state.creationDialog, editItem: action.payload }, + }; +}; + const trustedAppCreationDialogConfirmed: CaseReducer = ( state ) => { @@ -155,6 +167,16 @@ const updateEntriesExists: CaseReducer = (state, { pay return state; }; +const updatePolicies: CaseReducer = (state, { payload }) => { + if (trustedAppsListPageActive(state)) { + return { + ...state, + policies: payload, + }; + } + return state; +}; + export const trustedAppsPageReducer: StateReducer = ( state = initialTrustedAppsPageState(), action @@ -187,6 +209,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppCreationDialogFormStateUpdated': return trustedAppCreationDialogFormStateUpdated(state, action); + case 'trustedAppCreationEditItemStateChanged': + return handleUpdateToEditItemState(state, action); + case 'trustedAppCreationDialogConfirmed': return trustedAppCreationDialogConfirmed(state, action); @@ -198,6 +223,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppsExistStateChanged': return updateEntriesExists(state, action); + + case 'trustedAppsPoliciesStateChanged': + return updatePolicies(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index baa68eb314140..7c131c3eaa7a9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -24,6 +24,7 @@ import { TrustedAppsListPageLocation, TrustedAppsListPageState, } from '../state'; +import { GetPolicyListResponse } from '../../policy/types'; export const needsRefreshOfListData = (state: Immutable): boolean => { const freshDataTimestamp = state.listView.freshDataTimestamp; @@ -130,7 +131,7 @@ export const getDeletionDialogEntry = ( }; export const isCreationDialogLocation = (state: Immutable): boolean => { - return state.location.show === 'create'; + return !!state.location.show; }; export const getCreationSubmissionResourceState = ( @@ -185,3 +186,56 @@ export const entriesExist: (state: Immutable) => boole export const trustedAppsListPageActive: (state: Immutable) => boolean = ( state ) => state.active; + +export const policiesState = ( + state: Immutable +): Immutable => state.policies; + +export const loadingPolicies: ( + state: Immutable +) => boolean = createSelector(policiesState, (policies) => isLoadingResourceState(policies)); + +export const listOfPolicies: ( + state: Immutable +) => Immutable = createSelector(policiesState, (policies) => { + return isLoadedResourceState(policies) ? policies.data.items : []; +}); + +export const isEdit: (state: Immutable) => boolean = createSelector( + getCurrentLocation, + ({ show }) => { + return show === 'edit'; + } +); + +export const editItemId: ( + state: Immutable +) => string | undefined = createSelector(getCurrentLocation, ({ id }) => { + return id; +}); + +export const editItemState: ( + state: Immutable +) => Immutable['creationDialog']['editItem'] = (state) => { + return state.creationDialog.editItem; +}; + +export const isFetchingEditTrustedAppItem: ( + state: Immutable +) => boolean = createSelector(editItemState, (editTrustedAppState) => { + return editTrustedAppState ? isLoadingResourceState(editTrustedAppState) : false; +}); + +export const editTrustedAppFetchError: ( + state: Immutable +) => ServerApiError | undefined = createSelector(editItemState, (itemForEditState) => { + return itemForEditState && getCurrentResourceError(itemForEditState); +}); + +export const editingTrustedApp: ( + state: Immutable +) => undefined | Immutable = createSelector(editItemState, (editTrustedAppState) => { + if (editTrustedAppState && isLoadedResourceState(editTrustedAppState)) { + return editTrustedAppState.data; + } +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index faf111b1a55d8..faffc6b04a0cd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -44,12 +44,16 @@ const generate = (count: number, generator: (i: number) => T) => export const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedApp => { return { id: String(i), + version: 'abc123', name: generate(longTexts ? 10 : 1, () => `trusted app ${i}`).join(' '), description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '), created_at: '1 minute ago', created_by: 'someone', + updated_at: '1 minute ago', + updated_by: 'someone', os: OPERATING_SYSTEMS[i % 3], entries: [], + effectScope: { type: 'global' }, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap new file mode 100644 index 0000000000000..35fc520558d6a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -0,0 +1,5573 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`When on the Trusted Apps Page and the Add Trusted App button is clicked and there is a feature flag for agents policy should display agents policy if feature flag is enabled 1`] = ` +Object { + "asFragment": [Function], + "baseElement": + .c0 { + padding: 24px; +} + +.c0.siemWrapperPage--fullHeight { + height: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.c0.siemWrapperPage--noPadding { + padding: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.c0.siemWrapperPage--withTimeline { + padding-bottom: 70px; +} + +.c3 { + margin-top: 8px; +} + +.c3 .siemSubtitle__item { + color: #6a717d; + font-size: 12px; + line-height: 1.5; +} + +.c1 { + margin-bottom: 24px; +} + +.c2 { + display: block; +} + +.c4 .euiFlyout { + z-index: 4001; +} + +.c5 .and-badge { + padding-top: 20px; + padding-bottom: calc(32px + (8px * 2) + 3px); +} + +.c5 .group-entries { + margin-bottom: 8px; +} + +.c5 .group-entries > * { + margin-bottom: 8px; +} + +.c5 .group-entries > *:last-child { + margin-bottom: 0; +} + +.c5 .and-button { + min-width: 95px; +} + +.c6 .policy-name .euiSelectableListItem__text { + -webkit-text-decoration: none !important; + text-decoration: none !important; + color: #343741 !important; +} + +.c7 { + background-color: #f5f7fa; + padding: 16px; +} + +.c10 { + padding: 16px; +} + +.c8.c8.c8 { + width: 40%; +} + +.c9.c9.c9 { + width: 60%; +} + +@media only screen and (min-width:575px) { + .c3 .siemSubtitle__item { + display: inline-block; + margin-right: 16px; + } + + .c3 .siemSubtitle__item:last-child { + margin-right: 0; + } +} + +
+
+
+
+
+

+ Trusted Applications +

+
+

+ Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security. +

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