From fd1809c3c296505faec09e5b2f52e0dd56f09eaa Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 14 Jul 2020 15:55:12 -0400 Subject: [PATCH 01/26] [Ingest Manager] Refactor Package Installation (#71521) * refactor installation to add/remove installed assets as they are added/removed * update types * uninstall assets when installation fails * refactor installation to add/remove installed assets as they are added/removed * update types Co-authored-by: Elastic Machine --- .../ingest_manager/common/types/models/epm.ts | 22 +- .../server/routes/data_streams/handlers.ts | 2 +- .../server/routes/epm/handlers.ts | 21 +- .../server/saved_objects/index.ts | 9 +- .../elasticsearch/ingest_pipeline/index.ts | 9 + .../elasticsearch/ingest_pipeline/install.ts | 22 +- .../elasticsearch/ingest_pipeline/remove.ts | 60 +++++ .../epm/elasticsearch/template/install.ts | 26 +- .../epm/elasticsearch/template/template.ts | 15 +- .../services/epm/kibana/assets/install.ts | 126 +++++++++ .../services/epm/packages/get_objects.ts | 32 --- .../server/services/epm/packages/index.ts | 2 +- .../server/services/epm/packages/install.ts | 254 +++++++++--------- .../server/services/epm/packages/remove.ts | 61 ++--- .../ingest_manager/server/types/index.tsx | 3 +- .../store/policy_list/test_mock_utils.ts | 33 +-- 16 files changed, 439 insertions(+), 258 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index a34038d4fba040..ab6a6c73843c52 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -229,7 +229,8 @@ export type PackageInfo = Installable< >; export interface Installation extends SavedObjectAttributes { - installed: AssetReference[]; + installed_kibana: KibanaAssetReference[]; + installed_es: EsAssetReference[]; es_index_patterns: Record; name: string; version: string; @@ -246,19 +247,14 @@ export type NotInstalled = T & { status: InstallationStatus.notInstalled; }; -export type AssetReference = Pick & { - type: AssetType | IngestAssetType; -}; +export type AssetReference = KibanaAssetReference | EsAssetReference; -/** - * Types of assets which can be installed/removed - */ -export enum IngestAssetType { - IlmPolicy = 'ilm_policy', - IndexTemplate = 'index_template', - ComponentTemplate = 'component_template', - IngestPipeline = 'ingest_pipeline', -} +export type KibanaAssetReference = Pick & { + type: KibanaAssetType; +}; +export type EsAssetReference = Pick & { + type: ElasticsearchAssetType; +}; export enum DefaultPackages { system = 'system', diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 2c65b08a68700c..df37aeb27c75c4 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -122,7 +122,7 @@ export const getListHandler: RequestHandler = async (context, request, response) if (pkg !== '' && pkgSavedObject.length > 0 && !packageMetadata[pkg]) { // then pick the dashboards from the package saved object const dashboards = - pkgSavedObject[0].attributes?.installed?.filter( + pkgSavedObject[0].attributes?.installed_kibana?.filter( (o) => o.type === KibanaAssetType.dashboard ) || []; // and then pick the human-readable titles from the dashboard saved objects diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index fe813f29b72e69..f54e61280b98ad 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -5,6 +5,7 @@ */ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; +import { appContextService } from '../../services'; import { GetInfoResponse, InstallPackageResponse, @@ -29,6 +30,7 @@ import { installPackage, removeInstallation, getLimitedPackages, + getInstallationObject, } from '../../services/epm/packages'; export const getCategoriesHandler: RequestHandler< @@ -146,10 +148,12 @@ export const getInfoHandler: RequestHandler> = async (context, request, response) => { + const logger = appContextService.getLogger(); + const savedObjectsClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const { pkgkey } = request.params; + const [pkgName, pkgVersion] = pkgkey.split('-'); try { - const { pkgkey } = request.params; - const savedObjectsClient = context.core.savedObjects.client; - const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const res = await installPackage({ savedObjectsClient, pkgkey, @@ -161,6 +165,17 @@ export const installPackageHandler: RequestHandler { + // unlike other ES assets, pipeline names are versioned so after a template is updated + // it can be created pointing to the new template, without removing the old one and effecting data + // so do not remove the currently installed pipelines here const datasets = registryPackage.datasets; const pipelinePaths = paths.filter((path) => isPipeline(path)); if (datasets) { - const pipelines = datasets.reduce>>((acc, dataset) => { + const pipelines = datasets.reduce>>((acc, dataset) => { if (dataset.ingest_pipeline) { acc.push( installPipelinesForDataset({ @@ -41,7 +46,8 @@ export const installPipelines = async ( } return acc; }, []); - return Promise.all(pipelines).then((results) => results.flat()); + const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat()); + return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave); } return []; }; @@ -77,7 +83,7 @@ export async function installPipelinesForDataset({ pkgVersion: string; paths: string[]; dataset: Dataset; -}): Promise { +}): Promise { const pipelinePaths = paths.filter((path) => isDatasetPipeline(path, dataset.path)); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -123,7 +129,7 @@ async function installPipeline({ }: { callCluster: CallESAsCurrentUser; pipeline: any; -}): Promise { +}): Promise { const callClusterParams: { method: string; path: string; @@ -146,7 +152,7 @@ async function installPipeline({ // which we could otherwise use. // See src/core/server/elasticsearch/api_types.ts for available endpoints. await callCluster('transport.request', callClusterParams); - return { id: pipeline.nameForInstallation, type: IngestAssetType.IngestPipeline }; + return { id: pipeline.nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; } const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/'); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts new file mode 100644 index 00000000000000..8be3a1beab3927 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { appContextService } from '../../../'; +import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; +import { getInstallation } from '../../packages/get'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; + +export const deletePipelines = async ( + callCluster: CallESAsCurrentUser, + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +) => { + const logger = appContextService.getLogger(); + const previousPipelinesPattern = `*-${pkgName}.*-${pkgVersion}`; + + try { + await deletePipeline(callCluster, previousPipelinesPattern); + } catch (e) { + logger.error(e); + } + try { + await deletePipelineRefs(savedObjectsClient, pkgName, pkgVersion); + } catch (e) { + logger.error(e); + } +}; + +export const deletePipelineRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +) => { + const installation = await getInstallation({ savedObjectsClient, pkgName }); + if (!installation) return; + const installedEsAssets = installation.installed_es; + const filteredAssets = installedEsAssets.filter(({ type, id }) => { + if (type !== ElasticsearchAssetType.ingestPipeline) return true; + if (!id.includes(pkgVersion)) return true; + return false; + }); + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: filteredAssets, + }); +}; +export async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise { + // '*' shouldn't ever appear here, but it still would delete all ingest pipelines + if (id && id !== '*') { + try { + await callCluster('ingest.deletePipeline', { id }); + } catch (err) { + throw new Error(`error deleting pipeline ${id}`); + } + } +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index e14645bbbf5fb0..436a6a1bdc55d7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Dataset, RegistryPackage, @@ -17,13 +18,14 @@ import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { generateMappings, generateTemplateName, getTemplate } from './template'; import * as Registry from '../../registry'; +import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; export const installTemplates = async ( registryPackage: RegistryPackage, + isUpdate: boolean, callCluster: CallESAsCurrentUser, - pkgName: string, - pkgVersion: string, - paths: string[] + paths: string[], + savedObjectsClient: SavedObjectsClientContract ): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates @@ -31,6 +33,12 @@ export const installTemplates = async ( await installPreBuiltComponentTemplates(paths, callCluster); await installPreBuiltTemplates(paths, callCluster); + // remove package installation's references to index templates + await removeAssetsFromInstalledEsByType( + savedObjectsClient, + registryPackage.name, + ElasticsearchAssetType.indexTemplate + ); // build templates per dataset from yml files const datasets = registryPackage.datasets; if (datasets) { @@ -46,7 +54,17 @@ export const installTemplates = async ( }, []); const res = await Promise.all(installTemplatePromises); - return res.flat(); + const installedTemplates = res.flat(); + // get template refs to save + const installedTemplateRefs = installedTemplates.map((template) => ({ + id: template.templateName, + type: ElasticsearchAssetType.indexTemplate, + })); + + // add package installation's references to index templates + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); + + return installedTemplates; } return []; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 77ad96952269f3..b907c735d2630e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -326,9 +326,10 @@ export const updateCurrentWriteIndices = async ( callCluster: CallESAsCurrentUser, templates: TemplateRef[] ): Promise => { - if (!templates) return; + if (!templates.length) return; const allIndices = await queryIndicesFromTemplates(callCluster, templates); + if (!allIndices.length) return; return updateAllIndices(allIndices, callCluster); }; @@ -358,12 +359,12 @@ const getIndices = async ( method: 'GET', path: `/_data_stream/${templateName}-*`, }); - if (res.length) { - return res.map((datastream: any) => ({ - indexName: datastream.indices[datastream.indices.length - 1].index_name, - indexTemplate, - })); - } + const dataStreams = res.data_streams; + if (!dataStreams.length) return; + return dataStreams.map((dataStream: any) => ({ + indexName: dataStream.indices[dataStream.indices.length - 1].index_name, + indexTemplate, + })); }; const updateAllIndices = async ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts new file mode 100644 index 00000000000000..2a743f244e64d6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObject, + SavedObjectsBulkCreateObject, + SavedObjectsClientContract, +} from 'src/core/server'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; +import * as Registry from '../../registry'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; +import { getInstallationObject, savedObjectTypes } from '../../packages'; +import { saveInstalledKibanaRefs } from '../../packages/install'; + +type SavedObjectToBe = Required & { type: AssetType }; +export type ArchiveAsset = Pick< + SavedObject, + 'id' | 'attributes' | 'migrationVersion' | 'references' +> & { + type: AssetType; +}; + +export async function getKibanaAsset(key: string) { + const buffer = Registry.getAsset(key); + + // cache values are buffers. convert to string / JSON + return JSON.parse(buffer.toString('utf8')); +} + +export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectToBe { + // convert that to an object + return { + type: asset.type, + id: asset.id, + attributes: asset.attributes, + references: asset.references || [], + migrationVersion: asset.migrationVersion || {}, + }; +} + +// TODO: make it an exhaustive list +// e.g. switch statement with cases for each enum key returning `never` for default case +export async function installKibanaAssets(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + paths: string[]; + isUpdate: boolean; +}): Promise { + const { savedObjectsClient, paths, pkgName, isUpdate } = options; + + if (isUpdate) { + // delete currently installed kibana saved objects and installation references + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedKibanaRefs = installedPkg?.attributes.installed_kibana; + + if (installedKibanaRefs?.length) { + await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs); + await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs); + } + } + + // install the new assets and save installation references + const kibanaAssetTypes = Object.values(KibanaAssetType); + const installationPromises = kibanaAssetTypes.map((assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + ); + // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] + // call .flat to flatten into one dimensional array + const newInstalledKibanaAssets = await Promise.all(installationPromises).then((results) => + results.flat() + ); + await saveInstalledKibanaRefs(savedObjectsClient, pkgName, newInstalledKibanaAssets); + return newInstalledKibanaAssets; +} +export const deleteKibanaInstalledRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + installedKibanaRefs: AssetReference[] +) => { + const installedAssetsToSave = installedKibanaRefs.filter(({ id, type }) => { + const assetType = type as AssetType; + return !savedObjectTypes.includes(assetType); + }); + + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_kibana: installedAssetsToSave, + }); +}; + +async function installKibanaSavedObjects({ + savedObjectsClient, + assetType, + paths, +}: { + savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; + paths: string[]; +}) { + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const toBeSavedObjects = await Promise.all( + kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + ); + + if (toBeSavedObjects.length === 0) { + return []; + } else { + const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { + overwrite: true, + }); + const createdObjects = createResults.saved_objects; + const installed = createdObjects.map(toAssetReference); + return installed; + } +} + +function toAssetReference({ id, type }: SavedObject) { + const reference: AssetReference = { id, type: type as KibanaAssetType }; + + return reference; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts deleted file mode 100644 index b623295c5e0604..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; -import { AssetType } from '../../../types'; -import * as Registry from '../registry'; - -type ArchiveAsset = Pick; -type SavedObjectToBe = Required & { type: AssetType }; - -export async function getObject(key: string) { - const buffer = Registry.getAsset(key); - - // cache values are buffers. convert to string / JSON - const json = buffer.toString('utf8'); - // convert that to an object - const asset: ArchiveAsset = JSON.parse(json); - - const { type, file } = Registry.pathParts(key); - const savedObject: SavedObjectToBe = { - type, - id: file.replace('.json', ''), - attributes: asset.attributes, - references: asset.references || [], - migrationVersion: asset.migrationVersion || {}, - }; - - return savedObject; -} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 4bb803dfaf9127..57c4f77432455d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -23,7 +23,7 @@ export { SearchParams, } from './get'; -export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; +export { installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 910283549abdfc..35c5b58a93710d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,27 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, Installation, - KibanaAssetType, CallESAsCurrentUser, DefaultPackages, + AssetType, + KibanaAssetReference, + EsAssetReference, ElasticsearchAssetType, - IngestAssetType, } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getObject } from './get_objects'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; +import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove'; +import { installKibanaAssets } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; export async function installLatestPackage(options: { @@ -92,127 +92,113 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); - const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); - // see if some version of this package is already installed // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (pkgVersion < latestPackage.version) throw Boom.badRequest('Cannot install or update to an out-of-date package'); + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); + const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); + + // get the currently installed package + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false; + const reinstall = pkgVersion === installedPkg?.attributes.version; const removable = !isRequiredPackage(pkgName); const { internal = false } = registryPackageInfo; + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // delete the previous version's installation's SO kibana assets before installing new ones - // in case some assets were removed in the new version - if (installedPkg) { - try { - await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); - } catch (err) { - // log these errors, some assets may not exist if deleted during a failed update - } - } - - const [installedKibanaAssets, installedPipelines] = await Promise.all([ - installKibanaAssets({ + // add the package installation to the saved object + if (!installedPkg) { + await createInstallation({ savedObjectsClient, pkgName, pkgVersion, - paths, - }), - installPipelines(registryPackageInfo, paths, callCluster), - // index patterns and ilm policies are not currently associated with a particular package - // so we do not save them in the package saved object state. - installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), - // currenly only the base package has an ILM policy - // at some point ILM policies can be installed/modified - // per dataset and we should then save them - installILMPolicy(paths, callCluster), - ]); + internal, + removable, + installed_kibana: [], + installed_es: [], + toSaveESIndexPatterns, + }); + } - // install or update the templates + const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); + const installKibanaAssetsPromise = installKibanaAssets({ + savedObjectsClient, + pkgName, + paths, + isUpdate, + }); + + // the rest of the installation must happen in sequential order + + // currently only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per dataset and we should then save them + await installILMPolicy(paths, callCluster); + + // installs versionized pipelines without removing currently installed ones + const installedPipelines = await installPipelines( + registryPackageInfo, + paths, + callCluster, + savedObjectsClient + ); + // install or update the templates referencing the newly installed pipelines const installedTemplates = await installTemplates( registryPackageInfo, + isUpdate, callCluster, - pkgName, - pkgVersion, - paths + paths, + savedObjectsClient ); - const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); + // update current backing indices of each data stream + await updateCurrentWriteIndices(callCluster, installedTemplates); + + // if this is an update, delete the previous version's pipelines + if (installedPkg && !reinstall) { + await deletePipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.version + ); + } + + // update to newly installed version when all assets are successfully installed + if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, - type: IngestAssetType.IndexTemplate, + type: ElasticsearchAssetType.indexTemplate, })); - - if (installedPkg) { - // update current index for every index template created - await updateCurrentWriteIndices(callCluster, installedTemplates); - if (!reinstall) { - try { - // delete the previous version's installation's pipelines - // this must happen after the template is updated - await deleteAssetsByType({ - savedObjectsClient, - callCluster, - installedObjects: installedPkg.attributes.installed, - assetType: ElasticsearchAssetType.ingestPipeline, - }); - } catch (err) { - throw new Error(err.message); - } - } - } - const toSaveAssetRefs: AssetReference[] = [ - ...installedKibanaAssets, - ...installedPipelines, - ...installedTemplateRefs, - ]; - // Save references to installed assets in the package's saved object state - return saveInstallationReferences({ - savedObjectsClient, - pkgName, - pkgVersion, - internal, - removable, - toSaveAssetRefs, - toSaveESIndexPatterns, - }); -} - -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case -export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; - paths: string[]; -}) { - const { savedObjectsClient, paths } = options; - - // Only install Kibana assets during package installation. - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) - ); - - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array - return Promise.all(installationPromises).then((results) => results.flat()); + const [installedKibanaAssets] = await Promise.all([ + installKibanaAssetsPromise, + installIndexPatternPromise, + ]); + return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; } - -export async function saveInstallationReferences(options: { +const updateVersion = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +) => { + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + version: pkgVersion, + }); +}; +export async function createInstallation(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; internal: boolean; removable: boolean; - toSaveAssetRefs: AssetReference[]; + installed_kibana: KibanaAssetReference[]; + installed_es: EsAssetReference[]; toSaveESIndexPatterns: Record; }) { const { @@ -221,14 +207,15 @@ export async function saveInstallationReferences(options: { pkgVersion, internal, removable, - toSaveAssetRefs, + installed_kibana: installedKibana, + installed_es: installedEs, toSaveESIndexPatterns, } = options; - await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { - installed: toSaveAssetRefs, + installed_kibana: installedKibana, + installed_es: installedEs, es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, @@ -237,37 +224,46 @@ export async function saveInstallationReferences(options: { }, { id: pkgName, overwrite: true } ); - - return toSaveAssetRefs; + return [...installedKibana, ...installedEs]; } -async function installKibanaSavedObjects({ - savedObjectsClient, - assetType, - paths, -}: { - savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; - paths: string[]; -}) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); +export const saveInstalledKibanaRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + installedAssets: AssetReference[] +) => { + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_kibana: installedAssets, + }); + return installedAssets; +}; - if (toBeSavedObjects.length === 0) { - return []; - } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; - } -} +export const saveInstalledEsRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + installedAssets: EsAssetReference[] +) => { + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedAssetsToSave = installedPkg?.attributes.installed_es.concat(installedAssets); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: installedAssetsToSave, + }); + return installedAssets; +}; -function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; +export const removeAssetsFromInstalledEsByType = async ( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + assetType: AssetType +) => { + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedAssets = installedPkg?.attributes.installed_es; + if (!installedAssets?.length) return; + const installedAssetsToSave = installedAssets?.filter(({ id, type }) => { + return type !== assetType; + }); - return reference; -} + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: installedAssetsToSave, + }); +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 94af672d8e29f7..81bc5847e6c0e5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -10,8 +10,9 @@ import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '.. import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; import { CallESAsCurrentUser } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; +import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; import { installIndexPatterns } from '../kibana/index_pattern/install'; -import { packageConfigService } from '../..'; +import { packageConfigService, appContextService } from '../..'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -25,7 +26,6 @@ export async function removeInstallation(options: { if (!installation) throw Boom.badRequest(`${pkgName} is not installed`); if (installation.removable === false) throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); - const installedObjects = installation.installed || []; const { total } = await packageConfigService.list(savedObjectsClient, { kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, @@ -38,48 +38,40 @@ export async function removeInstallation(options: { `unable to remove package with existing package config(s) in use by agent(s)` ); - // Delete the manager saved object with references to the asset objects - // could also update with [] or some other state - await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); - // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); - // Delete the installed asset - await deleteAssets(installedObjects, savedObjectsClient, callCluster); + // Delete the installed assets + const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; + await deleteAssets(installedAssets, savedObjectsClient, callCluster); + + // Delete the manager saved object with references to the asset objects + // could also update with [] or some other state + await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); // successful delete's in SO client return {}. return something more useful - return installedObjects; + return installedAssets; } async function deleteAssets( installedObjects: AssetReference[], savedObjectsClient: SavedObjectsClientContract, callCluster: CallESAsCurrentUser ) { + const logger = appContextService.getLogger(); const deletePromises = installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { - savedObjectsClient.delete(assetType, id); + return savedObjectsClient.delete(assetType, id); } else if (assetType === ElasticsearchAssetType.ingestPipeline) { - deletePipeline(callCluster, id); + return deletePipeline(callCluster, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { - deleteTemplate(callCluster, id); + return deleteTemplate(callCluster, id); } }); try { await Promise.all([...deletePromises]); } catch (err) { - throw new Error(err.message); - } -} -async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise { - // '*' shouldn't ever appear here, but it still would delete all ingest pipelines - if (id && id !== '*') { - try { - await callCluster('ingest.deletePipeline', { id }); - } catch (err) { - throw new Error(`error deleting pipeline ${id}`); - } + logger.error(err); } } @@ -108,31 +100,14 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P } } -export async function deleteAssetsByType({ - savedObjectsClient, - callCluster, - installedObjects, - assetType, -}: { - savedObjectsClient: SavedObjectsClientContract; - callCluster: CallESAsCurrentUser; - installedObjects: AssetReference[]; - assetType: ElasticsearchAssetType; -}) { - const toDelete = installedObjects.filter((asset) => asset.type === assetType); - try { - await deleteAssets(toDelete, savedObjectsClient, callCluster); - } catch (err) { - throw new Error(err.message); - } -} - export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, installedObjects: AssetReference[] ) { + const logger = appContextService.getLogger(); const deletePromises = installedObjects.map(({ id, type }) => { const assetType = type as AssetType; + if (savedObjectTypes.includes(assetType)) { return savedObjectsClient.delete(assetType, id); } @@ -140,6 +115,6 @@ export async function deleteKibanaSavedObjectsAssets( try { await Promise.all(deletePromises); } catch (err) { - throw new Error('error deleting saved object asset'); + logger.warn(err); } } diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index a559ca18cfedef..5d0683a37dc5e4 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -43,8 +43,9 @@ export { Dataset, RegistryElasticsearch, AssetReference, + EsAssetReference, + KibanaAssetReference, ElasticsearchAssetType, - IngestAssetType, RegistryPackage, AssetType, Installable, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 963b7922a7bff7..b5c67cc2c20140 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -9,7 +9,8 @@ import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; import { - AssetReference, + KibanaAssetReference, + EsAssetReference, GetPackagesResponse, InstallationStatus, } from '../../../../../../../ingest_manager/common'; @@ -43,26 +44,28 @@ export const apiPathMockResponseProviders = { type: 'epm-packages', id: 'endpoint', attributes: { - installed: [ + installed_kibana: [ { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, - { id: 'logs-endpoint.alerts', type: 'index-template' }, - { id: 'events-endpoint', type: 'index-template' }, - { id: 'logs-endpoint.events.file', type: 'index-template' }, - { id: 'logs-endpoint.events.library', type: 'index-template' }, - { id: 'metrics-endpoint.metadata', type: 'index-template' }, - { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' }, - { id: 'logs-endpoint.events.network', type: 'index-template' }, - { id: 'metrics-endpoint.policy', type: 'index-template' }, - { id: 'logs-endpoint.events.process', type: 'index-template' }, - { id: 'logs-endpoint.events.registry', type: 'index-template' }, - { id: 'logs-endpoint.events.security', type: 'index-template' }, - { id: 'metrics-endpoint.telemetry', type: 'index-template' }, - ] as AssetReference[], + ] as KibanaAssetReference[], + installed_es: [ + { id: 'logs-endpoint.alerts', type: 'index_template' }, + { id: 'events-endpoint', type: 'index_template' }, + { id: 'logs-endpoint.events.file', type: 'index_template' }, + { id: 'logs-endpoint.events.library', type: 'index_template' }, + { id: 'metrics-endpoint.metadata', type: 'index_template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, + { id: 'logs-endpoint.events.network', type: 'index_template' }, + { id: 'metrics-endpoint.policy', type: 'index_template' }, + { id: 'logs-endpoint.events.process', type: 'index_template' }, + { id: 'logs-endpoint.events.registry', type: 'index_template' }, + { id: 'logs-endpoint.events.security', type: 'index_template' }, + { id: 'metrics-endpoint.telemetry', type: 'index_template' }, + ] as EsAssetReference[], es_index_patterns: { alerts: 'logs-endpoint.alerts-*', events: 'events-endpoint-*', From 0b675b89084b18faa1db1ca99ecd500a78af8f57 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 14 Jul 2020 14:59:21 -0500 Subject: [PATCH 02/26] [DOCS] Fixes to API docs (#71678) * [DOCS] Fixes to API docs * Fixes rogue -u --- docs/api/dashboard/export-dashboard.asciidoc | 2 +- docs/api/dashboard/import-dashboard.asciidoc | 2 +- .../create-logstash.asciidoc | 2 +- .../delete-pipeline.asciidoc | 2 +- docs/api/role-management/put.asciidoc | 10 +++++----- docs/api/saved-objects/bulk_create.asciidoc | 2 +- docs/api/saved-objects/bulk_get.asciidoc | 2 +- docs/api/saved-objects/create.asciidoc | 2 +- docs/api/saved-objects/delete.asciidoc | 2 +- docs/api/saved-objects/export.asciidoc | 8 ++++---- docs/api/saved-objects/find.asciidoc | 4 ++-- docs/api/saved-objects/get.asciidoc | 4 ++-- docs/api/saved-objects/import.asciidoc | 6 +++--- .../resolve_import_errors.asciidoc | 6 +++--- docs/api/saved-objects/update.asciidoc | 2 +- .../copy_saved_objects.asciidoc | 4 ++-- docs/api/spaces-management/post.asciidoc | 2 +- docs/api/spaces-management/put.asciidoc | 2 +- ...olve_copy_saved_objects_conflicts.asciidoc | 2 +- .../batch_reindexing.asciidoc | 6 ++++-- .../check_reindex_status.asciidoc | 1 + docs/api/url-shortening.asciidoc | 19 ++++++++++++------- docs/api/using-api.asciidoc | 2 +- 23 files changed, 51 insertions(+), 43 deletions(-) diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 36c551dee84fcc..2099fb599ba67c 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -35,7 +35,7 @@ experimental[] Export dashboards and corresponding saved objects. [source,sh] -------------------------------------------------- -$ curl -X GET "localhost:5601/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c" <1> +$ curl -X GET api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c <1> -------------------------------------------------- // KIBANA diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 320859f78c617a..020ec8018b85b4 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -42,7 +42,7 @@ Use the complete response body from the < "index1", @@ -40,7 +40,9 @@ POST /api/upgrade_assistant/reindex/batch ] } -------------------------------------------------- -<1> The order in which the indices are provided here determines the order in which the reindex tasks will be executed. +// KIBANA + +<1> The order of the indices determines the order that the reindex tasks are executed. Similar to the <>, the API returns the following: diff --git a/docs/api/upgrade-assistant/check_reindex_status.asciidoc b/docs/api/upgrade-assistant/check_reindex_status.asciidoc index 00801f201d1e14..98cf263673f730 100644 --- a/docs/api/upgrade-assistant/check_reindex_status.asciidoc +++ b/docs/api/upgrade-assistant/check_reindex_status.asciidoc @@ -64,6 +64,7 @@ The API returns the following: `3`:: Paused ++ NOTE: If the {kib} node that started the reindex is shutdown or restarted, the reindex goes into a paused state after some time. To resume the reindex, you must submit a new POST request to the `/api/upgrade_assistant/reindex/` endpoint. diff --git a/docs/api/url-shortening.asciidoc b/docs/api/url-shortening.asciidoc index a62529e11a9baa..ffe1d925e5dcb3 100644 --- a/docs/api/url-shortening.asciidoc +++ b/docs/api/url-shortening.asciidoc @@ -1,5 +1,5 @@ [[url-shortening-api]] -=== Shorten URL API +== Shorten URL API ++++ Shorten URL ++++ @@ -9,34 +9,39 @@ Internet Explorer has URL length restrictions, and some wiki and markup parsers Short URLs are designed to make sharing {kib} URLs easier. +[float] [[url-shortening-api-request]] -==== Request +=== Request `POST :/api/shorten_url` +[float] [[url-shortening-api-request-body]] -==== Request body +=== Request body `url`:: (Required, string) The {kib} URL that you want to shorten, relative to `/app/kibana`. +[float] [[url-shortening-api-response-body]] -==== Response body +=== Response body urlId:: A top-level property that contains the shortened URL token for the provided request body. +[float] [[url-shortening-api-codes]] -==== Response code +=== Response code `200`:: Indicates a successful call. +[float] [[url-shortening-api-example]] -==== Example +=== Example [source,sh] -------------------------------------------------- -$ curl -X POST "localhost:5601/api/shorten_url" +$ curl -X POST api/shorten_url { "url": "/app/kibana#/dashboard?_g=()&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(),gridData:(h:15,i:'1',w:24,x:0,y:0),id:'8f4d0c00-4c86-11e8-b3d7-01146121b73d',panelIndex:'1',type:visualization,version:'7.0.0-alpha1')),query:(language:lucene,query:''),timeRestore:!f,title:'New%20Dashboard',viewMode:edit)" } diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index e58d9c39ee8c4a..188c8f9a5909db 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -31,7 +31,7 @@ For example, the following `curl` command exports a dashboard: [source,sh] -- -curl -X POST -u $USER:$PASSWORD "localhost:5601/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c" +curl -X POST api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c -- // KIBANA From debcdbac3341cc9f8278d035926de505e79e38ec Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 14 Jul 2020 13:01:12 -0700 Subject: [PATCH 03/26] Fix mappings for Upgrade Assistant reindexOperationSavedObjectType. (#71710) --- .../reindex_operation_saved_object_type.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts index ba661fbeceb267..d8976cf19f7e83 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts @@ -15,13 +15,25 @@ export const reindexOperationSavedObjectType: SavedObjectsType = { mappings: { properties: { reindexTaskId: { - type: 'keyword', + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, }, indexName: { type: 'keyword', }, newIndexName: { - type: 'keyword', + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, }, status: { type: 'integer', @@ -30,10 +42,19 @@ export const reindexOperationSavedObjectType: SavedObjectsType = { type: 'date', }, lastCompletedStep: { - type: 'integer', + type: 'long', }, + // Note that reindex failures can result in extremely long error messages coming from ES. + // We need to map these errors as text and use ignore_above to prevent indexing really large + // messages as keyword. See https://github.com/elastic/kibana/issues/71642 for more info. errorMessage: { - type: 'keyword', + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, }, reindexTaskPercComplete: { type: 'float', From 6d5a18732c022dd56441c1eb0d94d3e0ad786f84 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 14 Jul 2020 22:17:50 +0200 Subject: [PATCH 04/26] removes timeline callout (#71718) --- .../timelines/components/open_timeline/open_timeline.tsx | 3 +-- .../timelines/components/open_timeline/translations.ts | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 60b009f59c13b0..13786c55e2a8d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiBasicTable, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -183,7 +183,6 @@ export const OpenTimeline = React.memo( /> - {!!timelineFilter && timelineFilter} Date: Tue, 14 Jul 2020 22:39:44 +0200 Subject: [PATCH 05/26] [Uptime] Visitors breakdowns and enable rum view only via URL (#71428) Co-authored-by: Elastic Machine --- .../cypress/integration/rum_dashboard.feature | 24 ++--- .../apm/e2e/cypress/integration/snapshots.js | 16 ---- .../step_definitions/rum/page_load_dist.ts | 4 +- .../step_definitions/rum/rum_dashboard.ts | 36 +++---- .../rum/service_name_filter.ts | 6 +- .../apm/public/components/app/Home/index.tsx | 16 +--- .../app/Main/route_config/index.tsx | 14 +-- .../Breakdowns/BreakdownGroup.tsx | 1 + .../Charts/VisitorBreakdownChart.tsx | 96 +++++++++++++++++++ .../app/RumDashboard/ClientMetrics/index.tsx | 1 + .../PageLoadDistribution/index.tsx | 1 + .../app/RumDashboard/PageViewsTrend/index.tsx | 1 + .../app/RumDashboard/RumDashboard.tsx | 13 ++- .../app/RumDashboard/RumHeader/index.tsx | 20 ++++ .../components/app/RumDashboard/RumHome.tsx | 27 ++++++ .../RumDashboard/VisitorBreakdown/index.tsx | 65 +++++++++++++ .../components/app/RumDashboard/index.tsx | 27 +++--- .../app/RumDashboard/translations.ts | 7 ++ .../app/ServiceDetails/ServiceDetailTabs.tsx | 24 +---- .../components/shared/KueryBar/index.tsx | 2 +- .../shared/Links/apm/RumOverviewLink.tsx | 27 ------ .../ServiceNameFilter/index.tsx | 4 +- .../context/UrlParamsContext/helpers.ts | 1 - .../lib/rum_client/get_visitor_breakdown.ts | 77 +++++++++++++++ .../apm/server/routes/create_apm_api.ts | 2 + .../plugins/apm/server/routes/rum_client.ts | 13 +++ 26 files changed, 373 insertions(+), 152 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx delete mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature index c98e3f81b2bc67..be1597c8340ebe 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature @@ -1,10 +1,8 @@ Feature: RUM Dashboard Scenario: Client metrics - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should have correct client metrics + When a user browses the APM UI application for RUM Data + Then should have correct client metrics Scenario Outline: Rum page filters When the user filters by "" @@ -15,22 +13,16 @@ Feature: RUM Dashboard | location | Scenario: Page load distribution percentiles - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should display percentile for page load chart + When a user browses the APM UI application for RUM Data + Then should display percentile for page load chart Scenario: Page load distribution chart tooltip - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should display tooltip on hover + When a user browses the APM UI application for RUM Data + Then should display tooltip on hover Scenario: Page load distribution chart legends - Given a user browses the APM UI application for RUM Data - When the user inspects the real user monitoring tab - Then should redirect to rum dashboard - And should display chart legend + When a user browses the APM UI application for RUM Data + Then should display chart legend Scenario: Breakdown filter Given a user click page load breakdown filter diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index 7fbce2583903c6..6ee204781c8a75 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,11 +1,6 @@ module.exports = { "__version": "4.9.0", "RUM Dashboard": { - "Client metrics": { - "1": "55 ", - "2": "0.08 sec", - "3": "0.01 sec" - }, "Rum page filters (example #1)": { "1": "8 ", "2": "0.08 sec", @@ -16,19 +11,8 @@ module.exports = { "2": "0.07 sec", "3": "0.01 sec" }, - "Page load distribution percentiles": { - "1": "50th", - "2": "75th", - "3": "90th", - "4": "95th" - }, "Page load distribution chart legends": { "1": "Overall" - }, - "Service name filter": { - "1": "7 ", - "2": "0.07 sec", - "3": "0.01 sec" } } } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts index 89dc3437c3e69f..f319f7ef986673 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts @@ -27,7 +27,9 @@ When(`the user selected the breakdown`, () => { Then(`breakdown series should appear in chart`, () => { cy.get('.euiLoadingChart').should('not.be.visible'); - cy.get('div.echLegendItem__label[title=Chrome] ') + cy.get('div.echLegendItem__label[title=Chrome] ', { + timeout: DEFAULT_TIMEOUT, + }) .invoke('text') .should('eq', 'Chrome'); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts index 24961ceb3b3c21..ac7aaf33b78496 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { Given, Then } from 'cypress-cucumber-preprocessor/steps'; import { loginAndWaitForPage } from '../../../integration/helpers'; /** The default time in ms to wait for a Cypress command to complete */ @@ -14,18 +14,10 @@ Given(`a user browses the APM UI application for RUM Data`, () => { // open service overview page const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; - loginAndWaitForPage(`/app/apm#/services`, { from: RANGE_FROM, to: RANGE_TO }); -}); - -When(`the user inspects the real user monitoring tab`, () => { - // click rum tab - cy.get(':contains(Real User Monitoring)', { timeout: DEFAULT_TIMEOUT }) - .last() - .click({ force: true }); -}); - -Then(`should redirect to rum dashboard`, () => { - cy.url().should('contain', `/app/apm#/rum-overview`); + loginAndWaitForPage(`/app/apm#/rum-preview`, { + from: RANGE_FROM, + to: RANGE_TO, + }); }); Then(`should have correct client metrics`, () => { @@ -33,31 +25,33 @@ Then(`should have correct client metrics`, () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title', { timeout: DEFAULT_TIMEOUT }).should('be.visible'); + cy.get('.euiSelect-isLoading').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - cy.get(clientMetrics).eq(2).invoke('text').snapshot(); + cy.get(clientMetrics).eq(2).should('have.text', '55 '); - cy.get(clientMetrics).eq(1).invoke('text').snapshot(); + cy.get(clientMetrics).eq(1).should('have.text', '0.08 sec'); - cy.get(clientMetrics).eq(0).invoke('text').snapshot(); + cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); }); Then(`should display percentile for page load chart`, () => { const pMarkers = '[data-cy=percentile-markers] span'; - cy.get('.euiLoadingChart').should('be.visible'); + cy.get('.euiLoadingChart', { timeout: DEFAULT_TIMEOUT }).should('be.visible'); // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - cy.get(pMarkers).eq(0).invoke('text').snapshot(); + cy.get(pMarkers).eq(0).should('have.text', '50th'); - cy.get(pMarkers).eq(1).invoke('text').snapshot(); + cy.get(pMarkers).eq(1).should('have.text', '75th'); - cy.get(pMarkers).eq(2).invoke('text').snapshot(); + cy.get(pMarkers).eq(2).should('have.text', '90th'); - cy.get(pMarkers).eq(3).invoke('text').snapshot(); + cy.get(pMarkers).eq(3).should('have.text', '95th'); }); Then(`should display chart legend`, () => { diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts index 9a3d7b52674b77..b0694c902085ad 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts @@ -22,9 +22,9 @@ Then(`it displays relevant client metrics`, () => { cy.get('kbnLoadingIndicator').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - cy.get(clientMetrics).eq(2).invoke('text').snapshot(); + cy.get(clientMetrics).eq(2).should('have.text', '7 '); - cy.get(clientMetrics).eq(1).invoke('text').snapshot(); + cy.get(clientMetrics).eq(1).should('have.text', '0.07 sec'); - cy.get(clientMetrics).eq(0).invoke('text').snapshot(); + cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); }); diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index bcc834fef6a6a3..b09c03f853aa9f 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -26,8 +26,6 @@ import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink' import { ServiceMap } from '../ServiceMap'; import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; -import { RumOverview } from '../RumDashboard'; -import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; function getHomeTabs({ serviceMapEnabled = true, @@ -73,18 +71,6 @@ function getHomeTabs({ }); } - homeTabs.push({ - link: ( - - {i18n.translate('xpack.apm.home.rumTabLabel', { - defaultMessage: 'Real User Monitoring', - })} - - ), - render: () => , - name: 'rum-overview', - }); - return homeTabs; } @@ -93,7 +79,7 @@ const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', { }); interface Props { - tab: 'traces' | 'services' | 'service-map' | 'rum-overview'; + tab: 'traces' | 'services' | 'service-map'; } export function Home({ tab }: Props) { diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 8379def2a7d9aa..057971b1ca3a4b 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -28,6 +28,7 @@ import { EditAgentConfigurationRouteHandler, CreateAgentConfigurationRouteHandler, } from './route_handlers/agent_configuration'; +import { RumHome } from '../../RumDashboard/RumHome'; const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', { defaultMessage: 'Metrics', @@ -253,17 +254,8 @@ export const routes: BreadcrumbRoute[] = [ }, { exact: true, - path: '/rum-overview', - component: () => , - breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', { - defaultMessage: 'Real User Monitoring', - }), - name: RouteName.RUM_OVERVIEW, - }, - { - exact: true, - path: '/services/:serviceName/rum-overview', - component: () => , + path: '/rum-preview', + component: () => , breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', { defaultMessage: 'Real User Monitoring', }), diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx index 007cdab0d2078c..5bf84b6c918c58 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx @@ -88,6 +88,7 @@ export const BreakdownGroup = ({ data-cy={`filter-breakdown-item_${name}`} key={name + count} onClick={onFilterItemClick(name)} + disabled={!selected && getSelItems().length > 0} > {name} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx new file mode 100644 index 00000000000000..1e28fde4aa2b4c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + Chart, + DARK_THEME, + Datum, + LIGHT_THEME, + Partition, + PartitionLayout, + Settings, +} from '@elastic/charts'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { + EUI_CHARTS_THEME_DARK, + EUI_CHARTS_THEME_LIGHT, +} from '@elastic/eui/dist/eui_charts_theme'; +import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ChartWrapper } from '../ChartWrapper'; + +interface Props { + options?: Array<{ + count: number; + name: string; + }>; +} + +export const VisitorBreakdownChart = ({ options }: Props) => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + return ( + + + + d.count as number} + valueGetter="percent" + percentFormatter={(d: number) => + `${Math.round((d + Number.EPSILON) * 100) / 100}%` + } + layers={[ + { + groupByRollup: (d: Datum) => d.name, + nodeLabel: (d: Datum) => d, + // fillLabel: { textInvertible: true }, + shape: { + fillColor: (d) => { + const clrs = [ + euiLightVars.euiColorVis1_behindText, + euiLightVars.euiColorVis0_behindText, + euiLightVars.euiColorVis2_behindText, + euiLightVars.euiColorVis3_behindText, + euiLightVars.euiColorVis4_behindText, + euiLightVars.euiColorVis5_behindText, + euiLightVars.euiColorVis6_behindText, + euiLightVars.euiColorVis7_behindText, + euiLightVars.euiColorVis8_behindText, + euiLightVars.euiColorVis9_behindText, + ]; + return clrs[d.sortIndex]; + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 32, + fontSize: 14, + }, + fontFamily: 'Arial', + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 0.9, // - 0.5 * Math.random(), + emptySizeRatio: 0, + circlePadding: 4, + }} + /> + + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index df72fa604e4b32..5fee2f4195f913 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -34,6 +34,7 @@ export function ClientMetrics() { }, }); } + return Promise.resolve(null); }, [start, end, serviceName, uiFilters] ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 81503e16f7bcfa..adeff2b31fd93c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -56,6 +56,7 @@ export const PageLoadDistribution = () => { }, }); } + return Promise.resolve(null); }, [ end, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 328b873ef85620..c6ef319f8a666a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -39,6 +39,7 @@ export const PageViewsTrend = () => { }, }); } + return Promise.resolve(undefined); }, [end, start, serviceName, uiFilters, breakdowns] ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index 326d4a00fd31f1..2eb79257334d76 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -16,8 +16,9 @@ import { ClientMetrics } from './ClientMetrics'; import { PageViewsTrend } from './PageViewsTrend'; import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; +import { VisitorBreakdown } from './VisitorBreakdown'; -export function RumDashboard() { +export const RumDashboard = () => { return ( @@ -42,7 +43,15 @@ export function RumDashboard() { + + + + + + + + ); -} +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx new file mode 100644 index 00000000000000..b1ff38fdd2d791 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHeader/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { DatePicker } from '../../../shared/DatePicker'; + +export const RumHeader: React.FC = ({ children }) => ( + <> + + {children} + + + + + +); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx new file mode 100644 index 00000000000000..a1b07640b5c17c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { RumOverview } from '../RumDashboard'; +import { RumHeader } from './RumHeader'; + +export function RumHome() { + return ( +
+ + + + +

End User Experience

+
+
+
+
+ +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx new file mode 100644 index 00000000000000..2e17e27587b635 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart'; +import { VisitorBreakdownLabel } from '../translations'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; + +export const VisitorBreakdown = () => { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const { data } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/rum-client/visitor-breakdown', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + return Promise.resolve(null); + }, + [end, start, serviceName, uiFilters] + ); + + return ( + <> + +

{VisitorBreakdownLabel}

+
+ + + + +

Browser

+
+
+ + + +

Operating System

+
+
+ + + +

Device

+
+
+
+ + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 3380a81c7bfab4..9b88202b2e5eff 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer, } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; @@ -20,6 +19,7 @@ import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter import { useUrlParams } from '../../../hooks/useUrlParams'; import { useFetcher } from '../../../hooks/useFetcher'; import { RUM_AGENTS } from '../../../../common/agent_name'; +import { EnvironmentFilter } from '../../shared/EnvironmentFilter'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -38,11 +38,7 @@ export function RumOverview() { urlParams: { start, end }, } = useUrlParams(); - const isRumServiceRoute = useRouteMatch( - '/services/:serviceName/rum-overview' - ); - - const { data } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -65,14 +61,17 @@ export function RumOverview() { + + - {!isRumServiceRoute && ( - <> - - - {' '} - - )} + <> + + + {' '} + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 2784d9bfd8efa8..96d1b529c52f9f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -50,3 +50,10 @@ export const I18LABELS = { defaultMessage: 'seconds', }), }; + +export const VisitorBreakdownLabel = i18n.translate( + 'xpack.apm.rum.visitorBreakdown', + { + defaultMessage: 'Visitor breakdown', + } +); diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index ce60ffa4ba4e36..2f35e329720de5 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -22,17 +22,9 @@ import { ServiceMap } from '../ServiceMap'; import { ServiceMetrics } from '../ServiceMetrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { TransactionOverview } from '../TransactionOverview'; -import { RumOverview } from '../RumDashboard'; -import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; interface Props { - tab: - | 'transactions' - | 'errors' - | 'metrics' - | 'nodes' - | 'service-map' - | 'rum-overview'; + tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; } export function ServiceDetailTabs({ tab }: Props) { @@ -118,20 +110,6 @@ export function ServiceDetailTabs({ tab }: Props) { tabs.push(serviceMapTab); } - if (isRumAgentName(agentName)) { - tabs.push({ - link: ( - - {i18n.translate('xpack.apm.home.rumTabLabel', { - defaultMessage: 'Real User Monitoring', - })} - - ), - render: () => , - name: 'rum-overview', - }); - } - const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab); return ( diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index eab685a4c1ab4f..6ddc4eecba7ede 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -76,7 +76,7 @@ export function KueryBar() { }); // The bar should be disabled when viewing the service map - const disabled = /\/(service-map|rum-overview)$/.test(location.pathname); + const disabled = /\/(service-map)$/.test(location.pathname); const disabledPlaceholder = i18n.translate( 'xpack.apm.kueryBar.disabledPlaceholder', { defaultMessage: 'Search is not available here' } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx deleted file mode 100644 index 729ed9b10f827d..00000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { APMLink, APMLinkExtendProps } from './APMLink'; - -interface RumOverviewLinkProps extends APMLinkExtendProps { - serviceName?: string; -} -export function RumOverviewLink({ - serviceName, - ...rest -}: RumOverviewLinkProps) { - const path = serviceName - ? `/services/${serviceName}/rum-overview` - : '/rum-overview'; - - return ; -} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx index 0bb62bd8efcffd..405a4cacae714b 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -18,9 +18,10 @@ import { fromQuery, toQuery } from '../../Links/url_helpers'; interface Props { serviceNames: string[]; + loading: boolean; } -const ServiceNameFilter = ({ serviceNames }: Props) => { +const ServiceNameFilter = ({ loading, serviceNames }: Props) => { const { urlParams: { serviceName }, } = useUrlParams(); @@ -60,6 +61,7 @@ const ServiceNameFilter = ({ serviceNames }: Props) => { ({ + count: bucket.doc_count, + name: bucket.key as string, + })), + os: os.buckets.map((bucket) => ({ + count: bucket.doc_count, + name: bucket.key as string, + })), + devices: devices.buckets.map((bucket) => ({ + count: bucket.doc_count, + name: bucket.key as string, + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 4e3aa6d4ebe1d2..11911cda79c17a 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -77,6 +77,7 @@ import { rumPageLoadDistributionRoute, rumPageLoadDistBreakdownRoute, rumServicesRoute, + rumVisitorsBreakdownRoute, } from './rum_client'; import { observabilityOverviewHasDataRoute, @@ -174,6 +175,7 @@ const createApmApi = () => { .add(rumPageLoadDistBreakdownRoute) .add(rumClientMetricsRoute) .add(rumServicesRoute) + .add(rumVisitorsBreakdownRoute) // Observability dashboard .add(observabilityOverviewHasDataRoute) diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 01e549632a0bcc..0781512c6f7a09 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -13,6 +13,7 @@ import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown'; import { getRumServices } from '../lib/rum_client/get_rum_services'; +import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -104,3 +105,15 @@ export const rumServicesRoute = createRoute(() => ({ return getRumServices({ setup }); }, })); + +export const rumVisitorsBreakdownRoute = createRoute(() => ({ + path: '/api/apm/rum-client/visitor-breakdown', + params: { + query: t.intersection([uiFiltersRt, rangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + return getVisitorBreakdown({ setup }); + }, +})); From cdbe12ff577292a7c69562c4e2c1d38c9b35308f Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 14 Jul 2020 22:41:58 +0200 Subject: [PATCH 06/26] [Lens] XY chart -long legend overflows chart in editor Feature:Lens (#70702) --- .../_workspace_panel_wrapper.scss | 4 ++ .../workspace_panel_wrapper.tsx | 44 +++++++++---------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss index e663754707e054..90cc049db96eb7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss @@ -36,3 +36,7 @@ } } } + +.lnsWorkspacePanelWrapper__toolbar { + margin-bottom: $euiSizeS; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index f21939b3a28954..f6e15002ca66c9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -66,8 +66,8 @@ export function WorkspacePanelWrapper({ [dispatch] ); return ( - - + <> +
)} - - - - {(!emptyExpression || title) && ( - - - {title || - i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} - - - )} - - {children} - - - - +
+ + {(!emptyExpression || title) && ( + + + {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} + + + )} + + {children} + + + ); } From 820f9ede2dcf649114305988f989ced2805cc7ad Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 14 Jul 2020 13:47:38 -0700 Subject: [PATCH 07/26] [Reporting] Move a few server files for shorter paths (#71591) --- src/dev/precommit_hook/casing_check_config.js | 12 ++++++------ x-pack/plugins/reporting/common/types.ts | 2 +- .../chromium/driver/chromium_driver.ts | 2 +- x-pack/plugins/reporting/server/core.ts | 2 +- .../server/export_types/common/constants.ts | 7 ------- .../decrypt_job_headers.test.ts | 4 ++-- .../{execute_job => }/decrypt_job_headers.ts | 2 +- .../common/get_absolute_url.test.ts | 0 .../export_types}/common/get_absolute_url.ts | 0 .../get_conditional_headers.test.ts | 12 ++++++------ .../get_conditional_headers.ts | 4 ++-- .../{execute_job => }/get_custom_logo.test.ts | 8 ++++---- .../{execute_job => }/get_custom_logo.ts | 8 ++++---- .../{execute_job => }/get_full_urls.test.ts | 6 +++--- .../common/{execute_job => }/get_full_urls.ts | 10 +++++----- .../common/{execute_job => }/index.ts | 1 + .../omit_blacklisted_headers.test.ts | 0 .../omit_blacklisted_headers.ts | 2 +- .../common/validate_urls.test.ts | 0 .../export_types}/common/validate_urls.ts | 0 .../csv/{server => }/create_job.ts | 6 +++--- .../csv/{server => }/execute_job.test.ts | 18 +++++++++--------- .../csv/{server => }/execute_job.ts | 10 +++++----- .../generate_csv/cell_has_formula.ts | 2 +- .../check_cells_for_formulas.test.ts | 0 .../generate_csv/check_cells_for_formulas.ts | 0 .../generate_csv/escape_value.test.ts | 0 .../{server => }/generate_csv/escape_value.ts | 2 +- .../generate_csv/field_format_map.test.ts | 2 +- .../generate_csv/field_format_map.ts | 2 +- .../generate_csv/flatten_hit.test.ts | 0 .../{server => }/generate_csv/flatten_hit.ts | 0 .../generate_csv/format_csv_values.test.ts | 0 .../generate_csv/format_csv_values.ts | 2 +- .../generate_csv/get_ui_settings.ts | 4 ++-- .../generate_csv/hit_iterator.test.ts | 6 +++--- .../{server => }/generate_csv/hit_iterator.ts | 6 +++--- .../csv/{server => }/generate_csv/index.ts | 12 ++++++------ .../max_size_string_builder.test.ts | 0 .../generate_csv/max_size_string_builder.ts | 0 .../server/export_types/csv/index.ts | 4 ++-- .../csv/{server => }/lib/get_request.ts | 4 ++-- .../{server => }/create_job.ts | 8 ++++---- .../{server => }/execute_job.ts | 10 +++++----- .../csv_from_savedobject/index.ts | 8 ++++---- .../{server => }/lib/get_csv_job.test.ts | 2 +- .../{server => }/lib/get_csv_job.ts | 6 +++--- .../{server => }/lib/get_data_source.ts | 4 ++-- .../{server => }/lib/get_fake_request.ts | 6 +++--- .../{server => }/lib/get_filters.test.ts | 4 ++-- .../{server => }/lib/get_filters.ts | 4 ++-- .../png/{server => }/create_job/index.ts | 8 ++++---- .../{server => }/execute_job/index.test.ts | 10 +++++----- .../png/{server => }/execute_job/index.ts | 8 ++++---- .../server/export_types/png/index.ts | 6 +++--- .../png/{server => }/lib/generate_png.ts | 9 ++++----- .../server/export_types/png/types.d.ts | 2 +- .../{server => }/create_job/index.ts | 8 ++++---- .../{server => }/execute_job/index.test.ts | 10 +++++----- .../{server => }/execute_job/index.ts | 8 ++++---- .../export_types/printable_pdf/index.ts | 4 ++-- .../{server => }/lib/generate_pdf.ts | 8 ++++---- .../lib/pdf/assets/fonts/noto/LICENSE_OFL.txt | 0 .../fonts/noto/NotoSansCJKtc-Medium.ttf | Bin .../fonts/noto/NotoSansCJKtc-Regular.ttf | Bin .../lib/pdf/assets/fonts/noto/index.js | 0 .../lib/pdf/assets/fonts/roboto/LICENSE.txt | 0 .../pdf/assets/fonts/roboto/Roboto-Italic.ttf | Bin .../pdf/assets/fonts/roboto/Roboto-Medium.ttf | Bin .../assets/fonts/roboto/Roboto-Regular.ttf | Bin .../lib/pdf/assets/img/logo-grey.png | Bin .../{server => }/lib/pdf/index.js | 0 .../printable_pdf/{server => }/lib/tracker.ts | 0 .../{server => }/lib/uri_encode.js | 2 +- .../export_types/printable_pdf/types.d.ts | 2 +- .../reporting/server/lib/create_queue.ts | 2 +- .../lib/{ => esqueue}/create_tagged_logger.ts | 2 +- x-pack/plugins/reporting/server/lib/index.ts | 6 +++--- .../common => lib}/layouts/create_layout.ts | 2 +- .../common => lib}/layouts/index.ts | 4 ++-- .../common => lib}/layouts/layout.ts | 0 .../layouts/preserve_layout.css | 0 .../common => lib}/layouts/preserve_layout.ts | 0 .../common => lib}/layouts/print.css | 2 +- .../common => lib}/layouts/print_layout.ts | 8 ++++---- .../common => }/lib/screenshots/constants.ts | 2 ++ .../screenshots/get_element_position_data.ts | 8 ++++---- .../lib/screenshots/get_number_of_items.ts | 9 ++++----- .../lib/screenshots/get_screenshots.ts | 6 +++--- .../lib/screenshots/get_time_range.ts | 6 +++--- .../common => }/lib/screenshots/index.ts | 0 .../common => }/lib/screenshots/inject_css.ts | 6 +++--- .../lib/screenshots/observable.test.ts | 12 ++++++------ .../common => }/lib/screenshots/observable.ts | 6 +++--- .../common => }/lib/screenshots/open_url.ts | 6 +++--- .../lib/screenshots/wait_for_render.ts | 8 ++++---- .../screenshots/wait_for_visualizations.ts | 8 ++++---- .../reporting/server/lib/store/store.ts | 2 +- .../reporting/server/lib/validate/index.ts | 2 +- .../validate/validate_max_content_length.ts | 2 +- .../generate_from_savedobject_immediate.ts | 4 ++-- .../plugins/reporting/server/routes/jobs.ts | 2 +- .../routes/lib/authorized_user_pre_routing.ts | 2 +- .../server/{ => routes}/lib/get_user.ts | 2 +- .../server/routes/lib/job_response_handler.ts | 2 +- .../server/{ => routes}/lib/jobs_query.ts | 6 +++--- .../create_mock_browserdriverfactory.ts | 2 +- .../create_mock_layoutinstance.ts | 2 +- x-pack/plugins/reporting/server/types.ts | 2 +- 109 files changed, 213 insertions(+), 219 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/export_types/common/constants.ts rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/decrypt_job_headers.test.ts (93%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/decrypt_job_headers.ts (96%) rename x-pack/plugins/reporting/{ => server/export_types}/common/get_absolute_url.test.ts (100%) rename x-pack/plugins/reporting/{ => server/export_types}/common/get_absolute_url.ts (100%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_conditional_headers.test.ts (93%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_conditional_headers.ts (91%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_custom_logo.test.ts (85%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_custom_logo.ts (82%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_full_urls.test.ts (97%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/get_full_urls.ts (90%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/index.ts (91%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/omit_blacklisted_headers.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/common/{execute_job => }/omit_blacklisted_headers.ts (95%) rename x-pack/plugins/reporting/{ => server/export_types}/common/validate_urls.test.ts (100%) rename x-pack/plugins/reporting/{ => server/export_types}/common/validate_urls.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/create_job.ts (90%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/execute_job.test.ts (98%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/execute_job.ts (92%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/cell_has_formula.ts (85%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/check_cells_for_formulas.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/check_cells_for_formulas.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/escape_value.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/escape_value.ts (95%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/field_format_map.test.ts (97%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/field_format_map.ts (97%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/flatten_hit.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/flatten_hit.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/format_csv_values.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/format_csv_values.ts (97%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/get_ui_settings.ts (94%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/hit_iterator.test.ts (96%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/hit_iterator.ts (95%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/index.ts (93%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/max_size_string_builder.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/generate_csv/max_size_string_builder.ts (100%) rename x-pack/plugins/reporting/server/export_types/csv/{server => }/lib/get_request.ts (93%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/create_job.ts (94%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/execute_job.ts (93%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_csv_job.test.ts (99%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_csv_job.ts (96%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_data_source.ts (95%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_fake_request.ts (90%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_filters.test.ts (98%) rename x-pack/plugins/reporting/server/export_types/csv_from_savedobject/{server => }/lib/get_filters.ts (95%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/create_job/index.ts (85%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/execute_job/index.test.ts (94%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/execute_job/index.ts (93%) rename x-pack/plugins/reporting/server/export_types/png/{server => }/lib/generate_png.ts (89%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/create_job/index.ts (86%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/execute_job/index.test.ts (93%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/execute_job/index.ts (94%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/generate_pdf.ts (96%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/noto/index.js (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/LICENSE.txt (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/assets/img/logo-grey.png (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/pdf/index.js (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/tracker.ts (100%) rename x-pack/plugins/reporting/server/export_types/printable_pdf/{server => }/lib/uri_encode.js (92%) rename x-pack/plugins/reporting/server/lib/{ => esqueue}/create_tagged_logger.ts (95%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/create_layout.ts (94%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/index.ts (94%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/layout.ts (100%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/preserve_layout.css (100%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/preserve_layout.ts (100%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/print.css (96%) rename x-pack/plugins/reporting/server/{export_types/common => lib}/layouts/print_layout.ts (91%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/constants.ts (92%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_element_position_data.ts (93%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_number_of_items.ts (91%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_screenshots.ts (91%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/get_time_range.ts (87%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/index.ts (100%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/inject_css.ts (90%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/observable.test.ts (97%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/observable.ts (97%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/open_url.ts (85%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/wait_for_render.ts (92%) rename x-pack/plugins/reporting/server/{export_types/common => }/lib/screenshots/wait_for_visualizations.ts (90%) rename x-pack/plugins/reporting/server/{ => routes}/lib/get_user.ts (87%) rename x-pack/plugins/reporting/server/{ => routes}/lib/jobs_query.ts (96%) diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index cec80dd547a53c..b8eacdd6a38972 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -173,12 +173,12 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/data.json.gz', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json', 'x-pack/test/functional/es_archives/monitoring/logstash-pipelines/data.json.gz', diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 2819c28cfb54fa..18b0ac2a728026 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { ReportingConfigType } from '../server/config'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { LayoutInstance } from '../server/export_types/common/layouts'; +export { LayoutInstance } from '../server/lib/layouts'; export type JobId = string; export type JobStatus = diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index bca9496bc9addf..eb16a9d6de1a8c 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -9,8 +9,8 @@ import { map, truncate } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; -import { ViewZoomWidthHeight } from '../../../export_types/common/layouts/layout'; import { LevelLogger } from '../../../lib'; +import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ConditionalHeaders, ElementPosition } from '../../../types'; import { allowRequest, NetworkPolicy } from '../../network_policy'; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index eccd6c7db16989..95dc7586ad4a6b 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -20,7 +20,7 @@ import { SecurityPluginSetup } from '../../security/server'; import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; -import { screenshotsObservableFactory } from './export_types/common/lib/screenshots'; +import { screenshotsObservableFactory } from './lib/screenshots'; import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { EnqueueJobFn } from './lib/enqueue_job'; diff --git a/x-pack/plugins/reporting/server/export_types/common/constants.ts b/x-pack/plugins/reporting/server/export_types/common/constants.ts deleted file mode 100644 index 76fab923978f86..00000000000000 --- a/x-pack/plugins/reporting/server/export_types/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const DEFAULT_PAGELOAD_SELECTOR = '.application'; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index 4998d936c9b166..908817a2ccf818 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { decryptJobHeaders } from './decrypt_job_headers'; +import { cryptoFactory, LevelLogger } from '../../lib'; +import { decryptJobHeaders } from './'; const encryptHeaders = async (encryptionKey: string, headers: Record) => { const crypto = cryptoFactory(encryptionKey); diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index 579b5196ad4d96..845b9adb38be98 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory, LevelLogger } from '../../lib'; interface HasEncryptedHeaders { headers?: string; diff --git a/x-pack/plugins/reporting/common/get_absolute_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts similarity index 100% rename from x-pack/plugins/reporting/common/get_absolute_url.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts diff --git a/x-pack/plugins/reporting/common/get_absolute_url.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts similarity index 100% rename from x-pack/plugins/reporting/common/get_absolute_url.ts rename to x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 030ced5dc4b80b..0372d515c21a86 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -5,12 +5,12 @@ */ import sinon from 'sinon'; -import { ReportingConfig } from '../../../'; -import { ReportingCore } from '../../../core'; -import { createMockReportingCore } from '../../../test_helpers'; -import { ScheduledTaskParams } from '../../../types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; -import { getConditionalHeaders, getCustomLogo } from './index'; +import { ReportingConfig } from '../../'; +import { ReportingCore } from '../../core'; +import { createMockReportingCore } from '../../test_helpers'; +import { ScheduledTaskParams } from '../../types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts index 7a50eaac80d859..799d0234868327 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig } from '../../../'; -import { ConditionalHeaders } from '../../../types'; +import { ReportingConfig } from '../../'; +import { ConditionalHeaders } from '../../types'; export const getConditionalHeaders = ({ config, diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index c364752c8dd0f5..a3d65a1398a202 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../../../core'; -import { createMockReportingCore } from '../../../test_helpers'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; -import { getConditionalHeaders, getCustomLogo } from './index'; +import { ReportingCore } from '../../core'; +import { createMockReportingCore } from '../../test_helpers'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { getConditionalHeaders, getCustomLogo } from './'; const mockConfigGet = jest.fn().mockImplementation((key: string) => { return 'localhost'; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts similarity index 82% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts rename to x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index 36c02eb47565c5..547cc45258dae2 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig, ReportingCore } from '../../../'; -import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; -import { ConditionalHeaders } from '../../../types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; // Logo is PDF only +import { ReportingConfig, ReportingCore } from '../../'; +import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; +import { ConditionalHeaders } from '../../types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ reporting, diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index ad952c084d4f39..73d7c7b03c1283 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig } from '../../../'; -import { ScheduledTaskParamsPNG } from '../../png/types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; +import { ReportingConfig } from '../../'; +import { ScheduledTaskParamsPNG } from '../png/types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; interface FullUrlsOpts { diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts rename to x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts index 67bc8d16fa758d..d3362fd1906807 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts @@ -10,11 +10,11 @@ import { UrlWithParsedQuery, UrlWithStringQuery, } from 'url'; -import { ReportingConfig } from '../../..'; -import { getAbsoluteUrlFactory } from '../../../../common/get_absolute_url'; -import { validateUrls } from '../../../../common/validate_urls'; -import { ScheduledTaskParamsPNG } from '../../png/types'; -import { ScheduledTaskParamsPDF } from '../../printable_pdf/types'; +import { ReportingConfig } from '../../'; +import { ScheduledTaskParamsPNG } from '../png/types'; +import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { getAbsoluteUrlFactory } from './get_absolute_url'; +import { validateUrls } from './validate_urls'; function isPngJob( job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts rename to x-pack/plugins/reporting/server/export_types/common/index.ts index b9d59b2be1296e..a4e114d6b2f2ef 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -9,3 +9,4 @@ export { getConditionalHeaders } from './get_conditional_headers'; export { getCustomLogo } from './get_custom_logo'; export { getFullUrls } from './get_full_urls'; export { omitBlacklistedHeaders } from './omit_blacklisted_headers'; +export { validateUrls } from './validate_urls'; diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.test.ts rename to x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts rename to x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts index 305fb6bab54786..e56ffc737764c8 100644 --- a/x-pack/plugins/reporting/server/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts @@ -7,7 +7,7 @@ import { omitBy } from 'lodash'; import { KBN_SCREENSHOT_HEADER_BLACKLIST, KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, -} from '../../../../common/constants'; +} from '../../../common/constants'; export const omitBlacklistedHeaders = ({ job, diff --git a/x-pack/plugins/reporting/common/validate_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/validate_urls.test.ts similarity index 100% rename from x-pack/plugins/reporting/common/validate_urls.test.ts rename to x-pack/plugins/reporting/server/export_types/common/validate_urls.test.ts diff --git a/x-pack/plugins/reporting/common/validate_urls.ts b/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts similarity index 100% rename from x-pack/plugins/reporting/common/validate_urls.ts rename to x-pack/plugins/reporting/server/export_types/common/validate_urls.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts rename to x-pack/plugins/reporting/server/export_types/csv/create_job.ts index fb2d9bfdc58382..5e8ce923a79e0b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cryptoFactory } from '../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; -import { JobParamsDiscoverCsv } from '../types'; +import { cryptoFactory } from '../../lib'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../types'; +import { JobParamsDiscoverCsv } from './types'; export const scheduleTaskFnFactory: ScheduleTaskFnFactory new Promise((resolve) => setTimeout(() => resolve(), ms)); diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts similarity index 92% rename from x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts rename to x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index b38cd8c5af9e72..f0c41a6a49703d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -7,11 +7,11 @@ import { Crypto } from '@elastic/node-crypto'; import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; -import { KibanaRequest } from '../../../../../../../src/core/server'; -import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory, LevelLogger } from '../../../lib'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory } from '../../../types'; -import { ScheduledTaskParamsCSV } from '../types'; +import { KibanaRequest } from '../../../../../../src/core/server'; +import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../common/constants'; +import { cryptoFactory, LevelLogger } from '../../lib'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory } from '../../types'; +import { ScheduledTaskParamsCSV } from './types'; import { createGenerateCsv } from './generate_csv'; const getRequest = async (headers: string | undefined, crypto: Crypto, logger: LevelLogger) => { diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/cell_has_formula.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/cell_has_formula.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts index 659aef85ed5938..1433d852ce6307 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/cell_has_formula.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts @@ -5,7 +5,7 @@ */ import { startsWith } from 'lodash'; -import { CSV_FORMULA_CHARS } from '../../../../../common/constants'; +import { CSV_FORMULA_CHARS } from '../../../../common/constants'; export const cellHasFormulas = (val: string) => CSV_FORMULA_CHARS.some((formulaChar) => startsWith(val, formulaChar)); diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/check_cells_for_formulas.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts index 344091ee18268f..c850d8b2dc7417 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/escape_value.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawValue } from '../../types'; +import { RawValue } from '../types'; import { cellHasFormulas } from './cell_has_formula'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts index 1f0e450da698f1..4cb8de58105847 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { fieldFormats, FieldFormatsGetConfigFn, UI_SETTINGS } from 'src/plugins/data/server'; -import { IndexPatternSavedObject } from '../../types'; +import { IndexPatternSavedObject } from '../types'; import { fieldFormatMapFactory } from './field_format_map'; type ConfigValue = { number: { id: string; params: {} } } | string; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts index 848cf569bc8d75..e01fee530fc65a 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/field_format_map.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { FieldFormat } from 'src/plugins/data/common'; import { FieldFormatConfig, IFieldFormatsRegistry } from 'src/plugins/data/server'; -import { IndexPatternSavedObject } from '../../types'; +import { IndexPatternSavedObject } from '../types'; /** * Create a map of FieldFormat instances for index pattern fields diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/flatten_hit.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/flatten_hit.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts index 387066415a1bca..d0294072112bf7 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/format_csv_values.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts @@ -6,7 +6,7 @@ import { isNull, isObject, isUndefined } from 'lodash'; import { FieldFormat } from 'src/plugins/data/common'; -import { RawValue } from '../../types'; +import { RawValue } from '../types'; export function createFormatCsvValues( escapeValue: (value: RawValue, index: number, array: RawValue[]) => string, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/get_ui_settings.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/get_ui_settings.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts index 8f72c467b0711c..915d5010a4885d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/get_ui_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/server'; -import { ReportingConfig } from '../../../..'; -import { LevelLogger } from '../../../../lib'; +import { ReportingConfig } from '../../../'; +import { LevelLogger } from '../../../lib'; export const getUiSettings = async ( timezone: string | undefined, diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts index 479879e3c8b019..831bf45cf72ea2 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts @@ -6,9 +6,9 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { CancellationToken } from '../../../../../common'; -import { LevelLogger } from '../../../../lib'; -import { ScrollConfig } from '../../../../types'; +import { CancellationToken } from '../../../../common'; +import { LevelLogger } from '../../../lib'; +import { ScrollConfig } from '../../../types'; import { createHitIterator } from './hit_iterator'; const mockLogger = { diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts index b877023064ac67..dee653cf30007b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/hit_iterator.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { SearchParams, SearchResponse } from 'elasticsearch'; -import { CancellationToken } from '../../../../../common'; -import { LevelLogger } from '../../../../lib'; -import { ScrollConfig } from '../../../../types'; +import { CancellationToken } from '../../../../common'; +import { LevelLogger } from '../../../lib'; +import { ScrollConfig } from '../../../types'; export type EndpointCaller = (method: string, params: object) => Promise>; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/index.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 2cb10e291619cb..8da27100ac31cf 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -6,12 +6,12 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'src/core/server'; -import { getFieldFormats } from '../../../../services'; -import { ReportingConfig } from '../../../..'; -import { CancellationToken } from '../../../../../../../plugins/reporting/common'; -import { CSV_BOM_CHARS } from '../../../../../common/constants'; -import { LevelLogger } from '../../../../lib'; -import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../../types'; +import { getFieldFormats } from '../../../services'; +import { ReportingConfig } from '../../../'; +import { CancellationToken } from '../../../../../../plugins/reporting/common'; +import { CSV_BOM_CHARS } from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; +import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; import { createEscapeValue } from './escape_value'; import { fieldFormatMapFactory } from './field_format_map'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.test.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/server/generate_csv/max_size_string_builder.ts rename to x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index b5eacdfc62c8b9..dffc874831dc23 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -15,8 +15,8 @@ import { import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { scheduleTaskFnFactory } from './server/create_job'; -import { runTaskFnFactory } from './server/execute_job'; +import { scheduleTaskFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; import { JobParamsDiscoverCsv, ScheduledTaskParamsCSV } from './types'; export const getExportType = (): ExportTypeDefinition< diff --git a/x-pack/plugins/reporting/server/export_types/csv/server/lib/get_request.ts b/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/csv/server/lib/get_request.ts rename to x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts index 21e49bd62ccc7e..09e6becc2baec0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/server/lib/get_request.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts @@ -7,8 +7,8 @@ import { Crypto } from '@elastic/node-crypto'; import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; -import { LevelLogger } from '../../../../lib'; +import { KibanaRequest } from '../../../../../../../src/core/server'; +import { LevelLogger } from '../../../lib'; export const getRequest = async ( headers: string | undefined, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index 96fb2033f09540..e7fb0c6e2cb995 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -7,9 +7,9 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory } from '../../../lib'; -import { ScheduleTaskFnFactory, TimeRangeParams } from '../../../types'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; +import { cryptoFactory } from '../../lib'; +import { ScheduleTaskFnFactory, TimeRangeParams } from '../../types'; import { JobParamsPanelCsv, SavedObject, @@ -18,7 +18,7 @@ import { SavedSearchObjectAttributesJSON, SearchPanel, VisObjectAttributesJSON, -} from '../types'; +} from './types'; export type ImmediateCreateJobFn = ( jobParams: JobParamsPanelCsv, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index a7992c34a88f11..ffe453f996698d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -5,11 +5,11 @@ */ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { CancellationToken } from '../../../../common'; -import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../../types'; -import { createGenerateCsv } from '../../csv/server/generate_csv'; -import { JobParamsPanelCsv, SearchPanel } from '../types'; +import { CancellationToken } from '../../../common'; +import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; +import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../types'; +import { createGenerateCsv } from '../csv/generate_csv'; +import { JobParamsPanelCsv, SearchPanel } from './types'; import { getFakeRequest } from './lib/get_fake_request'; import { getGenerateCsvParams } from './lib/get_csv_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 9a9f445de0b138..7467f415299fa1 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -15,16 +15,16 @@ import { import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './server/create_job'; -import { ImmediateExecuteFn, runTaskFnFactory } from './server/execute_job'; +import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './create_job'; +import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; import { JobParamsPanelCsv } from './types'; /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ -export { scheduleTaskFnFactory } from './server/create_job'; -export { runTaskFnFactory } from './server/execute_job'; +export { scheduleTaskFnFactory } from './create_job'; +export { runTaskFnFactory } from './execute_job'; export const getExportType = (): ExportTypeDefinition< JobParamsPanelCsv, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts similarity index 99% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts index 3271c6fdae24d9..9646d7eecd5b56 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamsPanelCsv, SearchPanel } from '../../types'; +import { JobParamsPanelCsv, SearchPanel } from '../types'; import { getGenerateCsvParams } from './get_csv_job'; describe('Get CSV Job', () => { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index 5f1954b80e1bc9..0fc29c5b208d9a 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -11,7 +11,7 @@ import { Filter, IIndexPattern, Query, -} from '../../../../../../../../src/plugins/data/server'; +} from '../../../../../../../src/plugins/data/server'; import { DocValueFields, IndexPatternField, @@ -20,10 +20,10 @@ import { SavedSearchObjectAttributes, SearchPanel, SearchSource, -} from '../../types'; +} from '../types'; import { getDataSource } from './get_data_source'; import { getFilters } from './get_filters'; -import { GenerateCsvParams } from '../../../csv/server/generate_csv'; +import { GenerateCsvParams } from '../../csv/generate_csv'; export const getEsQueryConfig = async (config: IUiSettingsClient) => { const configs = await Promise.all([ diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts index bf915696c89743..e3631b9c89724c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_data_source.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternSavedObject } from '../../../csv/types'; -import { SavedObjectReference, SavedSearchObjectAttributesJSON, SearchSource } from '../../types'; +import { IndexPatternSavedObject } from '../../csv/types'; +import { SavedObjectReference, SavedSearchObjectAttributesJSON, SearchSource } from '../types'; export async function getDataSource( savedObjectsClient: any, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_fake_request.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_fake_request.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts index 09c58806de120d..3afbaa650e6c8d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_fake_request.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_fake_request.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { KibanaRequest } from 'kibana/server'; -import { cryptoFactory, LevelLogger } from '../../../../lib'; -import { ScheduledTaskParams } from '../../../../types'; -import { JobParamsPanelCsv } from '../../types'; +import { cryptoFactory, LevelLogger } from '../../../lib'; +import { ScheduledTaskParams } from '../../../types'; +import { JobParamsPanelCsv } from '../types'; export const getFakeRequest = async ( job: ScheduledTaskParams, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts similarity index 98% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts index b5d564d93d0d6a..429b2c518cf146 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeRangeParams } from '../../../../types'; -import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../../types'; +import { TimeRangeParams } from '../../../types'; +import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; import { getFilters } from './get_filters'; interface Args { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts similarity index 95% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts rename to x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts index 1258b03d3051b1..a1b04cca0419d4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/lib/get_filters.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts @@ -6,8 +6,8 @@ import { badRequest } from 'boom'; import moment from 'moment-timezone'; -import { TimeRangeParams } from '../../../../types'; -import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../../types'; +import { TimeRangeParams } from '../../../types'; +import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; export function getFilters( indexPatternId: string, diff --git a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts rename to x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index f459b8f249c706..b63f2a09041b3a 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateUrls } from '../../../../../common/validate_urls'; -import { cryptoFactory } from '../../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../../types'; -import { JobParamsPNG } from '../../types'; +import { cryptoFactory } from '../../../lib'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { validateUrls } from '../../common'; +import { JobParamsPNG } from '../types'; export const scheduleTaskFnFactory: ScheduleTaskFnFactory>; diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index b708448b0f8b24..25b4dbd60535b4 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -12,10 +12,10 @@ import { LICENSE_TYPE_TRIAL, PNG_JOB_TYPE as jobType, } from '../../../common/constants'; -import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../..//types'; +import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { scheduleTaskFnFactory } from './server/create_job'; -import { runTaskFnFactory } from './server/execute_job'; +import { scheduleTaskFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; import { JobParamsPNG, ScheduledTaskParamsPNG } from './types'; export const getExportType = (): ExportTypeDefinition< diff --git a/x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts similarity index 89% rename from x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts rename to x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index d7e9d0f812b378..5969b5b8abc002 100644 --- a/x-pack/plugins/reporting/server/export_types/png/server/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -7,11 +7,10 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; -import { ReportingCore } from '../../../../'; -import { LevelLogger } from '../../../../lib'; -import { ConditionalHeaders, ScreenshotResults } from '../../../../types'; -import { LayoutParams } from '../../../common/layouts'; -import { PreserveLayout } from '../../../common/layouts/preserve_layout'; +import { ReportingCore } from '../../../'; +import { LevelLogger } from '../../../lib'; +import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; +import { ConditionalHeaders, ScreenshotResults } from '../../../types'; export async function generatePngObservableFactory(reporting: ReportingCore) { const getScreenshots = await reporting.getScreenshotsObservable(); diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 7a25f4ed8fe735..4c40f55f0f0d69 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -5,7 +5,7 @@ */ import { ScheduledTaskParams } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../common/layouts'; +import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data export interface JobParamsPNG { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts similarity index 86% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 76c57182497202..aa88ef863d32b6 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateUrls } from '../../../../../common/validate_urls'; -import { cryptoFactory } from '../../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../../types'; -import { JobParamsPDF } from '../../types'; +import { validateUrls } from '../../common'; +import { cryptoFactory } from '../../../lib'; +import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { JobParamsPDF } from '../types'; export const scheduleTaskFnFactory: ScheduleTaskFnFactory ({ generatePdfObservableFactory: jest.fn() })); import * as Rx from 'rxjs'; -import { ReportingCore } from '../../../../'; -import { CancellationToken } from '../../../../../common'; -import { cryptoFactory, LevelLogger } from '../../../../lib'; -import { createMockReportingCore } from '../../../../test_helpers'; -import { ScheduledTaskParamsPDF } from '../../types'; +import { ReportingCore } from '../../../'; +import { CancellationToken } from '../../../../common'; +import { cryptoFactory, LevelLogger } from '../../../lib'; +import { createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { ScheduledTaskParamsPDF } from '../types'; import { runTaskFnFactory } from './'; let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 7f8f2f4f6906ae..eb15c0a71ca3f2 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -7,17 +7,17 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { PDF_JOB_TYPE } from '../../../../../common/constants'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../../types'; +import { PDF_JOB_TYPE } from '../../../../common/constants'; +import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, getCustomLogo, getFullUrls, omitBlacklistedHeaders, -} from '../../../common/execute_job'; -import { ScheduledTaskParamsPDF } from '../../types'; +} from '../../common'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { ScheduledTaskParamsPDF } from '../types'; type QueuedPdfExecutorFactory = RunTaskFnFactory>; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index 073bd38b538fbe..e5115c243c6972 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -14,8 +14,8 @@ import { } from '../../../common/constants'; import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { metadata } from './metadata'; -import { scheduleTaskFnFactory } from './server/create_job'; -import { runTaskFnFactory } from './server/execute_job'; +import { scheduleTaskFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; import { JobParamsPDF, ScheduledTaskParamsPDF } from './types'; export const getExportType = (): ExportTypeDefinition< diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 366949a033757b..f2ce423566c46a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -7,10 +7,10 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -import { ReportingCore } from '../../../../'; -import { LevelLogger } from '../../../../lib'; -import { ConditionalHeaders, ScreenshotResults } from '../../../../types'; -import { createLayout, LayoutInstance, LayoutParams } from '../../../common/layouts'; +import { ReportingCore } from '../../../'; +import { LevelLogger } from '../../../lib'; +import { createLayout, LayoutInstance, LayoutParams } from '../../../lib/layouts'; +import { ConditionalHeaders, ScreenshotResults } from '../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; import { getTracker } from './tracker'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/index.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/img/logo-grey.png rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/index.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/tracker.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/uri_encode.js similarity index 92% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/uri_encode.js index d057cfba4ef304..657af71c42c83e 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/uri_encode.js +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/uri_encode.js @@ -5,7 +5,7 @@ */ import { forEach, isArray } from 'lodash'; -import { url } from '../../../../../../../../src/plugins/kibana_utils/server'; +import { url } from '../../../../../../../src/plugins/kibana_utils/server'; function toKeyValue(obj) { const parts = []; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 5399781a77753e..cba0f41f075367 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -5,7 +5,7 @@ */ import { ScheduledTaskParams } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../common/layouts'; +import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data, after being parsed from RISON export interface JobParamsPDF { diff --git a/x-pack/plugins/reporting/server/lib/create_queue.ts b/x-pack/plugins/reporting/server/lib/create_queue.ts index a8dcb92c55b2de..2da3d8bd47ccb3 100644 --- a/x-pack/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/plugins/reporting/server/lib/create_queue.ts @@ -6,10 +6,10 @@ import { ReportingCore } from '../core'; import { JobSource, TaskRunResult } from '../types'; -import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; +import { createTaggedLogger } from './esqueue/create_tagged_logger'; import { LevelLogger } from './level_logger'; import { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/create_tagged_logger.ts b/x-pack/plugins/reporting/server/lib/esqueue/create_tagged_logger.ts similarity index 95% rename from x-pack/plugins/reporting/server/lib/create_tagged_logger.ts rename to x-pack/plugins/reporting/server/lib/esqueue/create_tagged_logger.ts index 775930ec83bdf0..2b97f3f25217a5 100644 --- a/x-pack/plugins/reporting/server/lib/create_tagged_logger.ts +++ b/x-pack/plugins/reporting/server/lib/esqueue/create_tagged_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LevelLogger } from './level_logger'; +import { LevelLogger } from '../level_logger'; export function createTaggedLogger(logger: LevelLogger, tags: string[]) { return (msg: string, additionalTags = []) => { diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index f5a50fca28b7a6..e4adb1188e3fc4 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LevelLogger } from './level_logger'; export { checkLicense } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; export { getExportTypesRegistry } from './export_types_registry'; -export { runValidations } from './validate'; -export { startTrace } from './trace'; +export { LevelLogger } from './level_logger'; export { ReportingStore } from './store'; +export { startTrace } from './trace'; +export { runValidations } from './validate'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index 216a59d41cec01..921d302387edf6 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CaptureConfig } from '../../../types'; +import { CaptureConfig } from '../../types'; import { LayoutParams, LayoutTypes } from './'; import { Layout } from './layout'; import { PreserveLayout } from './preserve_layout'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts similarity index 94% rename from x-pack/plugins/reporting/server/export_types/common/layouts/index.ts rename to x-pack/plugins/reporting/server/lib/layouts/index.ts index 23e4c095afe611..d46f088475222f 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../../browsers'; -import { LevelLogger } from '../../../lib'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { LevelLogger } from '../'; import { Layout } from './layout'; export { createLayout } from './create_layout'; diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/layout.ts b/x-pack/plugins/reporting/server/lib/layouts/layout.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/layouts/layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/layout.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.css b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.css rename to x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/layouts/preserve_layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/print.css b/x-pack/plugins/reporting/server/lib/layouts/print.css similarity index 96% rename from x-pack/plugins/reporting/server/export_types/common/layouts/print.css rename to x-pack/plugins/reporting/server/lib/layouts/print.css index b5b6eae5e1ff60..4f1e3f4e5abd02 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/print.css +++ b/x-pack/plugins/reporting/server/lib/layouts/print.css @@ -110,7 +110,7 @@ discover-app .discover-table-footer { /** * 1. Reporting manually makes each visualization it wants to screenshot larger, so we need to hide * the visualizations in the other panels. We can only use properties that will be manually set in - * reporting/export_types/printable_pdf/server/lib/screenshot.js or this will also hide the visualization + * reporting/export_types/printable_pdf/lib/screenshot.js or this will also hide the visualization * we want to capture. * 2. React grid item's transform affects the visualizations, even when they are using fixed positioning. Chrome seems * to handle this fine, but firefox moves the visualizations around. diff --git a/x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts rename to x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 30c83771aa3c93..b055fae8a780dc 100644 --- a/x-pack/plugins/reporting/server/export_types/common/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -6,10 +6,10 @@ import path from 'path'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; -import { CaptureConfig } from '../../../types'; -import { HeadlessChromiumDriver } from '../../../browsers'; -import { LevelLogger } from '../../../lib'; -import { getDefaultLayoutSelectors, LayoutSelectorDictionary, Size, LayoutTypes } from './'; +import { LevelLogger } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig } from '../../types'; +import { getDefaultLayoutSelectors, LayoutSelectorDictionary, LayoutTypes, Size } from './'; import { Layout } from './layout'; export class PrintLayout extends Layout { diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts b/x-pack/plugins/reporting/server/lib/screenshots/constants.ts similarity index 92% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts rename to x-pack/plugins/reporting/server/lib/screenshots/constants.ts index a3faf9337524ee..854763e4991359 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/constants.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/constants.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const DEFAULT_PAGELOAD_SELECTOR = '.application'; + export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems'; export const CONTEXT_INJECTCSS = 'InjectCss'; export const CONTEXT_WAITFORRENDER = 'WaitForRender'; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts similarity index 93% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts index 140d76f8d1cd6c..4fb9fd96ecfe6d 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { AttributesMap, ElementsPositionAndAttribute } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { AttributesMap, ElementsPositionAndAttribute } from '../../types'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; export const getElementPositionAndAttributes = async ( diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts index 42eb91ecba830b..49c690e8c024d9 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig } from '../../types'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; export const getNumberOfItems = async ( @@ -68,7 +68,6 @@ export const getNumberOfItems = async ( }, }) ); - itemsCount = 1; } endTrace(); diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index 05c315b8341a33..bc7b7005674a73 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { ElementsPositionAndAttribute, Screenshot } from '../../../../types'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { ElementsPositionAndAttribute, Screenshot } from '../../types'; +import { LevelLogger, startTrace } from '../'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts similarity index 87% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts rename to x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts index ba68a5fec4e4cc..afd63644548352 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/get_time_range.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { LayoutInstance } from '../../layouts'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../../../common/types'; +import { HeadlessChromiumDriver } from '../../browsers'; import { CONTEXT_GETTIMERANGE } from './constants'; export const getTimeRange = async ( diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/index.ts rename to x-pack/plugins/reporting/server/lib/screenshots/index.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts rename to x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts index d72afacc1bef3c..f893951815e9ef 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { Layout } from '../../layouts/layout'; +import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_INJECTCSS } from './constants'; const fsp = { readFile: promisify(fs.readFile) }; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts rename to x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index b00233137943de..1b72be6c92f439 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../browsers/chromium/puppeteer', () => ({ +jest.mock('../../browsers/chromium/puppeteer', () => ({ puppeteerLaunch: () => ({ // Fixme needs event emitters newPage: () => ({ @@ -17,11 +17,11 @@ jest.mock('../../../../browsers/chromium/puppeteer', () => ({ import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger } from '../../../../lib'; -import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; -import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../../../types'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { LevelLogger } from '../'; +import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; +import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../types'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts rename to x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 028bff4aaa5eee..ab4dabf9ed2c21 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -16,15 +16,15 @@ import { tap, toArray, } from 'rxjs/operators'; -import { HeadlessChromiumDriverFactory } from '../../../../browsers'; +import { HeadlessChromiumDriverFactory } from '../../browsers'; import { CaptureConfig, ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults, ScreenshotsObservableFn, -} from '../../../../types'; -import { DEFAULT_PAGELOAD_SELECTOR } from '../../constants'; +} from '../../types'; +import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getScreenshots } from './get_screenshots'; diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts rename to x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index bd7e8c508c118f..c21ef3b91fab3f 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig, ConditionalHeaders } from '../../../../types'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { LevelLogger, startTrace } from '../'; export const openUrl = async ( captureConfig: CaptureConfig, diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts similarity index 92% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts rename to x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts index b6519e914430a5..f36a7b6f73664a 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { CaptureConfig } from '../../types'; +import { LevelLogger, startTrace } from '../'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORRENDER } from './constants'; export const waitForRenderComplete = async ( diff --git a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts similarity index 90% rename from x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts rename to x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index 75a7b6516473cb..779d00442522d0 100644 --- a/x-pack/plugins/reporting/server/export_types/common/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../../../browsers'; -import { LevelLogger, startTrace } from '../../../../lib'; -import { CaptureConfig } from '../../../../types'; -import { LayoutInstance } from '../../layouts'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { LevelLogger, startTrace } from '../'; +import { CaptureConfig } from '../../types'; +import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; type SelectorArgs = Record; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 1cb964a7bbfac2..0f1ed83b717671 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -7,8 +7,8 @@ import { ElasticsearchServiceSetup } from 'src/core/server'; import { LevelLogger } from '../'; import { ReportingCore } from '../../'; -import { LayoutInstance } from '../../export_types/common/layouts'; import { indexTimestamp } from './index_timestamp'; +import { LayoutInstance } from '../layouts'; import { mapping } from './mapping'; import { Report } from './report'; diff --git a/x-pack/plugins/reporting/server/lib/validate/index.ts b/x-pack/plugins/reporting/server/lib/validate/index.ts index 7c439d6023d5fa..d20df6b7315be1 100644 --- a/x-pack/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/plugins/reporting/server/lib/validate/index.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { ReportingConfig } from '../../'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; -import { LevelLogger } from '../../lib'; +import { LevelLogger } from '../'; import { validateBrowser } from './validate_browser'; import { validateMaxContentLength } from './validate_max_content_length'; diff --git a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts index 6d34937d9bd751..c38c6e52978545 100644 --- a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -8,7 +8,7 @@ import numeral from '@elastic/numeral'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { defaults, get } from 'lodash'; import { ReportingConfig } from '../../'; -import { LevelLogger } from '../../lib'; +import { LevelLogger } from '../'; const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 773295deea954a..8250ca462049b7 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,8 +7,8 @@ import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; -import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/server/create_job'; -import { runTaskFnFactory } from '../export_types/csv_from_savedobject/server/execute_job'; +import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/create_job'; +import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../types'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 90185f0736ed81..4033719b053ba4 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -8,8 +8,8 @@ import { schema } from '@kbn/config-schema'; import Boom from 'boom'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { jobsQueryFactory } from '../lib/jobs_query'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { jobsQueryFactory } from './lib/jobs_query'; import { deleteJobResponseHandlerFactory, downloadJobResponseHandlerFactory, diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 74737b0a5d1e27..3758eafc6d718c 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -6,8 +6,8 @@ import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../security/server'; -import { getUserFactory } from '../../lib/get_user'; import { ReportingCore } from '../../core'; +import { getUserFactory } from './get_user'; type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; diff --git a/x-pack/plugins/reporting/server/lib/get_user.ts b/x-pack/plugins/reporting/server/routes/lib/get_user.ts similarity index 87% rename from x-pack/plugins/reporting/server/lib/get_user.ts rename to x-pack/plugins/reporting/server/routes/lib/get_user.ts index 49d15a7c551003..fd56e8cfc28c77 100644 --- a/x-pack/plugins/reporting/server/lib/get_user.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_user.ts @@ -5,7 +5,7 @@ */ import { KibanaRequest } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; export function getUserFactory(security?: SecurityPluginSetup) { return (request: KibanaRequest) => { diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 651f1c34fee6c5..df346c8b9b832e 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -8,8 +8,8 @@ import { kibanaResponseFactory } from 'kibana/server'; import { ReportingCore } from '../../'; import { AuthenticatedUser } from '../../../../security/server'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; -import { jobsQueryFactory } from '../../lib/jobs_query'; import { getDocumentPayloadFactory } from './get_document_payload'; +import { jobsQueryFactory } from './jobs_query'; interface JobResponseHandlerParams { docId: string; diff --git a/x-pack/plugins/reporting/server/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts similarity index 96% rename from x-pack/plugins/reporting/server/lib/jobs_query.ts rename to x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index f4670847260ee6..f3955b4871b314 100644 --- a/x-pack/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { get } from 'lodash'; -import { ReportingCore } from '../'; -import { AuthenticatedUser } from '../../../security/server'; -import { JobSource } from '../types'; +import { ReportingCore } from '../../'; +import { AuthenticatedUser } from '../../../../security/server'; +import { JobSource } from '../../types'; const esErrors = elasticsearchErrors as Record; const defaultSize = 10; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 97e22e2ca28636..db10d96db22636 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -7,8 +7,8 @@ import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; -import * as contexts from '../export_types/common/lib/screenshots/constants'; import { LevelLogger } from '../lib'; +import * as contexts from '../lib/screenshots/constants'; import { CaptureConfig, ElementsPositionAndAttribute } from '../types'; interface CreateMockBrowserDriverFactoryOpts { diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index 22da9eb418e9ac..c9dbbda9fd68de 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout, LayoutInstance, LayoutTypes } from '../export_types/common/layouts'; +import { createLayout, LayoutInstance, LayoutTypes } from '../lib/layouts'; import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 667c1546c61473..ff597b53ea0b0b 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -15,8 +15,8 @@ import { SecurityPluginSetup } from '../../security/server'; import { JobStatus } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; -import { LayoutInstance } from './export_types/common/layouts'; import { LevelLogger } from './lib'; +import { LayoutInstance } from './lib/layouts'; /* * Routing / API types From c16bffc2038661dfbb8f4fc68b72dfc6c27ec89a Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 14 Jul 2020 16:49:00 -0400 Subject: [PATCH 08/26] [Ingest Manager] Copy change enroll new agent -> Add Agent (#71691) --- .../sections/agent_config/components/actions_menu.tsx | 2 +- .../ingest_manager/sections/fleet/agent_list_page/index.tsx | 2 +- .../ingest_manager/sections/fleet/components/list_layout.tsx | 2 +- .../applications/ingest_manager/sections/overview/index.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx index 86d191d4ff9048..a71de4b60c08cf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx @@ -85,7 +85,7 @@ export const AgentConfigActionMenu = memo<{ > , = () => { setIsEnrollmentFlyoutOpen(true)}> ) : null diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 60cbc31081302f..46190033d4d6bb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -112,7 +112,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { setIsEnrollmentFlyoutOpen(true)}>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index f4b68f0c5107ee..ea7ae093ee59ad 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -71,7 +71,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {

@@ -84,7 +84,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { setIsEnrollmentFlyoutOpen(true)}>
From 3f95b7a1f99cb929029105c9103472ab89b20ef9 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 14 Jul 2020 17:00:35 -0400 Subject: [PATCH 09/26] adjust query to include agents without endpoint as unenrolled (#71715) --- .../server/endpoint/routes/metadata/support/unenroll.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts index bba9d921310daf..136f314aa415f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -18,7 +18,8 @@ export async function findAllUnenrolledAgentIds( page: pageNum, perPage: pageSize, showInactive: true, - kuery: 'fleet-agents.packages:endpoint AND fleet-agents.active:false', + kuery: + '(fleet-agents.packages : "endpoint" AND fleet-agents.active : false) OR (NOT fleet-agents.packages : "endpoint" AND fleet-agents.active : true)', }; }; From e4546b3bf5414726e1c87823cacdcb4ec8d91ae4 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 14 Jul 2020 14:04:14 -0700 Subject: [PATCH 10/26] [tests] Temporarily skipped to promote snapshot Will be re-enabled in https://github.com/elastic/kibana/pull/71727 Signed-off-by: Tyler Smalley --- x-pack/test/api_integration/apis/fleet/setup.ts | 4 +++- .../security_solution_endpoint/apps/endpoint/policy_list.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/fleet/setup.ts b/x-pack/test/api_integration/apis/fleet/setup.ts index 4fcf39886e202a..317dec734568cf 100644 --- a/x-pack/test/api_integration/apis/fleet/setup.ts +++ b/x-pack/test/api_integration/apis/fleet/setup.ts @@ -11,7 +11,9 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - describe('fleet_setup', () => { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('fleet_setup', () => { beforeEach(async () => { try { await es.security.deleteUser({ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 57321ab4cd911f..5b4a5cca108f94 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -19,7 +19,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const policyTestResources = getService('policyTestResources'); const RELATIVE_DATE_FORMAT = /\d (?:seconds|minutes) ago/i; - describe('When on the Endpoint Policy List', function () { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('When on the Endpoint Policy List', function () { this.tags(['ciGroup7']); before(async () => { await pageObjects.policy.navigateToPolicyList(); From 919e0f6263978aaec7269fb3ae8e400c300d5327 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 14 Jul 2020 17:09:03 -0400 Subject: [PATCH 11/26] [Index Management] Adopt data stream API changes (#71682) --- x-pack/plugins/index_management/common/types/templates.ts | 4 ++-- .../components/template_form/template_form_schemas.tsx | 6 +++--- .../apis/management/index_management/data_streams.ts | 7 +++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index 32e254e490b2aa..eda00ec8191592 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -22,7 +22,7 @@ export interface TemplateSerialized { version?: number; priority?: number; _meta?: { [key: string]: any }; - data_stream?: { timestamp_field: string }; + data_stream?: {}; } /** @@ -46,7 +46,7 @@ export interface TemplateDeserialized { name: string; }; _meta?: { [key: string]: any }; // Composable template only - dataStream?: { timestamp_field: string }; // Composable template only + dataStream?: {}; // Composable template only _kbnMeta: { type: TemplateType; hasDatastream: boolean; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index d8c3ad8c259fcf..0d9ce57a64c843 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -136,9 +136,9 @@ export const schemas: Record = { defaultValue: false, serializer: (value) => { if (value === true) { - return { - timestamp_field: '@timestamp', - }; + // For now, ES expects an empty object when defining a data stream + // https://github.com/elastic/elasticsearch/pull/59317 + return {}; } }, deserializer: (value) => { diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index 0fe5dab1af52d0..9f5c2a3de07bf7 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -35,9 +35,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, }, - data_stream: { - timestamp_field: '@timestamp', - }, + data_stream: {}, }, }); @@ -53,7 +51,8 @@ export default function ({ getService }: FtrProviderContext) { await deleteComposableIndexTemplate(name); }; - describe('Data streams', function () { + // Temporarily skipping tests until ES snapshot is updated + describe.skip('Data streams', function () { describe('Get', () => { const testDataStreamName = 'test-data-stream'; From 04cdb5ad6fc2ef2483dcd4c82315d8470ae0e8b0 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 14 Jul 2020 17:13:30 -0400 Subject: [PATCH 12/26] Use updated onPreAuth from Platform (#71552) * Use updated onPreAuth from Platform * Add config flag. Increase default value. * Set max connections flag default to 0 (disabled) * Don't use limiting logic on checkin route * Confirm preAuth handler only added when max > 0 Co-authored-by: Elastic Machine --- .../ingest_manager/common/constants/routes.ts | 2 + .../ingest_manager/common/types/index.ts | 1 + .../ingest_manager/server/constants/index.ts | 1 + x-pack/plugins/ingest_manager/server/index.ts | 1 + .../plugins/ingest_manager/server/plugin.ts | 4 ++ .../server/routes/agent/index.ts | 6 +- .../ingest_manager/server/routes/index.ts | 1 + .../server/routes/limited_concurrency.test.ts | 35 +++++++++ .../server/routes/limited_concurrency.ts | 72 +++++++++++++++++++ 9 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 7c3b5a198571c9..94265c3920922c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -11,6 +11,8 @@ export const PACKAGE_CONFIG_API_ROOT = `${API_ROOT}/package_configs`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; +export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; + // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 0fce5cfa6226ff..d7edc04a357996 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -13,6 +13,7 @@ export interface IngestManagerConfigType { enabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; + maxConcurrentConnections: number; kibana: { host?: string; ca_sha256?: string; diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index d3c074ff2e8d0e..ce81736f2e84f1 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -15,6 +15,7 @@ export { AGENT_UPDATE_ACTIONS_INTERVAL_MS, INDEX_PATTERN_PLACEHOLDER_SUFFIX, // Routes + LIMITED_CONCURRENCY_ROUTE_TAG, PLUGIN_ID, EPM_API_ROUTES, DATA_STREAM_API_ROUTES, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 16c0b6449d1e86..6c72218abc5311 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -26,6 +26,7 @@ export const config = { enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), pollingRequestTimeout: schema.number({ defaultValue: 60000 }), + maxConcurrentConnections: schema.number({ defaultValue: 0 }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e32533dc907b90..69af475886bb92 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -34,6 +34,7 @@ import { } from './constants'; import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { + registerLimitedConcurrencyRoutes, registerEPMRoutes, registerPackageConfigRoutes, registerDataStreamRoutes, @@ -228,6 +229,9 @@ export class IngestManagerPlugin ); } } else { + // we currently only use this global interceptor if fleet is enabled + // since it would run this func on *every* req (other plugins, CSS, etc) + registerLimitedConcurrencyRoutes(core, config); registerAgentRoutes(router); registerEnrollmentApiKeyRoutes(router); registerInstallScriptRoutes({ diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index d7eec50eac3cfb..b85d96186f2338 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -10,7 +10,7 @@ */ import { IRouter } from 'src/core/server'; -import { PLUGIN_ID, AGENT_API_ROUTES } from '../../constants'; +import { PLUGIN_ID, AGENT_API_ROUTES, LIMITED_CONCURRENCY_ROUTE_TAG } from '../../constants'; import { GetAgentsRequestSchema, GetOneAgentRequestSchema, @@ -95,7 +95,7 @@ export const registerRoutes = (router: IRouter) => { { path: AGENT_API_ROUTES.ENROLL_PATTERN, validate: PostAgentEnrollRequestSchema, - options: { tags: [] }, + options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentEnrollHandler ); @@ -105,7 +105,7 @@ export const registerRoutes = (router: IRouter) => { { path: AGENT_API_ROUTES.ACKS_PATTERN, validate: PostAgentAcksRequestSchema, - options: { tags: [] }, + options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentAcksHandlerBuilder({ acknowledgeAgentActions: AgentService.acknowledgeAgentActions, diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index f6b4439d8bef15..87be3a80cea963 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -14,3 +14,4 @@ export { registerRoutes as registerInstallScriptRoutes } from './install_script' export { registerRoutes as registerOutputRoutes } from './output'; export { registerRoutes as registerSettingsRoutes } from './settings'; export { registerRoutes as registerAppRoutes } from './app'; +export { registerLimitedConcurrencyRoutes } from './limited_concurrency'; diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts new file mode 100644 index 00000000000000..a0bb8e9b86fbbf --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from 'src/core/server/mocks'; +import { registerLimitedConcurrencyRoutes } from './limited_concurrency'; +import { IngestManagerConfigType } from '../index'; + +describe('registerLimitedConcurrencyRoutes', () => { + test(`doesn't call registerOnPreAuth if maxConcurrentConnections is 0`, async () => { + const mockSetup = coreMock.createSetup(); + const mockConfig = { fleet: { maxConcurrentConnections: 0 } } as IngestManagerConfigType; + registerLimitedConcurrencyRoutes(mockSetup, mockConfig); + + expect(mockSetup.http.registerOnPreAuth).not.toHaveBeenCalled(); + }); + + test(`calls registerOnPreAuth once if maxConcurrentConnections is 1`, async () => { + const mockSetup = coreMock.createSetup(); + const mockConfig = { fleet: { maxConcurrentConnections: 1 } } as IngestManagerConfigType; + registerLimitedConcurrencyRoutes(mockSetup, mockConfig); + + expect(mockSetup.http.registerOnPreAuth).toHaveBeenCalledTimes(1); + }); + + test(`calls registerOnPreAuth once if maxConcurrentConnections is 1000`, async () => { + const mockSetup = coreMock.createSetup(); + const mockConfig = { fleet: { maxConcurrentConnections: 1000 } } as IngestManagerConfigType; + registerLimitedConcurrencyRoutes(mockSetup, mockConfig); + + expect(mockSetup.http.registerOnPreAuth).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts new file mode 100644 index 00000000000000..ec8e2f6c8d4361 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/limited_concurrency.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CoreSetup, + KibanaRequest, + LifecycleResponseFactory, + OnPreAuthToolkit, +} from 'kibana/server'; +import { LIMITED_CONCURRENCY_ROUTE_TAG } from '../../common'; +import { IngestManagerConfigType } from '../index'; +class MaxCounter { + constructor(private readonly max: number = 1) {} + private counter = 0; + valueOf() { + return this.counter; + } + increase() { + if (this.counter < this.max) { + this.counter += 1; + } + } + decrease() { + if (this.counter > 0) { + this.counter -= 1; + } + } + lessThanMax() { + return this.counter < this.max; + } +} + +function shouldHandleRequest(request: KibanaRequest) { + const tags = request.route.options.tags; + return tags.includes(LIMITED_CONCURRENCY_ROUTE_TAG); +} + +export function registerLimitedConcurrencyRoutes(core: CoreSetup, config: IngestManagerConfigType) { + const max = config.fleet.maxConcurrentConnections; + if (!max) return; + + const counter = new MaxCounter(max); + core.http.registerOnPreAuth(function preAuthHandler( + request: KibanaRequest, + response: LifecycleResponseFactory, + toolkit: OnPreAuthToolkit + ) { + if (!shouldHandleRequest(request)) { + return toolkit.next(); + } + + if (!counter.lessThanMax()) { + return response.customError({ + body: 'Too Many Requests', + statusCode: 429, + }); + } + + counter.increase(); + + // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes + // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 + request.events.aborted$.toPromise().then(() => { + counter.decrease(); + }); + + return toolkit.next(); + }); +} From f5259ed373e755b2c3431eb1263ec0c1acae025d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 14 Jul 2020 15:18:17 -0600 Subject: [PATCH 13/26] [Security solution] [Hosts] Endpoint overview on host details page (#71466) --- .../public/graphql/introspection.json | 79 ++++- .../security_solution/public/graphql/types.ts | 34 ++- .../hosts/overview/host_overview.gql_query.ts | 5 + .../endpoint_overview/index.test.tsx | 48 +++ .../host_overview/endpoint_overview/index.tsx | 90 ++++++ .../endpoint_overview/translations.ts | 28 ++ .../components/host_overview/index.test.tsx | 1 - .../components/host_overview/index.tsx | 275 ++++++++++-------- .../server/endpoint/routes/metadata/index.ts | 2 +- .../server/graphql/hosts/schema.gql.ts | 17 +- .../security_solution/server/graphql/types.ts | 78 ++++- .../server/lib/compose/kibana.ts | 6 +- .../lib/hosts/elasticsearch_adapter.test.ts | 25 +- .../server/lib/hosts/elasticsearch_adapter.ts | 57 +++- .../server/lib/hosts/mock.ts | 66 +++++ .../security_solution/server/plugin.ts | 13 +- .../apis/security_solution/hosts.ts | 1 + 17 files changed, 669 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 43c478ff120a08..4716440c36e61a 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -6525,26 +6525,26 @@ "deprecationReason": null }, { - "name": "lastSeen", + "name": "cloud", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "host", + "name": "endpoint", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, + "type": { "kind": "OBJECT", "name": "EndpointFields", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "cloud", + "name": "host", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, + "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -6555,6 +6555,14 @@ "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "lastSeen", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -6659,6 +6667,65 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "EndpointFields", + "description": "", + "fields": [ + { + "name": "endpointPolicy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sensorVersion", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "policyStatus", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "HostPolicyResponseActionStatus", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "HostPolicyResponseActionStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "success", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failure", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "warning", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "FirstLastSeenHost", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 084d1a63fec75f..98addf3317ff4e 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -301,6 +301,12 @@ export enum HostsFields { lastSeen = 'lastSeen', } +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + export enum UsersFields { name = 'name', count = 'count', @@ -1442,13 +1448,15 @@ export interface HostsEdges { export interface HostItem { _id?: Maybe; - lastSeen?: Maybe; + cloud?: Maybe; - host?: Maybe; + endpoint?: Maybe; - cloud?: Maybe; + host?: Maybe; inspect?: Maybe; + + lastSeen?: Maybe; } export interface CloudFields { @@ -1469,6 +1477,14 @@ export interface CloudMachine { type?: Maybe<(Maybe)[]>; } +export interface EndpointFields { + endpointPolicy?: Maybe; + + sensorVersion?: Maybe; + + policyStatus?: Maybe; +} + export interface FirstLastSeenHost { inspect?: Maybe; @@ -3044,6 +3060,8 @@ export namespace GetHostOverviewQuery { cloud: Maybe; inspect: Maybe; + + endpoint: Maybe; }; export type Host = { @@ -3107,6 +3125,16 @@ export namespace GetHostOverviewQuery { response: string[]; }; + + export type Endpoint = { + __typename?: 'EndpointFields'; + + endpointPolicy: Maybe; + + policyStatus: Maybe; + + sensorVersion: Maybe; + }; } export namespace GetKpiHostDetailsQuery { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts index 46794816dbf2ab..89937d0adf81e8 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/host_overview.gql_query.ts @@ -46,6 +46,11 @@ export const HostOverviewQuery = gql` dsl response } + endpoint { + endpointPolicy + policyStatus + sensorVersion + } } } } diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx new file mode 100644 index 00000000000000..8e221445a95d3d --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { TestProviders } from '../../../../common/mock'; + +import { EndpointOverview } from './index'; +import { HostPolicyResponseActionStatus } from '../../../../graphql/types'; + +describe('EndpointOverview Component', () => { + test('it renders with endpoint data', () => { + const endpointData = { + endpointPolicy: 'demo', + policyStatus: HostPolicyResponseActionStatus.success, + sensorVersion: '7.9.0-SNAPSHOT', + }; + const wrapper = mount( + + + + ); + + const findData = wrapper.find( + 'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description' + ); + expect(findData.at(0).text()).toEqual(endpointData.endpointPolicy); + expect(findData.at(1).text()).toEqual(endpointData.policyStatus); + expect(findData.at(2).text()).toContain(endpointData.sensorVersion); // contain because drag adds a space + }); + test('it renders with null data', () => { + const wrapper = mount( + + + + ); + + const findData = wrapper.find( + 'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description' + ); + expect(findData.at(0).text()).toEqual('—'); + expect(findData.at(1).text()).toEqual('—'); + expect(findData.at(2).text()).toContain('—'); // contain because drag adds a space + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx new file mode 100644 index 00000000000000..df06c2eb368375 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem, EuiHealth } from '@elastic/eui'; +import { getOr } from 'lodash/fp'; +import React, { useCallback, useMemo } from 'react'; + +import { DescriptionList } from '../../../../../common/utility_types'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; +import { EndpointFields, HostPolicyResponseActionStatus } from '../../../../graphql/types'; +import { DescriptionListStyled } from '../../../../common/components/page'; + +import * as i18n from './translations'; + +interface Props { + data: EndpointFields | null; +} + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( + + + +); + +export const EndpointOverview = React.memo(({ data }) => { + const getDefaultRenderer = useCallback( + (fieldName: string, fieldData: EndpointFields, attrName: string) => ( + + ), + [] + ); + const descriptionLists: Readonly = useMemo( + () => [ + [ + { + title: i18n.ENDPOINT_POLICY, + description: + data != null && data.endpointPolicy != null ? data.endpointPolicy : getEmptyTagValue(), + }, + ], + [ + { + title: i18n.POLICY_STATUS, + description: + data != null && data.policyStatus != null ? ( + + {data.policyStatus} + + ) : ( + getEmptyTagValue() + ), + }, + ], + [ + { + title: i18n.SENSORVERSION, + description: + data != null && data.sensorVersion != null + ? getDefaultRenderer('sensorVersion', data, 'agent.version') + : getEmptyTagValue(), + }, + ], + [], // needs 4 columns for design + ], + [data, getDefaultRenderer] + ); + + return ( + <> + {descriptionLists.map((descriptionList, index) => getDescriptionList(descriptionList, index))} + + ); +}); + +EndpointOverview.displayName = 'EndpointOverview'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts new file mode 100644 index 00000000000000..34e3347b5ff9a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ENDPOINT_POLICY = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.endpointPolicy', + { + defaultMessage: 'Endpoint policy', + } +); + +export const POLICY_STATUS = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.policyStatus', + { + defaultMessage: 'Policy status', + } +); + +export const SENSORVERSION = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.sensorversion', + { + defaultMessage: 'Sensorversion', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 56c232158ac02c..0286961fd78afe 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -11,7 +11,6 @@ import { TestProviders } from '../../../common/mock'; import { HostOverview } from './index'; import { mockData } from './mock'; import { mockAnomalies } from '../../../common/components/ml/mock'; - describe('Host Summary Component', () => { describe('rendering', () => { test('it renders the default Host Summary', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index c1004f772a0eee..0c679cc94f787a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { DescriptionList } from '../../../../common/utility_types'; @@ -33,6 +33,7 @@ import { } from '../../../hosts/components/first_last_seen_host'; import * as i18n from './translations'; +import { EndpointOverview } from './endpoint_overview'; interface HostSummaryProps { data: HostItem; @@ -53,143 +54,183 @@ const getDescriptionList = (descriptionList: DescriptionList[], key: number) => export const HostOverview = React.memo( ({ + anomaliesData, data, - loading, - id, - startDate, endDate, + id, isLoadingAnomaliesData, - anomaliesData, + loading, narrowDateRange, + startDate, }) => { const capabilities = useMlCapabilities(); const userPermissions = hasMlUserPermissions(capabilities); const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); - const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( - + const getDefaultRenderer = useCallback( + (fieldName: string, fieldData: HostItem) => ( + + ), + [] ); - const column: DescriptionList[] = [ - { - title: i18n.HOST_ID, - description: data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), - }, - { - title: i18n.FIRST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - title: i18n.LAST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - - ) : ( - getEmptyTagValue() - ), - }, - ]; - const firstColumn = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - - ), - }, - ] - : column; - - const descriptionLists: Readonly = [ - firstColumn, - [ + const column: DescriptionList[] = useMemo( + () => [ { - title: i18n.IP_ADDRESSES, - description: ( - (ip != null ? : getEmptyTagValue())} - /> - ), + title: i18n.HOST_ID, + description: data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), }, { - title: i18n.MAC_ADDRESSES, - description: getDefaultRenderer('host.mac', data), - }, - { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, - ], - [ - { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, - { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, - { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, - { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, - ], - [ - { - title: i18n.CLOUD_PROVIDER, - description: getDefaultRenderer('cloud.provider', data), - }, - { - title: i18n.REGION, - description: getDefaultRenderer('cloud.region', data), - }, - { - title: i18n.INSTANCE_ID, - description: getDefaultRenderer('cloud.instance.id', data), + title: i18n.FIRST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + + ) : ( + getEmptyTagValue() + ), }, { - title: i18n.MACHINE_TYPE, - description: getDefaultRenderer('cloud.machine.type', data), + title: i18n.LAST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + + ) : ( + getEmptyTagValue() + ), }, ], - ]; + [data] + ); + const firstColumn = useMemo( + () => + userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + + ), + }, + ] + : column, + [ + anomaliesData, + column, + endDate, + isLoadingAnomaliesData, + narrowDateRange, + startDate, + userPermissions, + ] + ); + const descriptionLists: Readonly = useMemo( + () => [ + firstColumn, + [ + { + title: i18n.IP_ADDRESSES, + description: ( + (ip != null ? : getEmptyTagValue())} + /> + ), + }, + { + title: i18n.MAC_ADDRESSES, + description: getDefaultRenderer('host.mac', data), + }, + { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, + ], + [ + { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, + { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, + { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, + { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, + ], + [ + { + title: i18n.CLOUD_PROVIDER, + description: getDefaultRenderer('cloud.provider', data), + }, + { + title: i18n.REGION, + description: getDefaultRenderer('cloud.region', data), + }, + { + title: i18n.INSTANCE_ID, + description: getDefaultRenderer('cloud.instance.id', data), + }, + { + title: i18n.MACHINE_TYPE, + description: getDefaultRenderer('cloud.machine.type', data), + }, + ], + ], + [data, firstColumn, getDefaultRenderer] + ); return ( - - - + <> + + + + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} + {loading && ( + + )} + + + {data.endpoint != null ? ( + <> + + + - {loading && ( - - )} - - + {loading && ( + + )} + + + ) : null} + ); } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 7915f1a8cbf509..cb9889ca0cb764 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -7,8 +7,8 @@ import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; - import Boom from 'boom'; + import { metadataIndexPattern } from '../../../../common/endpoint/constants'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index d813a08cad6db2..02f8341cd6fd9b 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts @@ -41,12 +41,25 @@ export const hostsSchema = gql` region: [String] } + enum HostPolicyResponseActionStatus { + success + failure + warning + } + + type EndpointFields { + endpointPolicy: String + sensorVersion: String + policyStatus: HostPolicyResponseActionStatus + } + type HostItem { _id: String - lastSeen: Date - host: HostEcsFields cloud: CloudFields + endpoint: EndpointFields + host: HostEcsFields inspect: Inspect + lastSeen: Date } type HostsEdges { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 668266cc67c3a9..1eaf47ad43812a 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -303,6 +303,12 @@ export enum HostsFields { lastSeen = 'lastSeen', } +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + export enum UsersFields { name = 'name', count = 'count', @@ -1444,13 +1450,15 @@ export interface HostsEdges { export interface HostItem { _id?: Maybe; - lastSeen?: Maybe; + cloud?: Maybe; - host?: Maybe; + endpoint?: Maybe; - cloud?: Maybe; + host?: Maybe; inspect?: Maybe; + + lastSeen?: Maybe; } export interface CloudFields { @@ -1471,6 +1479,14 @@ export interface CloudMachine { type?: Maybe<(Maybe)[]>; } +export interface EndpointFields { + endpointPolicy?: Maybe; + + sensorVersion?: Maybe; + + policyStatus?: Maybe; +} + export interface FirstLastSeenHost { inspect?: Maybe; @@ -6325,13 +6341,15 @@ export namespace HostItemResolvers { export interface Resolvers { _id?: _IdResolver, TypeParent, TContext>; - lastSeen?: LastSeenResolver, TypeParent, TContext>; + cloud?: CloudResolver, TypeParent, TContext>; - host?: HostResolver, TypeParent, TContext>; + endpoint?: EndpointResolver, TypeParent, TContext>; - cloud?: CloudResolver, TypeParent, TContext>; + host?: HostResolver, TypeParent, TContext>; inspect?: InspectResolver, TypeParent, TContext>; + + lastSeen?: LastSeenResolver, TypeParent, TContext>; } export type _IdResolver, Parent = HostItem, TContext = SiemContext> = Resolver< @@ -6339,18 +6357,18 @@ export namespace HostItemResolvers { Parent, TContext >; - export type LastSeenResolver< - R = Maybe, + export type CloudResolver< + R = Maybe, Parent = HostItem, TContext = SiemContext > = Resolver; - export type HostResolver< - R = Maybe, + export type EndpointResolver< + R = Maybe, Parent = HostItem, TContext = SiemContext > = Resolver; - export type CloudResolver< - R = Maybe, + export type HostResolver< + R = Maybe, Parent = HostItem, TContext = SiemContext > = Resolver; @@ -6359,6 +6377,11 @@ export namespace HostItemResolvers { Parent = HostItem, TContext = SiemContext > = Resolver; + export type LastSeenResolver< + R = Maybe, + Parent = HostItem, + TContext = SiemContext + > = Resolver; } export namespace CloudFieldsResolvers { @@ -6418,6 +6441,36 @@ export namespace CloudMachineResolvers { > = Resolver; } +export namespace EndpointFieldsResolvers { + export interface Resolvers { + endpointPolicy?: EndpointPolicyResolver, TypeParent, TContext>; + + sensorVersion?: SensorVersionResolver, TypeParent, TContext>; + + policyStatus?: PolicyStatusResolver< + Maybe, + TypeParent, + TContext + >; + } + + export type EndpointPolicyResolver< + R = Maybe, + Parent = EndpointFields, + TContext = SiemContext + > = Resolver; + export type SensorVersionResolver< + R = Maybe, + Parent = EndpointFields, + TContext = SiemContext + > = Resolver; + export type PolicyStatusResolver< + R = Maybe, + Parent = EndpointFields, + TContext = SiemContext + > = Resolver; +} + export namespace FirstLastSeenHostResolvers { export interface Resolvers { inspect?: InspectResolver, TypeParent, TContext>; @@ -9331,6 +9384,7 @@ export type IResolvers = { CloudFields?: CloudFieldsResolvers.Resolvers; CloudInstance?: CloudInstanceResolvers.Resolvers; CloudMachine?: CloudMachineResolvers.Resolvers; + EndpointFields?: EndpointFieldsResolvers.Resolvers; FirstLastSeenHost?: FirstLastSeenHostResolvers.Resolvers; IpOverviewData?: IpOverviewDataResolvers.Resolvers; Overview?: OverviewResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index 8bc90bed251683..db76f6d52dbb00 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -32,11 +32,13 @@ import * as note from '../note/saved_object'; import * as pinnedEvent from '../pinned_event/saved_object'; import * as timeline from '../timeline/saved_object'; import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram'; +import { EndpointAppContext } from '../../endpoint/types'; export function compose( core: CoreSetup, plugins: SetupPlugins, - isProductionMode: boolean + isProductionMode: boolean, + endpointContext: EndpointAppContext ): AppBackendLibs { const framework = new KibanaBackendFrameworkAdapter(core, plugins, isProductionMode); const sources = new Sources(new ConfigurationSourcesAdapter()); @@ -46,7 +48,7 @@ export function compose( authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), - hosts: new Hosts(new ElasticsearchHostsAdapter(framework)), + hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts index 20510e1089f96d..766fbd5dca0317 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.test.ts @@ -9,6 +9,7 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { ElasticsearchHostsAdapter, formatHostEdgesData } from './elasticsearch_adapter'; import { + mockEndpointMetadata, mockGetHostOverviewOptions, mockGetHostOverviewRequest, mockGetHostOverviewResponse, @@ -26,6 +27,10 @@ import { mockGetHostsQueryDsl, } from './mock'; import { HostAggEsItem } from './types'; +import { EndpointAppContext } from '../../endpoint/types'; +import { mockLogger } from '../detection_engine/signals/__mocks__/es_results'; +import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; +import { createMockEndpointAppContextServiceStartContract } from '../../endpoint/mocks'; jest.mock('./query.hosts.dsl', () => { return { @@ -44,6 +49,11 @@ jest.mock('./query.last_first_seen_host.dsl', () => { buildLastFirstSeenHostQuery: jest.fn(() => mockGetHostLastFirstSeenDsl), }; }); +jest.mock('../../endpoint/routes/metadata', () => { + return { + getHostData: jest.fn(() => mockEndpointMetadata), + }; +}); describe('hosts elasticsearch_adapter', () => { describe('#formatHostsData', () => { @@ -155,6 +165,15 @@ describe('hosts elasticsearch_adapter', () => { }); }); + const endpointAppContextService = new EndpointAppContextService(); + const startContract = createMockEndpointAppContextServiceStartContract(); + endpointAppContextService.start(startContract); + + const endpointContext: EndpointAppContext = { + logFactory: mockLogger, + service: endpointAppContextService, + config: jest.fn(), + }; describe('#getHosts', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostsResponse); @@ -166,7 +185,7 @@ describe('hosts elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework); + const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); const data: HostsData = await EsHosts.getHosts( mockGetHostsRequest as FrameworkRequest, mockGetHostsOptions @@ -186,7 +205,7 @@ describe('hosts elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework); + const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); const data: HostItem = await EsHosts.getHostOverview( mockGetHostOverviewRequest as FrameworkRequest, mockGetHostOverviewOptions @@ -206,7 +225,7 @@ describe('hosts elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest })); test('Happy Path', async () => { - const EsHosts = new ElasticsearchHostsAdapter(mockFramework); + const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext); const data: FirstLastSeenHost = await EsHosts.getHostFirstLastSeen( mockGetHostLastFirstSeenRequest as FrameworkRequest, mockGetHostLastFirstSeenOptions diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts index 90ac44ab3cb465..796338e189d601 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts @@ -6,12 +6,17 @@ import { get, getOr, has, head, set } from 'lodash/fp'; -import { FirstLastSeenHost, HostItem, HostsData, HostsEdges } from '../../graphql/types'; +import { + FirstLastSeenHost, + HostItem, + HostsData, + HostsEdges, + EndpointFields, +} from '../../graphql/types'; import { inspectStringifyObject } from '../../utils/build_query'; import { hostFieldsMap } from '../ecs_fields'; import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; - import { buildHostOverviewQuery } from './query.detail_host.dsl'; import { buildHostsQuery } from './query.hosts.dsl'; import { buildLastFirstSeenHostQuery } from './query.last_first_seen_host.dsl'; @@ -27,9 +32,14 @@ import { HostValue, } from './types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { EndpointAppContext } from '../../endpoint/types'; +import { getHostData } from '../../endpoint/routes/metadata'; export class ElasticsearchHostsAdapter implements HostsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} + constructor( + private readonly framework: FrameworkAdapter, + private readonly endpointContext: EndpointAppContext + ) {} public async getHosts( request: FrameworkRequest, @@ -83,8 +93,47 @@ export class ElasticsearchHostsAdapter implements HostsAdapter { dsl: [inspectStringifyObject(dsl)], response: [inspectStringifyObject(response)], }; + const formattedHostItem = formatHostItem(options.fields, aggregations); + const hostId = + formattedHostItem.host && formattedHostItem.host.id + ? Array.isArray(formattedHostItem.host.id) + ? formattedHostItem.host.id[0] + : formattedHostItem.host.id + : null; + const endpoint: EndpointFields | null = await this.getHostEndpoint(request, hostId); + return { inspect, _id: options.hostName, ...formattedHostItem, endpoint }; + } - return { inspect, _id: options.hostName, ...formatHostItem(options.fields, aggregations) }; + public async getHostEndpoint( + request: FrameworkRequest, + hostId: string | null + ): Promise { + const logger = this.endpointContext.logFactory.get('metadata'); + try { + const agentService = this.endpointContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } + const metadataRequestContext = { + agentService, + logger, + requestHandlerContext: request.context, + }; + const endpointData = + hostId != null && metadataRequestContext.agentService != null + ? await getHostData(metadataRequestContext, hostId) + : null; + return endpointData != null && endpointData.metadata + ? { + endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, + policyStatus: endpointData.metadata.Endpoint.policy.applied.status, + sensorVersion: endpointData.metadata.agent.version, + } + : null; + } catch (err) { + logger.warn(JSON.stringify(err, null, 2)); + return null; + } } public async getHostFirstLastSeen( diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts index 30082990b55f96..0f6bc5c1b0e0c8 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts @@ -497,6 +497,11 @@ export const mockGetHostOverviewResult = { provider: ['gce'], region: ['us-east-1'], }, + endpoint: { + endpointPolicy: 'demo', + policyStatus: 'success', + sensorVersion: '7.9.0-SNAPSHOT', + }, }; export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = { @@ -564,3 +569,64 @@ export const mockGetHostLastFirstSeenResult = { firstSeen: '2019-02-22T03:41:32.826Z', lastSeen: '2019-04-09T16:18:12.178Z', }; + +export const mockEndpointMetadata = { + metadata: { + '@timestamp': '2020-07-13T01:08:37.68896700Z', + Endpoint: { + policy: { + applied: { id: '3de86380-aa5a-11ea-b969-0bee1b260ab8', name: 'demo', status: 'success' }, + }, + status: 'enrolled', + }, + agent: { + build: { + original: + 'version: 7.9.0-SNAPSHOT, compiled: Thu Jul 09 07:56:12 2020, branch: 7.x, commit: 713a1071de475f15b3a1f0944d3602ed532597a5', + }, + id: 'c29e0de1-7476-480b-b242-38f0394bf6a1', + type: 'endpoint', + version: '7.9.0-SNAPSHOT', + }, + dataset: { name: 'endpoint.metadata', namespace: 'default', type: 'metrics' }, + ecs: { version: '1.5.0' }, + elastic: { agent: { id: '' } }, + event: { + action: 'endpoint_metadata', + category: ['host'], + created: '2020-07-13T01:08:37.68896700Z', + dataset: 'endpoint.metadata', + id: 'Lkio+AHbZGSPFb7q++++++2E', + kind: 'metric', + module: 'endpoint', + sequence: 146, + type: ['info'], + }, + host: { + architecture: 'x86_64', + hostname: 'DESKTOP-4I1B23J', + id: 'a4148b63-1758-ab1f-a6d3-f95075cb1a9c', + ip: [ + '172.16.166.129', + 'fe80::c07e:eee9:3e8d:ea6d', + '169.254.205.96', + 'fe80::1027:b13d:a4a7:cd60', + '127.0.0.1', + '::1', + ], + mac: ['00:0c:29:89:ff:73', '3c:22:fb:3c:93:4c'], + name: 'DESKTOP-4I1B23J', + os: { + Ext: { variant: 'Windows 10 Pro' }, + family: 'windows', + full: 'Windows 10 Pro 2004 (10.0.19041.329)', + kernel: '2004 (10.0.19041.329)', + name: 'Windows', + platform: 'windows', + version: '2004 (10.0.19041.329)', + }, + }, + message: 'Endpoint metadata', + }, + host_status: 'error', +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index b56c45a9205b60..17192057d2ad38 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -48,6 +48,7 @@ import { EndpointAppContextService } from './endpoint/endpoint_app_context_servi import { EndpointAppContext } from './endpoint/types'; import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts'; import { initUsageCollectors } from './usage'; +import { AppRequestContext } from './types'; export interface SetupPlugins { alerts: AlertingSetup; @@ -127,9 +128,12 @@ export class Plugin implements IPlugin ({ - getAppClient: () => this.appClientFactory.create(request), - })); + core.http.registerRouteHandlerContext( + APP_ID, + (context, request, response): AppRequestContext => ({ + getAppClient: () => this.appClientFactory.create(request), + }) + ); this.appClientFactory.setup({ getSpaceId: plugins.spaces?.spacesService?.getSpaceId, @@ -144,7 +148,6 @@ export class Plugin implements IPlugin { const expectedHost: Omit = { _id: 'zeek-sensor-san-francisco', + endpoint: null, host: { architecture: ['x86_64'], id: [CURSOR_ID], From 0c87aa506d401b966961bb3152d78fb0e1580f0e Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 14 Jul 2020 16:18:32 -0500 Subject: [PATCH 14/26] [DOCS] Adds API keys to API docs (#71738) * [DOCS] Adds API keys to API docs * Fixes link title * Update docs/api/using-api.asciidoc Co-authored-by: Brandon Morelli Co-authored-by: Brandon Morelli --- docs/api/using-api.asciidoc | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index 188c8f9a5909db..c61edfb62b0795 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -10,7 +10,23 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte [float] [[api-authentication]] === Authentication -{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, which is where the username and password are stored in order to be passed as part of the call. +The {kib} APIs support key- and token-based authentication. + +[float] +[[token-api-authentication]] +==== Token-based authentication + +To use token-based authentication, you use the same username and password that you use to log into Elastic. +In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, +which is where the username and password are stored in order to be passed as part of the call. + +[float] +[[key-authentication]] +==== Key-based authentication + +To use key-based authentication, you create an API key using the Elastic Console, then specify the key in the header of your API calls. + +For information about API keys, refer to <>. [float] [[api-calls]] @@ -51,7 +67,8 @@ For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsr * XSRF protections are disabled using the `server.xsrf.disableProtection` setting `Content-Type: application/json`:: - Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. + Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. + Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. Request header example: From 34c54ed31b70e4b6ffaf9cec003e3878ad68583f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Jul 2020 15:19:51 -0600 Subject: [PATCH 15/26] [Maps] fix custom icon palettes UI not being displayed (#71482) * [Maps] fix custom icon palettes UI not being displayed * cleanup test * remove uneeded change to vector style defaults * fix jest tests * review feedback * fix jest tests --- .../style_property_descriptor_types.ts | 2 +- .../create_layer_descriptor.test.ts | 9 +- .../security/create_layer_descriptors.test.ts | 9 +- .../tiled_vector_layer.test.tsx | 8 +- .../sources/ems_tms_source/ems_tms_source.js | 5 +- .../vector/components/style_map_select.js | 100 ------------- .../icon_map_select.test.tsx.snap | 124 ++++++++++++++++ .../components/symbol/dynamic_icon_form.js | 5 - .../components/symbol/icon_map_select.js | 59 -------- .../symbol/icon_map_select.test.tsx | 78 ++++++++++ .../components/symbol/icon_map_select.tsx | 136 ++++++++++++++++++ .../vector/components/symbol/icon_select.js | 16 +-- .../components/symbol/icon_select.test.js | 31 ++-- .../vector/components/symbol/icon_stops.js | 38 ++--- .../components/symbol/icon_stops.test.js | 34 ++++- .../components/symbol/static_icon_form.js | 15 +- .../symbol/vector_style_icon_editor.js | 14 +- .../properties/dynamic_style_property.d.ts | 1 + .../classes/styles/vector/symbol_utils.js | 4 +- .../vector/vector_style_defaults.test.ts | 9 +- .../styles/vector/vector_style_defaults.ts | 5 +- .../plugins/maps/public/kibana_services.d.ts | 1 + x-pack/plugins/maps/public/kibana_services.js | 3 + 23 files changed, 428 insertions(+), 278 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap delete mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index 4846054ca26cbd..ce6539c9c45205 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -95,7 +95,7 @@ export type ColorStylePropertyDescriptor = | ColorDynamicStylePropertyDescriptor; export type IconDynamicOptions = { - iconPaletteId?: string; + iconPaletteId: string | null; customIconStops?: IconStop[]; useCustomIconMap?: boolean; field?: StylePropertyField; diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts index 075d19dccdb686..e6349fbe9ab9dc 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts @@ -5,14 +5,9 @@ */ jest.mock('../../../../kibana_services', () => { - const mockUiSettings = { - get: () => { - return undefined; - }, - }; return { - getUiSettings: () => { - return mockUiSettings; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts index 49a86f45a681b8..d02f07923c6820 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.test.ts @@ -5,14 +5,9 @@ */ jest.mock('../../../../kibana_services', () => { - const mockUiSettings = { - get: () => { - return undefined; - }, - }; return { - getUiSettings: () => { - return mockUiSettings; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx index ecd625db344119..faae26cac08e71 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -8,12 +8,8 @@ import sinon from 'sinon'; jest.mock('../../../kibana_services', () => { return { - getUiSettings() { - return { - get() { - return false; - }, - }; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js index 83c87eb53d4fe7..b364dd32860f36 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js @@ -12,7 +12,7 @@ import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { SOURCE_TYPES } from '../../../../common/constants'; -import { getEmsTileLayerId, getUiSettings } from '../../../kibana_services'; +import { getEmsTileLayerId, getIsDarkMode } from '../../../kibana_services'; import { registerSource } from '../source_registry'; export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { @@ -122,9 +122,8 @@ export class EMSTMSSource extends AbstractTMSSource { return this._descriptor.id; } - const isDarkMode = getUiSettings().get('theme:darkMode', false); const emsTileLayerId = getEmsTileLayerId(); - return isDarkMode ? emsTileLayerId.dark : emsTileLayerId.bright; + return getIsDarkMode() ? emsTileLayerId.dark : emsTileLayerId.bright; } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js deleted file mode 100644 index e4dc9d1b4d8f6a..00000000000000 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; - -import { EuiSuperSelect, EuiSpacer } from '@elastic/eui'; - -const CUSTOM_MAP = 'CUSTOM_MAP'; - -export class StyleMapSelect extends Component { - state = {}; - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.customMapStops === prevState.prevPropsCustomMapStops) { - return null; - } - - return { - prevPropsCustomMapStops: nextProps.customMapStops, // reset tracker to latest value - customMapStops: nextProps.customMapStops, // reset customMapStops to latest value - }; - } - - _onMapSelect = (selectedValue) => { - const useCustomMap = selectedValue === CUSTOM_MAP; - this.props.onChange({ - selectedMapId: useCustomMap ? null : selectedValue, - useCustomMap, - }); - }; - - _onCustomMapChange = ({ customMapStops, isInvalid }) => { - // Manage invalid custom map in local state - if (isInvalid) { - this.setState({ customMapStops }); - return; - } - - this.props.onChange({ - useCustomMap: true, - customMapStops, - }); - }; - - _renderCustomStopsInput() { - return !this.props.isCustomOnly && !this.props.useCustomMap - ? null - : this.props.renderCustomStopsInput(this._onCustomMapChange); - } - - _renderMapSelect() { - if (this.props.isCustomOnly) { - return null; - } - - const mapOptionsWithCustom = [ - { - value: CUSTOM_MAP, - inputDisplay: this.props.customOptionLabel, - }, - ...this.props.options, - ]; - - let valueOfSelected; - if (this.props.useCustomMap) { - valueOfSelected = CUSTOM_MAP; - } else { - valueOfSelected = this.props.options.find( - (option) => option.value === this.props.selectedMapId - ) - ? this.props.selectedMapId - : ''; - } - - return ( - - - - - ); - } - - render() { - return ( - - {this._renderMapSelect()} - {this._renderCustomStopsInput()} - - ); - } -} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap new file mode 100644 index 00000000000000..b0b85268aa1c88 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_map_select.test.tsx.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should not render icon map select when isCustomOnly 1`] = ` + + + +`; + +exports[`Should render custom stops input when useCustomIconMap 1`] = ` + + + mock filledShapes option + , + "value": "filledShapes", + }, + Object { + "inputDisplay":
+ mock hollowShapes option +
, + "value": "hollowShapes", + }, + ] + } + valueOfSelected="CUSTOM_MAP_ID" + /> + + +
+`; + +exports[`Should render default props 1`] = ` + + + mock filledShapes option + , + "value": "filledShapes", + }, + Object { + "inputDisplay":
+ mock hollowShapes option +
, + "value": "hollowShapes", + }, + ] + } + valueOfSelected="filledShapes" + /> + +
+`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js index e3724d42a783b7..0601922077b4ad 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js @@ -12,11 +12,9 @@ import { IconMapSelect } from './icon_map_select'; export function DynamicIconForm({ fields, - isDarkMode, onDynamicStyleChange, staticDynamicSelect, styleProperty, - symbolOptions, }) { const styleOptions = styleProperty.getOptions(); @@ -44,11 +42,8 @@ export function DynamicIconForm({ return ( ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js deleted file mode 100644 index 6cfe656d65a1e3..00000000000000 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { StyleMapSelect } from '../style_map_select'; -import { i18n } from '@kbn/i18n'; -import { IconStops } from './icon_stops'; -import { getIconPaletteOptions } from '../../symbol_utils'; - -export function IconMapSelect({ - customIconStops, - iconPaletteId, - isDarkMode, - onChange, - styleProperty, - symbolOptions, - useCustomIconMap, - isCustomOnly, -}) { - function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) { - onChange({ - customIconStops: customMapStops, - iconPaletteId: selectedMapId, - useCustomIconMap: useCustomMap, - }); - } - - function renderCustomIconStopsInput(onCustomMapChange) { - return ( - - ); - } - - return ( - - ); -} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx new file mode 100644 index 00000000000000..4e68baf0bd7b7f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable max-classes-per-file */ + +jest.mock('./icon_stops', () => ({ + IconStops: () => { + return
mockIconStops
; + }, +})); + +jest.mock('../../symbol_utils', () => { + return { + getIconPaletteOptions: () => { + return [ + { value: 'filledShapes', inputDisplay:
mock filledShapes option
}, + { value: 'hollowShapes', inputDisplay:
mock hollowShapes option
}, + ]; + }, + PREFERRED_ICONS: ['circle'], + }; +}); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { FIELD_ORIGIN } from '../../../../../../common/constants'; +import { AbstractField } from '../../../../fields/field'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; +import { IconMapSelect } from './icon_map_select'; + +class MockField extends AbstractField {} + +class MockDynamicStyleProperty { + getField() { + return new MockField({ fieldName: 'myField', origin: FIELD_ORIGIN.SOURCE }); + } + + getValueSuggestions() { + return []; + } +} + +const defaultProps = { + iconPaletteId: 'filledShapes', + onChange: () => {}, + styleProperty: (new MockDynamicStyleProperty() as unknown) as IDynamicStyleProperty, + isCustomOnly: false, +}; + +test('Should render default props', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('Should render custom stops input when useCustomIconMap', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should not render icon map select when isCustomOnly', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx new file mode 100644 index 00000000000000..1dd55bbb47f78a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { EuiSuperSelect, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +// @ts-expect-error +import { IconStops } from './icon_stops'; +// @ts-expect-error +import { getIconPaletteOptions, PREFERRED_ICONS } from '../../symbol_utils'; +import { IconStop } from '../../../../../../common/descriptor_types'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; + +const CUSTOM_MAP_ID = 'CUSTOM_MAP_ID'; + +const DEFAULT_ICON_STOPS = [ + { stop: null, icon: PREFERRED_ICONS[0] }, // first stop is the "other" category + { stop: '', icon: PREFERRED_ICONS[1] }, +]; + +interface StyleOptionChanges { + customIconStops?: IconStop[]; + iconPaletteId?: string | null; + useCustomIconMap: boolean; +} + +interface Props { + customIconStops?: IconStop[]; + iconPaletteId: string | null; + onChange: ({ customIconStops, iconPaletteId, useCustomIconMap }: StyleOptionChanges) => void; + styleProperty: IDynamicStyleProperty; + useCustomIconMap?: boolean; + isCustomOnly: boolean; +} + +interface State { + customIconStops: IconStop[]; +} + +export class IconMapSelect extends Component { + state = { + customIconStops: this.props.customIconStops ? this.props.customIconStops : DEFAULT_ICON_STOPS, + }; + + _onMapSelect = (selectedValue: string) => { + const useCustomIconMap = selectedValue === CUSTOM_MAP_ID; + const changes: StyleOptionChanges = { + iconPaletteId: useCustomIconMap ? null : selectedValue, + useCustomIconMap, + }; + // edge case when custom palette is first enabled + // customIconStops is undefined so need to update custom stops with default so icons are rendered. + if (!this.props.customIconStops) { + changes.customIconStops = DEFAULT_ICON_STOPS; + } + this.props.onChange(changes); + }; + + _onCustomMapChange = ({ + customStops, + isInvalid, + }: { + customStops: IconStop[]; + isInvalid: boolean; + }) => { + // Manage invalid custom map in local state + this.setState({ customIconStops: customStops }); + + if (!isInvalid) { + this.props.onChange({ + useCustomIconMap: true, + customIconStops: customStops, + }); + } + }; + + _renderCustomStopsInput() { + return !this.props.isCustomOnly && !this.props.useCustomIconMap ? null : ( + + ); + } + + _renderMapSelect() { + if (this.props.isCustomOnly) { + return null; + } + + const mapOptionsWithCustom = [ + { + value: CUSTOM_MAP_ID, + inputDisplay: i18n.translate('xpack.maps.styles.icon.customMapLabel', { + defaultMessage: 'Custom icon palette', + }), + }, + ...getIconPaletteOptions(), + ]; + + let valueOfSelected = ''; + if (this.props.useCustomIconMap) { + valueOfSelected = CUSTOM_MAP_ID; + } else if (this.props.iconPaletteId) { + valueOfSelected = this.props.iconPaletteId; + } + + return ( + + + + + ); + } + + render() { + return ( + + {this._renderMapSelect()} + {this._renderCustomStopsInput()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js index 1ceff3e3ba8019..c8ad869d33d33f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js @@ -15,6 +15,8 @@ import { EuiSelectable, } from '@elastic/eui'; import { SymbolIcon } from '../legend/symbol_icon'; +import { SYMBOL_OPTIONS } from '../../symbol_utils'; +import { getIsDarkMode } from '../../../../../kibana_services'; function isKeyboardEvent(event) { return typeof event === 'object' && 'keyCode' in event; @@ -62,7 +64,6 @@ export class IconSelect extends Component { }; _renderPopoverButton() { - const { isDarkMode, value } = this.props; return ( } /> @@ -93,8 +94,7 @@ export class IconSelect extends Component { } _renderIconSelectable() { - const { isDarkMode } = this.props; - const options = this.props.symbolOptions.map(({ value, label }) => { + const options = SYMBOL_OPTIONS.map(({ value, label }) => { return { value, label, @@ -102,7 +102,7 @@ export class IconSelect extends Component { ), }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js index 56dce6fad8386c..8dc2057054e626 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js @@ -4,25 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('../../../../../kibana_services', () => { + return { + getIsDarkMode() { + return false; + }, + }; +}); + +jest.mock('../../symbol_utils', () => { + return { + SYMBOL_OPTIONS: [ + { value: 'symbol1', label: 'symbol1' }, + { value: 'symbol2', label: 'symbol2' }, + ], + }; +}); + import React from 'react'; import { shallow } from 'enzyme'; import { IconSelect } from './icon_select'; -const symbolOptions = [ - { value: 'symbol1', label: 'symbol1' }, - { value: 'symbol2', label: 'symbol2' }, -]; - test('Should render icon select', () => { - const component = shallow( - {}} - symbolOptions={symbolOptions} - isDarkMode={false} - /> - ); + const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js index 81a44fcaadbd36..78fa6c10b899d2 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js @@ -11,7 +11,7 @@ import { getOtherCategoryLabel } from '../../style_util'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText } from '@elastic/eui'; import { IconSelect } from './icon_select'; import { StopInput } from '../stop_input'; -import { PREFERRED_ICONS } from '../../symbol_utils'; +import { PREFERRED_ICONS, SYMBOL_OPTIONS } from '../../symbol_utils'; function isDuplicateStop(targetStop, iconStops) { const stops = iconStops.filter(({ stop }) => { @@ -20,7 +20,7 @@ function isDuplicateStop(targetStop, iconStops) { return stops.length > 1; } -export function getFirstUnusedSymbol(symbolOptions, iconStops) { +export function getFirstUnusedSymbol(iconStops) { const firstUnusedPreferredIconId = PREFERRED_ICONS.find((iconId) => { const isSymbolBeingUsed = iconStops.some(({ icon }) => { return icon === iconId; @@ -32,7 +32,7 @@ export function getFirstUnusedSymbol(symbolOptions, iconStops) { return firstUnusedPreferredIconId; } - const firstUnusedSymbol = symbolOptions.find(({ value }) => { + const firstUnusedSymbol = SYMBOL_OPTIONS.find(({ value }) => { const isSymbolBeingUsed = iconStops.some(({ icon }) => { return icon === value; }); @@ -42,19 +42,7 @@ export function getFirstUnusedSymbol(symbolOptions, iconStops) { return firstUnusedSymbol ? firstUnusedSymbol.value : DEFAULT_ICON; } -const DEFAULT_ICON_STOPS = [ - { stop: null, icon: PREFERRED_ICONS[0] }, //first stop is the "other" color - { stop: '', icon: PREFERRED_ICONS[1] }, -]; - -export function IconStops({ - field, - getValueSuggestions, - iconStops = DEFAULT_ICON_STOPS, - isDarkMode, - onChange, - symbolOptions, -}) { +export function IconStops({ field, getValueSuggestions, iconStops, onChange }) { return iconStops.map(({ stop, icon }, index) => { const onIconSelect = (selectedIconId) => { const newIconStops = [...iconStops]; @@ -62,7 +50,7 @@ export function IconStops({ ...iconStops[index], icon: selectedIconId, }; - onChange({ customMapStops: newIconStops }); + onChange({ customStops: newIconStops }); }; const onStopChange = (newStopValue) => { const newIconStops = [...iconStops]; @@ -71,17 +59,17 @@ export function IconStops({ stop: newStopValue, }; onChange({ - customMapStops: newIconStops, + customStops: newIconStops, isInvalid: isDuplicateStop(newStopValue, iconStops), }); }; const onAdd = () => { onChange({ - customMapStops: [ + customStops: [ ...iconStops.slice(0, index + 1), { stop: '', - icon: getFirstUnusedSymbol(symbolOptions, iconStops), + icon: getFirstUnusedSymbol(iconStops), }, ...iconStops.slice(index + 1), ], @@ -89,7 +77,7 @@ export function IconStops({ }; const onRemove = () => { onChange({ - customMapStops: [...iconStops.slice(0, index), ...iconStops.slice(index + 1)], + customStops: [...iconStops.slice(0, index), ...iconStops.slice(index + 1)], }); }; @@ -157,13 +145,7 @@ export function IconStops({ {stopInput} - +
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js index ffe9b6feef4624..fe73659b0fe58d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js @@ -4,17 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { getFirstUnusedSymbol } from './icon_stops'; -describe('getFirstUnusedSymbol', () => { - const symbolOptions = [{ value: 'icon1' }, { value: 'icon2' }]; +jest.mock('./icon_select', () => ({ + IconSelect: () => { + return
mockIconSelect
; + }, +})); + +jest.mock('../../symbol_utils', () => { + return { + SYMBOL_OPTIONS: [{ value: 'icon1' }, { value: 'icon2' }], + PREFERRED_ICONS: [ + 'circle', + 'marker', + 'square', + 'star', + 'triangle', + 'hospital', + 'circle-stroked', + 'marker-stroked', + 'square-stroked', + 'star-stroked', + 'triangle-stroked', + ], + }; +}); +describe('getFirstUnusedSymbol', () => { test('Should return first unused icon from PREFERRED_ICONS', () => { const iconStops = [ { stop: 'category1', icon: 'circle' }, { stop: 'category2', icon: 'marker' }, ]; - const nextIcon = getFirstUnusedSymbol(symbolOptions, iconStops); + const nextIcon = getFirstUnusedSymbol(iconStops); expect(nextIcon).toBe('square'); }); @@ -33,7 +57,7 @@ describe('getFirstUnusedSymbol', () => { { stop: 'category11', icon: 'triangle-stroked' }, { stop: 'category12', icon: 'icon1' }, ]; - const nextIcon = getFirstUnusedSymbol(symbolOptions, iconStops); + const nextIcon = getFirstUnusedSymbol(iconStops); expect(nextIcon).toBe('icon2'); }); @@ -53,7 +77,7 @@ describe('getFirstUnusedSymbol', () => { { stop: 'category12', icon: 'icon1' }, { stop: 'category13', icon: 'icon2' }, ]; - const nextIcon = getFirstUnusedSymbol(symbolOptions, iconStops); + const nextIcon = getFirstUnusedSymbol(iconStops); expect(nextIcon).toBe('marker'); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js index 56e5737f724498..986f279dddc1a2 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js @@ -8,13 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IconSelect } from './icon_select'; -export function StaticIconForm({ - isDarkMode, - onStaticStyleChange, - staticDynamicSelect, - styleProperty, - symbolOptions, -}) { +export function StaticIconForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) { const onChange = (selectedIconId) => { onStaticStyleChange(styleProperty.getStyleName(), { value: selectedIconId }); }; @@ -25,12 +19,7 @@ export function StaticIconForm({ {staticDynamicSelect} - + ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js index 36b6c1a76470ca..2a983a32f0d825 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js @@ -6,25 +6,15 @@ import React from 'react'; -import { getUiSettings } from '../../../../../kibana_services'; import { StylePropEditor } from '../style_prop_editor'; import { DynamicIconForm } from './dynamic_icon_form'; import { StaticIconForm } from './static_icon_form'; -import { SYMBOL_OPTIONS } from '../../symbol_utils'; export function VectorStyleIconEditor(props) { const iconForm = props.styleProperty.isDynamic() ? ( - + ) : ( - + ); return {iconForm}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts index b53623ab52edb2..e153b6e4850f79 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts @@ -33,4 +33,5 @@ export interface IDynamicStyleProperty extends IStyleProperty { pluckCategoricalStyleMetaFromFeatures(features: unknown[]): CategoryFieldMeta; pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData: unknown): RangeFieldMeta; pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData: unknown): CategoryFieldMeta; + getValueSuggestions(query: string): string[]; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js index 04df9d73d75cd8..3a5f9b8f6690ec 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js @@ -9,6 +9,7 @@ import maki from '@elastic/maki'; import xml2js from 'xml2js'; import { parseXmlString } from '../../../../common/parse_xml_string'; import { SymbolIcon } from './components/legend/symbol_icon'; +import { getIsDarkMode } from '../../../kibana_services'; export const LARGE_MAKI_ICON_SIZE = 15; const LARGE_MAKI_ICON_SIZE_AS_STRING = LARGE_MAKI_ICON_SIZE.toString(); @@ -111,7 +112,8 @@ ICON_PALETTES.forEach((iconPalette) => { }); }); -export function getIconPaletteOptions(isDarkMode) { +export function getIconPaletteOptions() { + const isDarkMode = getIsDarkMode(); return ICON_PALETTES.map(({ id, icons }) => { const iconsDisplay = icons.map((iconId) => { const style = { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts index bc032639dd07d4..d630d2909b3d8b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts @@ -5,14 +5,9 @@ */ jest.mock('../../../kibana_services', () => { - const mockUiSettings = { - get: () => { - return undefined; - }, - }; return { - getUiSettings: () => { - return mockUiSettings; + getIsDarkMode() { + return false; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index a3ae80e0a59359..50321510c2ba82 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -18,8 +18,7 @@ import { CATEGORICAL_COLOR_PALETTES, } from '../color_palettes'; import { VectorStylePropertiesDescriptor } from '../../../../common/descriptor_types'; -// @ts-ignore -import { getUiSettings } from '../../../kibana_services'; +import { getIsDarkMode } from '../../../kibana_services'; export const MIN_SIZE = 1; export const MAX_SIZE = 64; @@ -67,7 +66,7 @@ export function getDefaultStaticProperties( const nextFillColor = DEFAULT_FILL_COLORS[nextColorIndex]; const nextLineColor = DEFAULT_LINE_COLORS[nextColorIndex]; - const isDarkMode = getUiSettings().get('theme:darkMode', false); + const isDarkMode = getIsDarkMode(); return { [VECTOR_STYLES.ICON]: { diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts index 8fa52500fb16e1..d4a7fa5d50af8a 100644 --- a/x-pack/plugins/maps/public/kibana_services.d.ts +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -24,6 +24,7 @@ export function getVisualizations(): any; export function getDocLinks(): any; export function getCoreChrome(): any; export function getUiSettings(): any; +export function getIsDarkMode(): boolean; export function getCoreOverlays(): any; export function getData(): any; export function getUiActions(): any; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 1684acfb0f463b..97d7f0c66c629a 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -40,6 +40,9 @@ export const getFileUploadComponent = () => { let uiSettings; export const setUiSettings = (coreUiSettings) => (uiSettings = coreUiSettings); export const getUiSettings = () => uiSettings; +export const getIsDarkMode = () => { + return getUiSettings().get('theme:darkMode', false); +}; let indexPatternSelectComponent; export const setIndexPatternSelect = (indexPatternSelect) => From 9506dc90caafd4b4ecbee6dd29dbca3d5418654c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 14 Jul 2020 16:25:31 -0500 Subject: [PATCH 16/26] [DOCS] Adds ID to logstash pipeline (#71726) --- .../logstash-configuration-management/create-logstash.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/logstash-configuration-management/create-logstash.asciidoc b/docs/api/logstash-configuration-management/create-logstash.asciidoc index 9bd5a9028ee9af..b608f4ee698f74 100644 --- a/docs/api/logstash-configuration-management/create-logstash.asciidoc +++ b/docs/api/logstash-configuration-management/create-logstash.asciidoc @@ -20,6 +20,9 @@ experimental[] Create a centrally-managed Logstash pipeline, or update an existi [[logstash-configuration-management-api-create-request-body]] ==== Request body +`id`:: + (Required, string) The pipeline ID. + `description`:: (Optional, string) The pipeline description. From 754ade5130a18604c0a1d5bb01e8442568c8dd44 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 15 Jul 2020 00:26:39 +0300 Subject: [PATCH 17/26] [SIEM] Fix custom date time mapping bug (#70713) Co-authored-by: Xavier Mouligneau Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../common/graphql/shared/schema.gql.ts | 9 +- .../common/types/timeline/index.ts | 8 +- .../integration/ml_conditional_links.spec.ts | 26 +-- .../integration/url_compatibility.spec.ts | 22 +- .../cypress/integration/url_state.spec.ts | 68 +++--- .../security_solution/cypress/urls/state.ts | 18 +- .../components/alerts_viewer/alerts_table.tsx | 8 +- .../events_viewer/events_viewer.test.tsx | 112 ++++++++- .../events_viewer/events_viewer.tsx | 30 ++- .../components/events_viewer/index.test.tsx | 6 +- .../common/components/events_viewer/index.tsx | 12 +- .../common/components/events_viewer/mock.ts | 12 +- .../matrix_histogram/index.test.tsx | 4 +- .../components/matrix_histogram/index.tsx | 4 +- .../components/matrix_histogram/types.ts | 12 +- .../components/matrix_histogram/utils.test.ts | 8 +- .../components/matrix_histogram/utils.ts | 4 +- .../ml/anomaly/anomaly_table_provider.tsx | 4 +- .../ml/anomaly/use_anomalies_table_data.ts | 10 +- .../ml/links/create_explorer_link.test.ts | 4 +- .../ml/links/create_explorer_link.tsx | 6 +- .../__snapshots__/anomaly_score.test.tsx.snap | 4 +- .../anomaly_scores.test.tsx.snap | 8 +- .../create_descriptions_list.test.tsx.snap | 4 +- .../ml/score/anomaly_score.test.tsx | 10 +- .../components/ml/score/anomaly_score.tsx | 4 +- .../ml/score/anomaly_scores.test.tsx | 17 +- .../components/ml/score/anomaly_scores.tsx | 4 +- .../ml/score/create_description_list.tsx | 4 +- .../score/create_descriptions_list.test.tsx | 11 +- .../score/score_interval_to_datetime.test.ts | 16 +- .../ml/score/score_interval_to_datetime.ts | 12 +- .../get_anomalies_host_table_columns.test.tsx | 4 +- .../get_anomalies_host_table_columns.tsx | 8 +- ...t_anomalies_network_table_columns.test.tsx | 4 +- .../get_anomalies_network_table_columns.tsx | 8 +- .../ml/tables/host_equality.test.ts | 48 ++-- .../ml/tables/network_equality.test.ts | 56 ++--- .../public/common/components/ml/types.ts | 4 +- .../navigation/breadcrumbs/index.test.ts | 30 +-- .../components/navigation/index.test.tsx | 24 +- .../navigation/tab_navigation/index.test.tsx | 16 +- .../components/stat_items/index.test.tsx | 16 +- .../common/components/stat_items/index.tsx | 8 +- .../super_date_picker/index.test.tsx | 8 +- .../components/super_date_picker/index.tsx | 4 +- .../super_date_picker/selectors.test.ts | 28 +-- .../common/components/top_n/index.test.tsx | 14 +- .../common/components/top_n/top_n.test.tsx | 16 +- .../public/common/components/top_n/top_n.tsx | 4 +- .../__mocks__/normalize_time_range.ts | 10 + .../components/url_state/index.test.tsx | 29 +-- .../url_state/index_mocked.test.tsx | 20 +- .../url_state/initialize_redux_by_url.tsx | 5 + .../url_state/normalize_time_range.test.ts | 132 +++++------ .../url_state/normalize_time_range.ts | 13 +- .../components/url_state/test_dependencies.ts | 8 +- .../public/common/components/utils.ts | 2 +- .../events/last_event_time/index.ts | 4 + .../last_event_time.gql_query.ts | 8 +- .../containers/events/last_event_time/mock.ts | 1 + .../common/containers/global_time/index.tsx | 98 ++++++++ .../matrix_histogram/index.test.tsx | 12 +- .../common/containers/query_template.tsx | 8 +- .../containers/query_template_paginated.tsx | 8 +- .../common/containers/source/index.test.tsx | 11 + .../public/common/containers/source/index.tsx | 34 +++ .../public/common/containers/source/mock.ts | 13 +- .../public/common/mock/global_state.ts | 20 +- .../public/common/mock/timeline_results.ts | 12 +- .../public/common/store/inputs/actions.ts | 12 +- .../common/store/inputs/helpers.test.ts | 24 +- .../public/common/store/inputs/model.ts | 13 +- .../utils/default_date_settings.test.ts | 36 +-- .../common/utils/default_date_settings.ts | 4 +- .../alerts_histogram.test.tsx | 4 +- .../alerts_histogram.tsx | 4 +- .../alerts_histogram_panel/helpers.tsx | 7 +- .../alerts_histogram_panel/index.test.tsx | 4 +- .../components/alerts_table/actions.test.tsx | 16 +- .../components/alerts_table/actions.tsx | 4 +- .../components/alerts_table/index.test.tsx | 4 +- .../components/alerts_table/index.tsx | 4 +- .../components/alerts_table/types.ts | 4 +- .../rules/fetch_index_patterns.test.tsx | 11 + .../rules/fetch_index_patterns.tsx | 53 +++-- .../detection_engine.test.tsx | 9 +- .../detection_engine/detection_engine.tsx | 6 +- .../rules/details/index.test.tsx | 9 +- .../detection_engine/rules/details/index.tsx | 6 +- .../public/graphql/introspection.json | 219 +++++++++++++++++- .../security_solution/public/graphql/types.ts | 50 +++- .../hosts/components/kpi_hosts/index.test.tsx | 4 +- .../hosts/components/kpi_hosts/index.tsx | 4 +- .../authentications/index.gql_query.ts | 2 + .../containers/authentications/index.tsx | 2 + .../first_last_seen.gql_query.ts | 13 +- .../containers/hosts/first_last_seen/index.ts | 4 +- .../containers/hosts/first_last_seen/mock.ts | 1 + .../containers/hosts/hosts_table.gql_query.ts | 2 + .../public/hosts/containers/hosts/index.tsx | 10 +- .../hosts/containers/hosts/overview/index.tsx | 8 +- .../hosts/pages/details/details_tabs.test.tsx | 19 +- .../hosts/pages/details/details_tabs.tsx | 9 +- .../public/hosts/pages/details/index.tsx | 9 +- .../public/hosts/pages/details/types.ts | 10 +- .../public/hosts/pages/hosts.tsx | 9 +- .../public/hosts/pages/hosts_tabs.tsx | 11 +- .../authentications_query_tab_body.tsx | 2 + .../pages/navigation/hosts_query_tab_body.tsx | 2 + .../public/hosts/pages/navigation/types.ts | 2 + .../public/hosts/pages/types.ts | 6 +- .../embeddables/embedded_map.test.tsx | 4 +- .../components/embeddables/embedded_map.tsx | 4 +- .../embeddables/embedded_map_helpers.test.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/ip_overview/index.test.tsx | 4 +- .../network/components/ip_overview/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 8 +- .../components/kpi_network/index.test.tsx | 4 +- .../network/components/kpi_network/index.tsx | 8 +- .../network/components/kpi_network/mock.ts | 4 +- .../containers/ip_overview/index.gql_query.ts | 8 +- .../network/containers/ip_overview/index.tsx | 3 +- .../public/network/containers/tls/index.tsx | 4 +- .../network/pages/ip_details/index.test.tsx | 17 +- .../public/network/pages/ip_details/index.tsx | 3 +- .../public/network/pages/ip_details/types.ts | 4 +- .../pages/navigation/network_routes.tsx | 6 +- .../public/network/pages/navigation/types.ts | 4 +- .../public/network/pages/network.test.tsx | 4 +- .../public/network/pages/network.tsx | 6 +- .../public/network/pages/types.ts | 4 +- .../alerts_by_category/index.test.tsx | 4 +- .../components/event_counts/index.test.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/host_overview/index.test.tsx | 4 +- .../components/host_overview/index.tsx | 4 +- .../components/overview_host/index.test.tsx | 4 +- .../overview_network/index.test.tsx | 4 +- .../components/signals_by_category/index.tsx | 6 +- .../containers/overview_host/index.tsx | 4 +- .../containers/overview_network/index.tsx | 4 +- .../public/overview/pages/overview.test.tsx | 9 +- .../open_timeline/export_timeline/mocks.ts | 2 +- .../components/open_timeline/helpers.test.ts | 57 ++--- .../components/open_timeline/helpers.ts | 11 +- .../components/open_timeline/types.ts | 4 +- .../__snapshots__/timeline.test.tsx.snap | 6 +- .../components/timeline/body/events/index.tsx | 5 +- .../timeline/body/events/stateful_event.tsx | 5 +- .../components/timeline/body/index.test.tsx | 1 + .../components/timeline/body/index.tsx | 5 +- .../timeline/body/stateful_body.tsx | 6 +- .../components/timeline/helpers.test.tsx | 45 ++-- .../timelines/components/timeline/helpers.tsx | 19 +- .../components/timeline/index.test.tsx | 6 +- .../timelines/components/timeline/index.tsx | 7 +- .../timeline/query_bar/index.test.tsx | 24 +- .../components/timeline/query_bar/index.tsx | 4 +- .../search_or_filter/search_or_filter.tsx | 4 +- .../components/timeline/timeline.test.tsx | 42 +++- .../components/timeline/timeline.tsx | 70 ++++-- .../containers/details/index.gql_query.ts | 8 +- .../timelines/containers/details/index.tsx | 4 + .../timelines/containers/index.gql_query.ts | 4 + .../public/timelines/containers/index.tsx | 11 + .../timelines/store/timeline/actions.ts | 6 +- .../timelines/store/timeline/defaults.ts | 9 +- .../timelines/store/timeline/epic.test.ts | 10 +- .../timeline/epic_local_storage.test.tsx | 6 +- .../timelines/store/timeline/helpers.ts | 13 +- .../public/timelines/store/timeline/model.ts | 4 +- .../timelines/store/timeline/reducer.test.ts | 54 ++--- .../graphql/authentications/schema.gql.ts | 1 + .../server/graphql/events/resolvers.ts | 1 + .../server/graphql/events/schema.gql.ts | 3 + .../server/graphql/hosts/resolvers.ts | 1 + .../server/graphql/hosts/schema.gql.ts | 8 +- .../server/graphql/ip_details/schema.gql.ts | 1 + .../server/graphql/network/schema.gql.ts | 1 + .../server/graphql/timeline/schema.gql.ts | 4 +- .../security_solution/server/graphql/types.ts | 58 ++++- .../server/lib/authentications/query.dsl.ts | 5 + .../lib/events/elasticsearch_adapter.ts | 2 +- .../server/lib/events/query.dsl.ts | 74 +----- .../lib/events/query.last_event_time.dsl.ts | 6 + .../server/lib/events/types.ts | 8 +- .../server/lib/framework/types.ts | 2 + .../server/lib/hosts/mock.ts | 4 +- .../server/lib/hosts/query.hosts.dsl.ts | 5 + .../hosts/query.last_first_seen_host.dsl.ts | 3 + .../server/lib/hosts/types.ts | 2 + .../lib/ip_details/query_overview.dsl.ts | 9 +- .../server/lib/ip_details/query_users.dsl.ts | 6 +- .../server/lib/kpi_hosts/mock.ts | 4 +- .../lib/kpi_hosts/query_authentication.dsl.ts | 1 + .../server/lib/kpi_hosts/query_hosts.dsl.ts | 1 + .../lib/kpi_hosts/query_unique_ips.dsl.ts | 1 + .../server/lib/kpi_network/mock.ts | 8 +- .../server/lib/kpi_network/query_dns.dsl.ts | 1 + .../lib/kpi_network/query_network_events.ts | 1 + .../kpi_network/query_tls_handshakes.dsl.ts | 1 + .../lib/kpi_network/query_unique_flow.ts | 1 + .../query_unique_private_ips.dsl.ts | 1 + .../query.anomalies_over_time.dsl.ts | 7 +- .../query.authentications_over_time.dsl.ts | 7 +- .../query.events_over_time.dsl.ts | 7 +- .../lib/matrix_histogram/query_alerts.dsl.ts | 7 +- .../query_dns_histogram.dsl.ts | 1 + .../server/lib/network/mock.ts | 2 +- .../server/lib/network/query_dns.dsl.ts | 5 + .../server/lib/network/query_http.dsl.ts | 6 +- .../lib/network/query_top_countries.dsl.ts | 6 +- .../lib/network/query_top_n_flow.dsl.ts | 6 +- .../server/lib/overview/mock.ts | 16 +- .../server/lib/overview/query.dsl.ts | 2 + .../routes/__mocks__/import_timelines.ts | 10 +- .../routes/__mocks__/request_responses.ts | 6 +- .../security_solution/server/lib/tls/mock.ts | 2 +- .../server/lib/tls/query_tls.dsl.ts | 6 +- .../lib/uncommon_processes/query.dsl.ts | 1 + .../calculate_timeseries_interval.ts | 4 +- .../utils/build_query/create_options.test.ts | 73 +++++- .../utils/build_query/create_options.ts | 5 + .../apis/security_solution/authentications.ts | 6 +- .../apis/security_solution/hosts.ts | 8 +- .../apis/security_solution/ip_overview.ts | 2 + .../security_solution/kpi_host_details.ts | 6 +- .../apis/security_solution/kpi_hosts.ts | 10 +- .../apis/security_solution/kpi_network.ts | 10 +- .../apis/security_solution/network_dns.ts | 6 +- .../security_solution/network_top_n_flow.ts | 8 +- .../apis/security_solution/overview_host.ts | 5 +- .../security_solution/overview_network.ts | 15 +- .../saved_objects/timeline.ts | 2 +- .../apis/security_solution/sources.ts | 1 + .../apis/security_solution/timeline.ts | 20 +- .../security_solution/timeline_details.ts | 1 + .../apis/security_solution/tls.ts | 8 +- .../security_solution/uncommon_processes.ts | 8 +- .../apis/security_solution/users.ts | 5 +- 242 files changed, 2024 insertions(+), 979 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx diff --git a/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts b/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts index d043c1587d3c34..546fdd68b4257a 100644 --- a/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts +++ b/x-pack/plugins/security_solution/common/graphql/shared/schema.gql.ts @@ -11,9 +11,14 @@ export const sharedSchema = gql` "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan." interval: String! "The end of the timerange" - to: Float! + to: String! "The beginning of the timerange" - from: Float! + from: String! + } + + input docValueFieldsInput { + field: String! + format: String! } type CursorType { diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 021e5a7f00b173..98d17fc87f6ce3 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -124,8 +124,12 @@ const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ * DatePicker Range Types */ const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ - start: unionWithNullType(runtimeTypes.number), - end: unionWithNullType(runtimeTypes.number), + /* Before the change of all timestamp to ISO string the values of start and from + * attributes where a number. Specifically UNIX timestamps. + * To support old timeline's saved object we need to add the number io-ts type + */ + start: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), + end: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), }); /* diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index 6b3fc9e751ea40..0b302efd655a8c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -94,7 +94,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -102,7 +102,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -110,7 +110,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))' + 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27))' ); }); @@ -118,7 +118,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -126,7 +126,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -134,7 +134,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -142,7 +142,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -150,7 +150,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -158,7 +158,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -166,7 +166,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -174,7 +174,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -182,7 +182,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -190,7 +190,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index 205a49fc771cf3..5b42897b065e31 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -4,9 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loginAndWaitForPage } from '../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; +import { ABSOLUTE_DATE_RANGE } from '../urls/state'; +import { + DATE_PICKER_START_DATE_POPOVER_BUTTON, + DATE_PICKER_END_DATE_POPOVER_BUTTON, +} from '../screens/date_picker'; + +const ABSOLUTE_DATE = { + endTime: '2019-08-01T20:33:29.186Z', + startTime: '2019-08-01T20:03:29.186Z', +}; describe('URL compatibility', () => { it('Redirects to Detection alerts from old Detections URL', () => { @@ -14,4 +24,14 @@ describe('URL compatibility', () => { cy.url().should('include', '/security/detections'); }); + + it('sets the global start and end dates from the url with timestamps', () => { + loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlWithTimestamps); + cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( + 'have.attr', + 'title', + ABSOLUTE_DATE.startTime + ); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 81af9ece9ed451..cdcdde252d6d61 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -42,24 +42,12 @@ import { HOSTS_URL } from '../urls/navigation'; import { ABSOLUTE_DATE_RANGE } from '../urls/state'; const ABSOLUTE_DATE = { - endTime: '1564691609186', - endTimeFormat: '2019-08-01T20:33:29.186Z', - endTimeTimeline: '1564779809186', - endTimeTimelineFormat: '2019-08-02T21:03:29.186Z', - endTimeTimelineTyped: 'Aug 02, 2019 @ 21:03:29.186', - endTimeTyped: 'Aug 01, 2019 @ 14:33:29.186', - newEndTime: '1564693409186', - newEndTimeFormat: '2019-08-01T21:03:29.186Z', + endTime: '2019-08-01T20:33:29.186Z', + endTimeTimeline: '2019-08-02T21:03:29.186Z', newEndTimeTyped: 'Aug 01, 2019 @ 15:03:29.186', - newStartTime: '1564691609186', - newStartTimeFormat: '2019-08-01T20:33:29.186Z', newStartTimeTyped: 'Aug 01, 2019 @ 14:33:29.186', - startTime: '1564689809186', - startTimeFormat: '2019-08-01T20:03:29.186Z', - startTimeTimeline: '1564776209186', - startTimeTimelineFormat: '2019-08-02T20:03:29.186Z', - startTimeTimelineTyped: 'Aug 02, 2019 @ 14:03:29.186', - startTimeTyped: 'Aug 01, 2019 @ 14:03:29.186', + startTime: '2019-08-01T20:03:29.186Z', + startTimeTimeline: '2019-08-02T20:03:29.186Z', }; describe('url state', () => { @@ -68,13 +56,9 @@ describe('url state', () => { cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeFormat - ); - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should( - 'have.attr', - 'title', - ABSOLUTE_DATE.endTimeFormat + ABSOLUTE_DATE.startTime ); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); }); it('sets the url state when start and end date are set', () => { @@ -87,9 +71,11 @@ describe('url state', () => { cy.url().should( 'include', - `(global:(linkTo:!(timeline),timerange:(from:${new Date( + `(global:(linkTo:!(timeline),timerange:(from:%27${new Date( ABSOLUTE_DATE.newStartTimeTyped - ).valueOf()},kind:absolute,to:${new Date(ABSOLUTE_DATE.newEndTimeTyped).valueOf()}))` + ).toISOString()}%27,kind:absolute,to:%27${new Date( + ABSOLUTE_DATE.newEndTimeTyped + ).toISOString()}%27))` ); }); @@ -100,12 +86,12 @@ describe('url state', () => { cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeFormat + ABSOLUTE_DATE.startTime ); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.endTimeFormat + ABSOLUTE_DATE.endTime ); }); @@ -114,25 +100,21 @@ describe('url state', () => { cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeFormat - ); - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should( - 'have.attr', - 'title', - ABSOLUTE_DATE.endTimeFormat + ABSOLUTE_DATE.startTime ); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); openTimeline(); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.startTimeTimelineFormat + ABSOLUTE_DATE.startTimeTimeline ); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', 'title', - ABSOLUTE_DATE.endTimeTimelineFormat + ABSOLUTE_DATE.endTimeTimeline ); }); @@ -146,9 +128,11 @@ describe('url state', () => { cy.url().should( 'include', - `timeline:(linkTo:!(),timerange:(from:${new Date( + `timeline:(linkTo:!(),timerange:(from:%27${new Date( ABSOLUTE_DATE.newStartTimeTyped - ).valueOf()},kind:absolute,to:${new Date(ABSOLUTE_DATE.newEndTimeTyped).valueOf()}))` + ).toISOString()}%27,kind:absolute,to:%27${new Date( + ABSOLUTE_DATE.newEndTimeTyped + ).toISOString()}%27))` ); }); @@ -180,7 +164,7 @@ describe('url state', () => { cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))` + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` ); }); @@ -193,12 +177,12 @@ describe('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(HOSTS_NAMES).first().invoke('text').should('eq', 'siem-kibana'); @@ -209,21 +193,21 @@ describe('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))" ); cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); }); diff --git a/x-pack/plugins/security_solution/cypress/urls/state.ts b/x-pack/plugins/security_solution/cypress/urls/state.ts index bdd90c21fbedf4..7825be08e38e1a 100644 --- a/x-pack/plugins/security_solution/cypress/urls/state.ts +++ b/x-pack/plugins/security_solution/cypress/urls/state.ts @@ -6,16 +6,18 @@ export const ABSOLUTE_DATE_RANGE = { url: - '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', + urlWithTimestamps: + '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', urlUnlinked: - '/app/security/network/flows/?timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', - urlKqlNetworkNetwork: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlNetworkHosts: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsNetwork: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsHosts: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + '/app/security/network/flows/?timerange=(global:(linkTo:!(),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(),timerange:(from:%272019-08-02T20:03:29.186Z%27,kind:absolute,to:%272019-08-02T21:03:29.186Z%27)))', + urlKqlNetworkNetwork: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, + urlKqlNetworkHosts: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, + urlKqlHostsNetwork: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, + urlKqlHostsHosts: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))`, urlHost: - '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', urlHostNew: - '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))', + '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272020-01-01T21:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272020-01-01T21:33:29.186Z%27)))', }; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index bf2d8948b72928..841a1ef09ede6c 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -17,9 +17,9 @@ import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; export interface OwnProps { - end: number; + end: string; id: string; - start: number; + start: string; } const defaultAlertsFilters: Filter[] = [ @@ -57,8 +57,8 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; - endDate: number; - startDate: number; + endDate: string; + startDate: string; pageFilters?: Filter[]; } diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 38ca1176d1700c..674eb3325efc2c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -15,29 +15,36 @@ import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { defaultHeaders } from './default_headers'; import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; -import { mockBrowserFields } from '../../containers/source/mock'; +import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../components/url_state/normalize_time_range.ts'); + const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); -mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - }, -]); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); -const from = 1566943856794; -const to = 1566857456791; +const from = '2019-08-26T22:10:56.791Z'; +const to = '2019-08-27T22:10:56.794Z'; describe('EventsViewer', () => { const mount = useMountAppended(); + beforeEach(() => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: false, + }, + ]); + }); + test('it renders the "Showing..." subtitle with the expected event count', async () => { const wrapper = mount( @@ -60,6 +67,93 @@ describe('EventsViewer', () => { ); }); + test('it does NOT render fetch index pattern is loading', async () => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: true, + }, + ]); + + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + }); + + test('it does NOT render when start is empty', async () => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: true, + }, + ]); + + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + }); + + test('it does NOT render when end is empty', async () => { + mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + docValueFields: mockDocValueFields, + isLoading: true, + }, + ]); + + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + }); + test('it renders the Fields Browser as a settings gear', async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index a81c5facb07182..5e0d5a6e9b0993 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { BrowserFields } from '../../containers/source'; +import { BrowserFields, DocValueFields } from '../../containers/source'; import { TimelineQuery } from '../../../timelines/containers'; import { Direction } from '../../../graphql/types'; import { useKibana } from '../../lib/kibana'; @@ -51,19 +51,21 @@ interface Props { columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; deletedEventIds: Readonly; - end: number; + docValueFields: DocValueFields[]; + end: string; filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; id: string; indexPattern: IIndexPattern; isLive: boolean; + isLoadingIndexPattern: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; kqlMode: KqlMode; onChangeItemsPerPage: OnChangeItemsPerPage; query: Query; - start: number; + start: string; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -76,6 +78,7 @@ const EventsViewerComponent: React.FC = ({ columns, dataProviders, deletedEventIds, + docValueFields, end, filters, headerFilterGroup, @@ -83,6 +86,7 @@ const EventsViewerComponent: React.FC = ({ id, indexPattern, isLive, + isLoadingIndexPattern, itemsPerPage, itemsPerPageOptions, kqlMode, @@ -122,6 +126,17 @@ const EventsViewerComponent: React.FC = ({ end, isEventViewer: true, }); + + const canQueryTimeline = useMemo( + () => + combinedQueries != null && + isLoadingIndexPattern != null && + !isLoadingIndexPattern && + !isEmpty(start) && + !isEmpty(end), + [isLoadingIndexPattern, combinedQueries, start, end] + ); + const fields = useMemo( () => union( @@ -140,16 +155,19 @@ const EventsViewerComponent: React.FC = ({ return ( - {combinedQueries != null ? ( + {canQueryTimeline ? ( {({ events, @@ -187,6 +205,7 @@ const EventsViewerComponent: React.FC = ({ !deletedEventIds.includes(e._id))} + docValueFields={docValueFields} id={id} isEventViewer={true} height={height} @@ -232,6 +251,7 @@ export const EventsViewer = React.memo( (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.dataProviders === nextProps.dataProviders && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index a5f4dc0c5ed6fa..1f820c0c748b61 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -18,6 +18,8 @@ import { useFetchIndexPatterns } from '../../../detections/containers/detection_ import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; +jest.mock('../../components/url_state/normalize_time_range.ts'); + const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ @@ -31,8 +33,8 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); -const from = 1566943856794; -const to = 1566857456791; +const from = '2019-08-27T22:10:56.794Z'; +const to = '2019-08-26T22:10:56.791Z'; describe('StatefulEventsViewer', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 637f1a48143a9f..6c610a084e7f29 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -27,9 +27,9 @@ import { InspectButtonContainer } from '../inspect'; export interface OwnProps { defaultIndices?: string[]; defaultModel: SubsetTimelineModel; - end: number; + end: string; id: string; - start: number; + start: string; headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -65,9 +65,9 @@ const StatefulEventsViewerComponent: React.FC = ({ // If truthy, the graph viewer (Resolver) is showing graphEventId, }) => { - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY) - ); + const [ + { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern }, + ] = useFetchIndexPatterns(defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY)); useEffect(() => { if (createTimeline != null) { @@ -120,10 +120,12 @@ const StatefulEventsViewerComponent: React.FC = ({ { const mockMatrixOverTimeHistogramProps = { defaultIndex: ['defaultIndex'], defaultStackByOption: { text: 'text', value: 'value' }, - endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), + endDate: '2019-07-18T20:00:00.000Z', errorMessage: 'error', histogramType: HistogramType.alerts, id: 'mockId', @@ -64,7 +64,7 @@ describe('Matrix Histogram Component', () => { sourceId: 'default', stackByField: 'mockStackByField', stackByOptions: [{ text: 'text', value: 'value' }], - startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(), + startDate: '2019-07-18T19:00: 00.000Z', subtitle: 'mockSubtitle', totalCount: -1, title: 'mockTitle', diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 16fe2a6669ff0f..fa512ad1ed80bf 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -115,8 +115,8 @@ export const MatrixHistogramComponent: React.FC< const [min, max] = x; dispatchSetAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, - from: min, - to: max, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), }); }, yTickFormatter, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index ff0816758cb0c4..a859b0dd392310 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -44,8 +44,8 @@ interface MatrixHistogramBasicProps { defaultStackByOption: MatrixHistogramOption; dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; endDate: GlobalTimeArgs['to']; headerChildren?: React.ReactNode; @@ -63,17 +63,17 @@ interface MatrixHistogramBasicProps { } export interface MatrixHistogramQueryProps { - endDate: number; + endDate: string; errorMessage: string; filterQuery?: ESQuery | string | undefined; setAbsoluteRangeDatePicker?: ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; setAbsoluteRangeDatePickerTarget?: InputsModelId; stackByField: string; - startDate: number; + startDate: string; indexToAdd?: string[] | null; isInspected: boolean; histogramType: HistogramType; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts index 9e3ddcc014c61b..7a3f44d3ea7296 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.test.ts @@ -22,8 +22,8 @@ describe('utils', () => { let configs: BarchartConfigs; beforeAll(() => { configs = getBarchartConfigs({ - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', onBrushEnd: jest.fn() as UpdateDateRange, }); }); @@ -53,8 +53,8 @@ describe('utils', () => { beforeAll(() => { configs = getBarchartConfigs({ chartHeight: mockChartHeight, - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', onBrushEnd: jest.fn() as UpdateDateRange, yTickFormatter: mockYTickFormatter, showLegend: false, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts index 45e9c54b2eff87..9474929d35a517 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts @@ -13,9 +13,9 @@ import { histogramDateTimeFormatter } from '../utils'; interface GetBarchartConfigsProps { chartHeight?: number; - from: number; + from: string; legendPosition?: Position; - to: number; + to: string; onBrushEnd: UpdateDateRange; yTickFormatter?: (value: number) => string; showLegend?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx index 6ccc41546e5587..66e70ddc2e14f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx @@ -15,8 +15,8 @@ interface ChildrenArgs { interface Props { influencers?: InfluencerInput[]; - startDate: number; - endDate: number; + startDate: string; + endDate: string; criteriaFields?: CriteriaFields[]; children: (args: ChildrenArgs) => React.ReactNode; skip: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 8568c7e6b55757..a6bbdee79cf04f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; @@ -19,8 +19,8 @@ import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; interface Args { influencers?: InfluencerInput[]; - endDate: number; - startDate: number; + endDate: string; + startDate: string; threshold?: number; skip?: boolean; criteriaFields?: CriteriaFields[]; @@ -67,6 +67,8 @@ export const useAnomaliesTableData = ({ const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); const siemJobIds = siemJobs.filter((job) => job.isInstalled).map((job) => job.id); + const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); + const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); useEffect(() => { let isSubscribed = true; @@ -116,7 +118,7 @@ export const useAnomaliesTableData = ({ } } - fetchAnomaliesTableData(influencers, criteriaFields, startDate, endDate); + fetchAnomaliesTableData(influencers, criteriaFields, startDateMs, endDateMs); return () => { isSubscribed = false; abortCtrl.abort(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts index 4a25f82a94a618..30d0673192af85 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts @@ -18,8 +18,8 @@ describe('create_explorer_link', () => { test('it returns expected link', () => { const entities = createExplorerLink( anomalies.anomalies[0], - new Date('1970').valueOf(), - new Date('3000').valueOf() + new Date('1970').toISOString(), + new Date('3000').toISOString() ); expect(entities).toEqual( "#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)" diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx index e00f53a08a9184..468bc962453f68 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx @@ -11,8 +11,8 @@ import { useKibana } from '../../../lib/kibana'; interface ExplorerLinkProps { score: Anomaly; - startDate: number; - endDate: number; + startDate: string; + endDate: string; linkName: React.ReactNode; } @@ -35,7 +35,7 @@ export const ExplorerLink: React.FC = ({ ); }; -export const createExplorerLink = (score: Anomaly, startDate: number, endDate: number): string => { +export const createExplorerLink = (score: Anomaly, startDate: string, endDate: string): string => { const startDateIso = new Date(startDate).toISOString(); const endDateIso = new Date(endDate).toISOString(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index 6694cec53987b2..0abb94f6e92ffa 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -127,7 +127,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` grow={false} > , diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap index de9ae94c4d95ee..b9e4a76363a40d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap @@ -7,7 +7,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` responsive={false} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap index 2e771f9f045b88..5d052ef028e0f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap @@ -44,7 +44,7 @@ exports[`create_description_list renders correctly against snapshot 1`] = ` grow={false} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx index b172c22a9ed4e6..f7fa0ac0a8be15 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx @@ -13,7 +13,9 @@ import { TestProviders } from '../../../mock/test_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; import { Anomalies } from '../types'; -const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const startDate: string = '2020-07-07T08:20:18.966Z'; +const endDate: string = '3000-01-01T00:00:00.000Z'; + const narrowDateRange = jest.fn(); describe('anomaly_scores', () => { @@ -28,7 +30,7 @@ describe('anomaly_scores', () => { const wrapper = shallow( { { { @@ -29,7 +30,7 @@ describe('anomaly_scores', () => { const wrapper = shallow( { { { { { { { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx index 7c8900bf77d95b..e9dd5f922e26a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx @@ -13,7 +13,8 @@ import { Anomaly } from '../types'; jest.mock('../../../lib/kibana'); -const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const startDate: string = '2020-07-07T08:20:18.966Z'; +const endDate: string = '3000-01-01T00:00:00.000Z'; describe('create_description_list', () => { let narrowDateRange = jest.fn(); @@ -27,7 +28,7 @@ describe('create_description_list', () => { { { { { test('converts a second interval to plus or minus (+/-) one hour', () => { const expected: FromTo = { - from: new Date('2019-06-25T04:31:59.345Z').valueOf(), - to: new Date('2019-06-25T06:31:59.345Z').valueOf(), + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'second')).toEqual(expected); @@ -26,8 +26,8 @@ describe('score_interval_to_datetime', () => { test('converts a minute interval to plus or minus (+/-) one hour', () => { const expected: FromTo = { - from: new Date('2019-06-25T04:31:59.345Z').valueOf(), - to: new Date('2019-06-25T06:31:59.345Z').valueOf(), + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'minute')).toEqual(expected); @@ -35,8 +35,8 @@ describe('score_interval_to_datetime', () => { test('converts a hour interval to plus or minus (+/-) one hour', () => { const expected: FromTo = { - from: new Date('2019-06-25T04:31:59.345Z').valueOf(), - to: new Date('2019-06-25T06:31:59.345Z').valueOf(), + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'hour')).toEqual(expected); @@ -44,8 +44,8 @@ describe('score_interval_to_datetime', () => { test('converts a day interval to plus or minus (+/-) one day', () => { const expected: FromTo = { - from: new Date('2019-06-24T05:31:59.345Z').valueOf(), - to: new Date('2019-06-26T05:31:59.345Z').valueOf(), + from: '2019-06-24T05:31:59.345Z', + to: '2019-06-26T05:31:59.345Z', }; anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf(); expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'day')).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts b/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts index b1257676a64b25..69b5be9272a385 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score_interval_to_datetime.ts @@ -8,21 +8,21 @@ import moment from 'moment'; import { Anomaly } from '../types'; export interface FromTo { - from: number; - to: number; + from: string; + to: string; } export const scoreIntervalToDateTime = (score: Anomaly, interval: string): FromTo => { if (interval === 'second' || interval === 'minute' || interval === 'hour') { return { - from: moment(score.time).subtract(1, 'hour').valueOf(), - to: moment(score.time).add(1, 'hour').valueOf(), + from: moment(score.time).subtract(1, 'hour').toISOString(), + to: moment(score.time).add(1, 'hour').toISOString(), }; } else { // default should be a day return { - from: moment(score.time).subtract(1, 'day').valueOf(), - to: moment(score.time).add(1, 'day').valueOf(), + from: moment(score.time).subtract(1, 'day').toISOString(), + to: moment(score.time).add(1, 'day').toISOString(), }; } }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 93b22460d4ed77..b90946c534f3ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -13,8 +13,8 @@ import { TestProviders } from '../../../mock'; import React from 'react'; import { useMountAppended } from '../../../utils/use_mount_appended'; -const startDate = new Date(2001).valueOf(); -const endDate = new Date(3000).valueOf(); +const startDate = new Date(2001).toISOString(); +const endDate = new Date(3000).toISOString(); const interval = 'days'; const narrowDateRange = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx index fc89189bf4f465..b72da55128f994 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -24,8 +24,8 @@ import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; export const getAnomaliesHostTableColumns = ( - startDate: number, - endDate: number, + startDate: string, + endDate: string, interval: string, narrowDateRange: NarrowDateRange ): [ @@ -132,8 +132,8 @@ export const getAnomaliesHostTableColumns = ( export const getAnomaliesHostTableColumnsCurated = ( pageType: HostsType, - startDate: number, - endDate: number, + startDate: string, + endDate: string, interval: string, narrowDateRange: NarrowDateRange ) => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index b113c692c535a2..79277c46e1c9d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -13,8 +13,8 @@ import React from 'react'; import { TestProviders } from '../../../mock'; import { useMountAppended } from '../../../utils/use_mount_appended'; -const startDate = new Date(2001).valueOf(); -const endDate = new Date(3000).valueOf(); +const startDate = new Date(2001).toISOString(); +const endDate = new Date(3000).toISOString(); describe('get_anomalies_network_table_columns', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index ce4269afbe5b2a..52b26a20a8f644 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -26,8 +26,8 @@ import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FlowTarget } from '../../../../graphql/types'; export const getAnomaliesNetworkTableColumns = ( - startDate: number, - endDate: number, + startDate: string, + endDate: string, flowTarget?: FlowTarget ): [ Columns, @@ -127,8 +127,8 @@ export const getAnomaliesNetworkTableColumns = ( export const getAnomaliesNetworkTableColumnsCurated = ( pageType: NetworkType, - startDate: number, - endDate: number, + startDate: string, + endDate: string, flowTarget?: FlowTarget ) => { const columns = getAnomaliesNetworkTableColumns(startDate, endDate, flowTarget); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts index 89b87f95e5159d..eaaf5a9aedcdb9 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts @@ -11,15 +11,15 @@ import { HostsType } from '../../../../hosts/store/model'; describe('host_equality', () => { test('it returns true if start and end date are equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -30,15 +30,15 @@ describe('host_equality', () => { test('it returns false if starts are not equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -49,15 +49,15 @@ describe('host_equality', () => { test('it returns false if starts are not equal for next', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -68,15 +68,15 @@ describe('host_equality', () => { test('it returns false if ends are not equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -87,15 +87,15 @@ describe('host_equality', () => { test('it returns false if ends are not equal for next', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, @@ -106,15 +106,15 @@ describe('host_equality', () => { test('it returns false if skip is not equal', () => { const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: true, type: HostsType.details, }; const next: AnomaliesHostTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: HostsType.details, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts index 8b3e30c3290314..3819e9d0e4b3fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts @@ -12,15 +12,15 @@ import { FlowTarget } from '../../../../graphql/types'; describe('network_equality', () => { test('it returns true if start and end date are equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -31,15 +31,15 @@ describe('network_equality', () => { test('it returns false if starts are not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -50,15 +50,15 @@ describe('network_equality', () => { test('it returns false if starts are not equal for next', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2001').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2001').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -69,15 +69,15 @@ describe('network_equality', () => { test('it returns false if ends are not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -88,15 +88,15 @@ describe('network_equality', () => { test('it returns false if ends are not equal for next', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2001').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2001').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -107,15 +107,15 @@ describe('network_equality', () => { test('it returns false if skip is not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: true, type: NetworkType.details, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, @@ -126,16 +126,16 @@ describe('network_equality', () => { test('it returns false if flowType is not equal', () => { const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: true, type: NetworkType.details, flowTarget: FlowTarget.source, }; const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').valueOf(), - endDate: new Date('2000').valueOf(), + startDate: new Date('2000').toISOString(), + endDate: new Date('2000').toISOString(), narrowDateRange: jest.fn(), skip: false, type: NetworkType.details, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/types.ts b/x-pack/plugins/security_solution/public/common/components/ml/types.ts index 13bceaa473a84f..a4c4f728b0f8f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/types.ts @@ -75,8 +75,8 @@ export interface AnomaliesByNetwork { } export interface HostOrNetworkProps { - startDate: number; - endDate: number; + startDate: string; + endDate: string; narrowDateRange: NarrowDateRange; skip: boolean; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index ade76f8e243383..7e508c28c62dfa 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -80,20 +80,20 @@ const getMockObject = ( global: { linkTo: ['timeline'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, timeline: { linkTo: ['global'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, @@ -123,7 +123,7 @@ describe('Navigation Breadcrumbs', () => { }, { href: - 'securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", text: 'Hosts', }, { @@ -143,7 +143,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - 'securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Flows', @@ -162,7 +162,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Timelines', href: - 'securitySolution:timelines?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:timelines?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, ]); }); @@ -177,12 +177,12 @@ describe('Navigation Breadcrumbs', () => { { text: 'Hosts', href: - 'securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'siem-kibana', href: - 'securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Authentications', href: '' }, ]); @@ -198,11 +198,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - 'securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: ipv4, - href: `securitySolution:network/ip/${ipv4}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + href: `securitySolution:network/ip/${ipv4}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { text: 'Flows', href: '' }, ]); @@ -218,11 +218,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - 'securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: ipv6, - href: `securitySolution:network/ip/${ipv6Encoded}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + href: `securitySolution:network/ip/${ipv6Encoded}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { text: 'Flows', href: '' }, ]); @@ -237,12 +237,12 @@ describe('Navigation Breadcrumbs', () => { { text: 'Hosts', href: - 'securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'siem-kibana', href: - 'securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Authentications', href: '' }, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index c60feb63241fb9..16cb19f5a0c146 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -57,20 +57,20 @@ describe('SIEM Navigation', () => { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], @@ -160,20 +160,20 @@ describe('SIEM Navigation', () => { global: { linkTo: ['timeline'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, timeline: { linkTo: ['global'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, @@ -259,20 +259,20 @@ describe('SIEM Navigation', () => { global: { linkTo: ['timeline'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, timeline: { linkTo: ['global'], timerange: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index f345346d620cb1..b25cf3779801b7 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -47,20 +47,20 @@ describe('Tab Navigation', () => { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], @@ -105,20 +105,20 @@ describe('Tab Navigation', () => { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx index f548275b36e709..8a78706e17a4cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx @@ -41,8 +41,8 @@ import { State, createStore } from '../../store'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { KpiNetworkData, KpiHostsData } from '../../../graphql/types'; -const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); -const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); +const from = '2019-06-15T06:00:00.000Z'; +const to = '2019-06-18T06:00:00.000Z'; jest.mock('../charts/areachart', () => { return { AreaChart: () =>
}; @@ -131,18 +131,18 @@ describe('Stat Items Component', () => { { key: 'uniqueSourceIpsHistogram', value: [ - { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, ], color: '#D36086', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, ], color: '#9170B8', }, diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index dee730059b03a6..183f89d9320f35 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -66,10 +66,10 @@ export interface StatItems { export interface StatItemsProps extends StatItems { areaChart?: ChartSeriesData[]; barChart?: ChartSeriesData[]; - from: number; + from: string; id: string; narrowDateRange: UpdateDateRange; - to: number; + to: string; } export const numberFormatter = (value: string | number): string => value.toLocaleString(); @@ -160,8 +160,8 @@ export const useKpiMatrixStatus = ( mappings: Readonly, data: KpiHostsData | KpiNetworkData, id: string, - from: number, - to: number, + from: string, + to: string, narrowDateRange: UpdateDateRange ): StatItemsProps[] => { const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]); diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 164ca177ee91ad..0795e46c9e45fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -156,8 +156,8 @@ describe('SIEM Super Date Picker', () => { }); test('Make Sure to (end date) is superior than from (start date)', () => { - expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( - store.getState().inputs.global.timerange.from + expect(new Date(store.getState().inputs.global.timerange.to).valueOf()).toBeGreaterThan( + new Date(store.getState().inputs.global.timerange.from).valueOf() ); }); }); @@ -321,7 +321,7 @@ describe('SIEM Super Date Picker', () => { const mapStateToProps = makeMapStateToProps(); const props1 = mapStateToProps(state, { id: 'global' }); const clone = cloneDeep(state); - clone.inputs.global.timerange.from = 999; + clone.inputs.global.timerange.from = '2020-07-07T09:20:18.966Z'; const props2 = mapStateToProps(clone, { id: 'global' }); expect(props1.start).not.toBe(props2.start); }); @@ -330,7 +330,7 @@ describe('SIEM Super Date Picker', () => { const mapStateToProps = makeMapStateToProps(); const props1 = mapStateToProps(state, { id: 'global' }); const clone = cloneDeep(state); - clone.inputs.global.timerange.to = 999; + clone.inputs.global.timerange.to = '2020-07-08T09:20:18.966Z'; const props2 = mapStateToProps(clone, { id: 'global' }); expect(props1.end).not.toBe(props2.end); }); diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx index 84ff1120f6496f..4443d24531b22e 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx @@ -216,9 +216,9 @@ export const formatDate = ( options?: { roundUp?: boolean; } -) => { +): string => { const momentDate = dateMath.parse(date, options); - return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; + return momentDate != null && momentDate.isValid() ? momentDate.toISOString() : ''; }; export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts index 1dafa141542bfd..7cb4ea9ada93fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/selectors.test.ts @@ -23,8 +23,8 @@ describe('selectors', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', }; let inputState: InputsRange = { @@ -57,8 +57,8 @@ describe('selectors', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: 0, - to: 0, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', }; inputState = { @@ -147,8 +147,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 1, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -179,8 +179,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 1, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -211,8 +211,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 0, - to: 1, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -243,8 +243,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 0, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, @@ -275,8 +275,8 @@ describe('selectors', () => { kind: 'relative', fromStr: '', toStr: '', - from: 0, - to: 0, + from: '2020-07-08T08:20:18.966Z', + to: '2020-07-09T08:20:18.966Z', }; const change: InputsRange = { ...inputState, diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index c8232b0c3b3cbe..b393e9ae6319b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -88,8 +88,8 @@ const state: State = { kind: 'relative', fromStr: 'now-24h', toStr: 'now', - from: 1586835969047, - to: 1586922369047, + from: '2020-04-14T03:46:09.047Z', + to: '2020-04-15T03:46:09.047Z', }, }, }, @@ -242,7 +242,7 @@ describe('StatefulTopN', () => { test(`provides 'from' via GlobalTime when rendering in a global context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.from).toEqual(0); + expect(props.from).toEqual('2020-07-07T08:20:18.966Z'); }); test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { @@ -260,7 +260,7 @@ describe('StatefulTopN', () => { test(`provides 'to' via GlobalTime when rendering in a global context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.to).toEqual(1); + expect(props.to).toEqual('2020-07-08T08:20:18.966Z'); }); }); @@ -298,7 +298,7 @@ describe('StatefulTopN', () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; expect(props.combinedQueries).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' ); }); @@ -323,7 +323,7 @@ describe('StatefulTopN', () => { test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.from).toEqual(1586835969047); + expect(props.from).toEqual('2020-04-14T03:46:09.047Z'); }); test('provides an empty query when rendering in a timeline context', () => { @@ -341,7 +341,7 @@ describe('StatefulTopN', () => { test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - expect(props.to).toEqual(1586922369047); + expect(props.to).toEqual('2020-04-15T03:46:09.047Z'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index b1979c501c7786..e5a1fb61202850 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -114,14 +114,14 @@ describe('TopN', () => { defaultView="raw" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={defaultOptions} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={toggleTopN} value={value} /> @@ -153,14 +153,14 @@ describe('TopN', () => { defaultView="raw" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={defaultOptions} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={toggleTopN} value={value} /> @@ -191,14 +191,14 @@ describe('TopN', () => { defaultView="alert" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={defaultOptions} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={toggleTopN} value={value} /> @@ -228,14 +228,14 @@ describe('TopN', () => { defaultView="all" field={field} filters={[]} - from={1586824307695} + from={'2020-04-14T00:31:47.695Z'} indexPattern={mockIndexPattern} options={allEvents} query={query} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget="global" setQuery={jest.fn()} - to={1586910707695} + to={'2020-04-15T00:31:47.695Z'} toggleTopN={jest.fn()} value={value} /> diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 5e2fd998224c63..064241a7216f46 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -54,8 +54,8 @@ export interface Props extends Pick; setAbsoluteRangeDatePickerTarget: InputsModelId; timelineId?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts b/x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.ts new file mode 100644 index 00000000000000..37c839c2969d43 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/url_state/__mocks__/normalize_time_range.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const normalizeTimeRange = () => ({ + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index eeeaacc25a15ec..9d0d9e7b250a05 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -38,7 +38,7 @@ jest.mock('../../utils/route/use_route_spy', () => ({ jest.mock('../super_date_picker', () => ({ formatDate: (date: string) => { - return 11223344556677; + return '2020-01-01T00:00:00.000Z'; }, })); @@ -53,11 +53,14 @@ jest.mock('../../lib/kibana', () => ({ }, }, }), + KibanaServices: { + get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), + }, })); describe('UrlStateContainer', () => { afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe('handleInitialize', () => { describe('URL state updates redux', () => { @@ -75,19 +78,19 @@ describe('UrlStateContainer', () => { mount( useUrlStateHooks(args)} />); expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 11223344556677, + from: '2020-01-01T00:00:00.000Z', fromStr: 'now-1d/d', kind: 'relative', - to: 11223344556677, + to: '2020-01-01T00:00:00.000Z', toStr: 'now-1d/d', id: 'global', }); expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 11223344556677, + from: '2020-01-01T00:00:00.000Z', fromStr: 'now-15m', kind: 'relative', - to: 11223344556677, + to: '2020-01-01T00:00:00.000Z', toStr: 'now', id: 'timeline', }); @@ -104,16 +107,16 @@ describe('UrlStateContainer', () => { mount( useUrlStateHooks(args)} />); expect(mockSetAbsoluteRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 1556736012685, + from: '2019-05-01T18:40:12.685Z', kind: 'absolute', - to: 1556822416082, + to: '2019-05-02T18:40:16.082Z', id: 'global', }); expect(mockSetAbsoluteRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 1556736012685, + from: '2019-05-01T18:40:12.685Z', kind: 'absolute', - to: 1556822416082, + to: '2019-05-02T18:40:16.082Z', id: 'timeline', }); } @@ -157,7 +160,7 @@ describe('UrlStateContainer', () => { ).toEqual({ hash: '', pathname: examplePath, - search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, state: '', }); } @@ -195,10 +198,10 @@ describe('UrlStateContainer', () => { if (CONSTANTS.detectionsPage === page) { expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ - from: 11223344556677, + from: '2020-01-01T00:00:00.000Z', fromStr: 'now-1d/d', kind: 'relative', - to: 11223344556677, + to: '2020-01-01T00:00:00.000Z', toStr: 'now-1d/d', id: 'global', }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index f7502661da308b..723f2d235864fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -54,20 +54,20 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, linkTo: ['global'], @@ -83,7 +83,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", state: '', }); }); @@ -114,7 +114,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); }); @@ -147,7 +147,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)', + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)", state: '', }); }); @@ -176,7 +176,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: examplePath, search: - '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); } @@ -204,7 +204,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect( mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search ).toEqual( - '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); wrapper.setProps({ hookProps: updatedProps }); @@ -213,7 +213,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect( mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search ).toEqual( - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index ab03e2199474c6..6eccf52ec72da2 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -120,6 +120,7 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { const absoluteRange = normalizeTimeRange( get('timeline.timerange', timerangeStateData) ); + dispatch( inputsActions.setAbsoluteRangeDatePicker({ ...absoluteRange, @@ -127,10 +128,12 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { }) ); } + if (timelineType === 'relative') { const relativeRange = normalizeTimeRange( get('timeline.timerange', timerangeStateData) ); + dispatch( inputsActions.setRelativeRangeDatePicker({ ...relativeRange, @@ -145,6 +148,7 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { const absoluteRange = normalizeTimeRange( get('global.timerange', timerangeStateData) ); + dispatch( inputsActions.setAbsoluteRangeDatePicker({ ...absoluteRange, @@ -156,6 +160,7 @@ const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { const relativeRange = normalizeTimeRange( get('global.timerange', timerangeStateData) ); + dispatch( inputsActions.setRelativeRangeDatePicker({ ...relativeRange, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts index dcdadf0f340727..d0cd9a26850777 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.test.ts @@ -13,8 +13,32 @@ import { isRelativeTimeRange, } from '../../store/inputs/model'; +import { getTimeRangeSettings } from '../../utils/default_date_settings'; + +const getTimeRangeSettingsMock = getTimeRangeSettings as jest.Mock; + +jest.mock('../../utils/default_date_settings'); +jest.mock('@elastic/datemath', () => ({ + parse: (date: string) => { + if (date === 'now') { + return { toISOString: () => '2020-07-08T08:20:18.966Z' }; + } + + if (date === 'now-24h') { + return { toISOString: () => '2020-07-07T08:20:18.966Z' }; + } + }, +})); + +getTimeRangeSettingsMock.mockImplementation(() => ({ + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', + fromStr: 'now-24h', + toStr: 'now', +})); + describe('#normalizeTimeRange', () => { - test('Absolute time range returns empty strings as 0', () => { + test('Absolute time range returns defaults for empty strings', () => { const dateTimeRange: URLTimeRange = { kind: 'absolute', fromStr: undefined, @@ -25,30 +49,8 @@ describe('#normalizeTimeRange', () => { if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: 0, - to: 0, - fromStr: undefined, - toStr: undefined, - }; - expect(normalizeTimeRange(dateTimeRange)).toEqual(expected); - } else { - throw new Error('Was expecting date time range to be a AbsoluteTimeRange'); - } - }); - - test('Absolute time range returns string with empty spaces as 0', () => { - const dateTimeRange: URLTimeRange = { - kind: 'absolute', - fromStr: undefined, - toStr: undefined, - from: ' ', - to: ' ', - }; - if (isAbsoluteTimeRange(dateTimeRange)) { - const expected: AbsoluteTimeRange = { - kind: 'absolute', - from: 0, - to: 0, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: undefined, toStr: undefined, }; @@ -71,8 +73,8 @@ describe('#normalizeTimeRange', () => { if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: undefined, toStr: undefined, }; @@ -89,14 +91,14 @@ describe('#normalizeTimeRange', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), }; if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: undefined, toStr: undefined, }; @@ -113,14 +115,14 @@ describe('#normalizeTimeRange', () => { kind: 'absolute', fromStr: undefined, toStr: undefined, - from: `${from.valueOf()}`, - to: `${to.valueOf()}`, + from: `${from.toISOString()}`, + to: `${to.toISOString()}`, }; if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: undefined, toStr: undefined, }; @@ -130,7 +132,7 @@ describe('#normalizeTimeRange', () => { } }); - test('Absolute time range returns NaN with from and to when garbage is sent in', () => { + test('Absolute time range returns defaults when garbage is sent in', () => { const to = 'garbage'; const from = 'garbage'; const dateTimeRange: URLTimeRange = { @@ -143,8 +145,8 @@ describe('#normalizeTimeRange', () => { if (isAbsoluteTimeRange(dateTimeRange)) { const expected: AbsoluteTimeRange = { kind: 'absolute', - from: NaN, - to: NaN, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: undefined, toStr: undefined, }; @@ -154,7 +156,7 @@ describe('#normalizeTimeRange', () => { } }); - test('Relative time range returns empty strings as 0', () => { + test('Relative time range returns defaults fro empty strings', () => { const dateTimeRange: URLTimeRange = { kind: 'relative', fromStr: '', @@ -165,30 +167,8 @@ describe('#normalizeTimeRange', () => { if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: 0, - to: 0, - fromStr: '', - toStr: '', - }; - expect(normalizeTimeRange(dateTimeRange)).toEqual(expected); - } else { - throw new Error('Was expecting date time range to be a RelativeTimeRange'); - } - }); - - test('Relative time range returns string with empty spaces as 0', () => { - const dateTimeRange: URLTimeRange = { - kind: 'relative', - fromStr: '', - toStr: '', - from: ' ', - to: ' ', - }; - if (isRelativeTimeRange(dateTimeRange)) { - const expected: RelativeTimeRange = { - kind: 'relative', - from: 0, - to: 0, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: '', toStr: '', }; @@ -211,8 +191,8 @@ describe('#normalizeTimeRange', () => { if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: '', toStr: '', }; @@ -229,14 +209,14 @@ describe('#normalizeTimeRange', () => { kind: 'relative', fromStr: '', toStr: '', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), }; if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: '', toStr: '', }; @@ -253,14 +233,14 @@ describe('#normalizeTimeRange', () => { kind: 'relative', fromStr: '', toStr: '', - from: `${from.valueOf()}`, - to: `${to.valueOf()}`, + from: `${from.toISOString()}`, + to: `${to.toISOString()}`, }; if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: from.valueOf(), - to: to.valueOf(), + from: from.toISOString(), + to: to.toISOString(), fromStr: '', toStr: '', }; @@ -270,7 +250,7 @@ describe('#normalizeTimeRange', () => { } }); - test('Relative time range returns NaN with from and to when garbage is sent in', () => { + test('Relative time range returns defaults when garbage is sent in', () => { const to = 'garbage'; const from = 'garbage'; const dateTimeRange: URLTimeRange = { @@ -283,8 +263,8 @@ describe('#normalizeTimeRange', () => { if (isRelativeTimeRange(dateTimeRange)) { const expected: RelativeTimeRange = { kind: 'relative', - from: NaN, - to: NaN, + from: '2020-07-04T08:20:18.966Z', + to: '2020-07-05T08:20:18.966Z', fromStr: '', toStr: '', }; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts index 851f89dcd2a5a6..6dc0949665530c 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/normalize_time_range.ts @@ -5,13 +5,20 @@ */ import { URLTimeRange } from '../../store/inputs/model'; +import { getTimeRangeSettings } from '../../utils/default_date_settings'; import { getMaybeDate } from '../formatted_date/maybe_date'; -export const normalizeTimeRange = (dateRange: T): T => { +export const normalizeTimeRange = < + T extends URLTimeRange | { to: string | number; from: string | number } +>( + dateRange: T, + uiSettings = true +): T => { const maybeTo = getMaybeDate(dateRange.to); const maybeFrom = getMaybeDate(dateRange.from); - const to: number = maybeTo.isValid() ? maybeTo.valueOf() : Number(dateRange.to); - const from: number = maybeFrom.isValid() ? maybeFrom.valueOf() : Number(dateRange.from); + const { to: benchTo, from: benchFrom } = getTimeRangeSettings(uiSettings); + const to: string = maybeTo.isValid() ? maybeTo.toISOString() : benchTo; + const from: string = maybeFrom.isValid() ? maybeFrom.toISOString() : benchFrom; return { ...dateRange, to, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index dec1672b076eb5..8d471e843320c2 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -92,20 +92,20 @@ export const defaultProps: UrlStateContainerPropTypes = { [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['timeline'], }, timeline: { [CONSTANTS.timerange]: { - from: 1558048243696, + from: '2019-05-16T23:10:43.696Z', fromStr: 'now-24h', kind: 'relative', - to: 1558134643697, + to: '2019-05-17T23:10:43.697Z', toStr: 'now', }, linkTo: ['global'], diff --git a/x-pack/plugins/security_solution/public/common/components/utils.ts b/x-pack/plugins/security_solution/public/common/components/utils.ts index ff022fd7d763d7..3620b09495eb6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/utils.ts @@ -20,7 +20,7 @@ export const getDaysDiff = (minDate: moment.Moment, maxDate: moment.Moment) => { return diff; }; -export const histogramDateTimeFormatter = (domain: [number, number] | null, fixedDiff?: number) => { +export const histogramDateTimeFormatter = (domain: [string, string] | null, fixedDiff?: number) => { const diff = fixedDiff ?? getDaysDiff(moment(domain![0]), moment(domain![1])); const format = niceTimeFormatByDay(diff); return timeFormatter(format); diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index 6050dafc0b1910..00b78c3a965504 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -19,6 +19,7 @@ import { useUiSetting$ } from '../../../lib/kibana'; import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; import { useApolloClient } from '../../../utils/apollo_context'; +import { useWithSource } from '../../source'; export interface LastEventTimeArgs { id: string; @@ -44,6 +45,8 @@ export function useLastEventTimeQuery( const [currentIndexKey, updateCurrentIndexKey] = useState(null); const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const apolloClient = useApolloClient(); + const { docValueFields } = useWithSource(sourceId); + async function fetchLastEventTime(signal: AbortSignal) { updateLoading(true); if (apolloClient) { @@ -52,6 +55,7 @@ export function useLastEventTimeQuery( query: LastEventTimeGqlQuery, fetchPolicy: 'cache-first', variables: { + docValueFields, sourceId, indexKey, details, diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts index 049c73b607b7e5..36305ef0dc8820 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/last_event_time.gql_query.ts @@ -12,10 +12,16 @@ export const LastEventTimeGqlQuery = gql` $indexKey: LastEventIndexKey! $details: LastTimeDetails! $defaultIndex: [String!]! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id - LastEventTime(indexKey: $indexKey, details: $details, defaultIndex: $defaultIndex) { + LastEventTime( + indexKey: $indexKey + details: $details + defaultIndex: $defaultIndex + docValueFields: $docValueFields + ) { lastSeen } } diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts index 938473f92782a7..bdeb1db4e1b284 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/mock.ts @@ -44,6 +44,7 @@ export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ indexKey: LastEventIndexKey.hosts, details: {}, defaultIndex: DEFAULT_INDEX_PATTERN, + docValueFields: [], }, }, result: { diff --git a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx new file mode 100644 index 00000000000000..f2545c1642d493 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { inputsModel, inputsSelectors, State } from '../../store'; +import { inputsActions } from '../../store/actions'; + +interface SetQuery { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch | inputsModel.RefetchKql; +} + +export interface GlobalTimeArgs { + from: string; + to: string; + setQuery: ({ id, inspect, loading, refetch }: SetQuery) => void; + deleteQuery?: ({ id }: { id: string }) => void; + isInitializing: boolean; +} + +interface OwnProps { + children: (args: GlobalTimeArgs) => React.ReactNode; +} + +type GlobalTimeProps = OwnProps & PropsFromRedux; + +export const GlobalTimeComponent: React.FC = ({ + children, + deleteAllQuery, + deleteOneQuery, + from, + to, + setGlobalQuery, +}) => { + const [isInitializing, setIsInitializing] = useState(true); + + const setQuery = useCallback( + ({ id, inspect, loading, refetch }: SetQuery) => + setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), + [setGlobalQuery] + ); + + const deleteQuery = useCallback( + ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + [deleteOneQuery] + ); + + useEffect(() => { + if (isInitializing) { + setIsInitializing(false); + } + return () => { + deleteAllQuery({ id: 'global' }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + {children({ + isInitializing, + from, + to, + setQuery, + deleteQuery, + })} + + ); +}; + +const mapStateToProps = (state: State) => { + const timerange: inputsModel.TimeRange = inputsSelectors.globalTimeRangeSelector(state); + return { + from: timerange.from, + to: timerange.to, + }; +}; + +const mapDispatchToProps = { + deleteAllQuery: inputsActions.deleteAllQuery, + deleteOneQuery: inputsActions.deleteOneQuery, + setGlobalQuery: inputsActions.setQuery, +}; + +export const connector = connect(mapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const GlobalTime = connector(React.memo(GlobalTimeComponent)); + +GlobalTime.displayName = 'GlobalTime'; diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx index cb988d7ebf1901..6e780e6b06b521 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.tsx @@ -61,13 +61,13 @@ describe('useQuery', () => { }); const TestComponent = () => { result = useQuery({ - endDate: 100, + endDate: '2020-07-07T08:20:00.000Z', errorMessage: 'fakeErrorMsg', filterQuery: '', histogramType: HistogramType.alerts, isInspected: false, stackByField: 'fakeField', - startDate: 0, + startDate: '2020-07-07T08:08:00.000Z', }); return
; @@ -85,8 +85,8 @@ describe('useQuery', () => { sourceId: 'default', timerange: { interval: '12h', - from: 0, - to: 100, + from: '2020-07-07T08:08:00.000Z', + to: '2020-07-07T08:20:00.000Z', }, defaultIndex: 'mockDefaultIndex', inspect: false, @@ -123,13 +123,13 @@ describe('useQuery', () => { }); const TestComponent = () => { result = useQuery({ - endDate: 100, + endDate: '2020-07-07T08:20:18.966Z', errorMessage: 'fakeErrorMsg', filterQuery: '', histogramType: HistogramType.alerts, isInspected: false, stackByField: 'fakeField', - startDate: 0, + startDate: '2020-07-08T08:20:18.966Z', }); return
; diff --git a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx index fdc95c1dadfe16..eaa43c255a944a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx @@ -9,14 +9,18 @@ import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; import { ESQuery } from '../../../common/typed_json'; +import { DocValueFields } from './source'; + +export { DocValueFields }; export interface QueryTemplateProps { + docValueFields?: DocValueFields[]; id?: string; - endDate?: number; + endDate?: string; filterQuery?: ESQuery | string; skip?: boolean; sourceId: string; - startDate?: number; + startDate?: string; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FetchMoreOptionsArgs = FetchMoreQueryOptions & diff --git a/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx b/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx index 446e1125b2807e..f40ae4d31c586e 100644 --- a/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/query_template_paginated.tsx @@ -13,14 +13,18 @@ import deepEqual from 'fast-deep-equal'; import { ESQuery } from '../../../common/typed_json'; import { inputsModel } from '../store/model'; import { generateTablePaginationOptions } from '../components/paginated_table/helpers'; +import { DocValueFields } from './source'; + +export { DocValueFields }; export interface QueryTemplatePaginatedProps { + docValueFields?: DocValueFields[]; id?: string; - endDate?: number; + endDate?: string; filterQuery?: ESQuery | string; skip?: boolean; sourceId: string; - startDate?: number; + startDate?: string; } // eslint-disable-next-line @typescript-eslint/no-explicit-any type FetchMoreOptionsArgs = FetchMoreQueryOptions & diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index bfde17723aef48..03ad6ad3396f89 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -25,6 +25,7 @@ describe('Index Fields & Browser Fields', () => { return expect(initialResult).toEqual({ browserFields: {}, + docValueFields: [], errorMessage: null, indexPattern: { fields: [], @@ -56,6 +57,16 @@ describe('Index Fields & Browser Fields', () => { current: { indicesExist: true, browserFields: mockBrowserFields, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], indexPattern: { fields: mockIndexFields, title: 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 4f42f20c45ae16..9b7dfe84277c6e 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 @@ -33,6 +33,11 @@ export interface BrowserField { type: string; } +export interface DocValueFields { + field: string; + format: string; +} + export type BrowserFields = Readonly>>; export const getAllBrowserFields = (browserFields: BrowserFields): Array> => @@ -75,14 +80,38 @@ export const getBrowserFields = memoizeOne( (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); +export const getdocValueFields = memoizeOne( + (_title: string, fields: IndexField[]): DocValueFields[] => + fields && fields.length > 0 + ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { + if (field.type === 'date' && accumulator.length < 100) { + const format: string = + field.format != null && !isEmpty(field.format) ? field.format : 'date_time'; + return [ + ...accumulator, + { + field: field.name, + format, + }, + ]; + } + return accumulator; + }, []) + : [], + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] +); + export const indicesExistOrDataTemporarilyUnavailable = ( indicesExist: boolean | null | undefined ) => indicesExist || isUndefined(indicesExist); const EMPTY_BROWSER_FIELDS = {}; +const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; interface UseWithSourceState { browserFields: BrowserFields; + docValueFields: DocValueFields[]; errorMessage: string | null; indexPattern: IIndexPattern; indicesExist: boolean | undefined | null; @@ -104,6 +133,7 @@ export const useWithSource = ( const [state, setState] = useState({ browserFields: EMPTY_BROWSER_FIELDS, + docValueFields: EMPTY_DOCVALUE_FIELD, errorMessage: null, indexPattern: getIndexFields(defaultIndex.join(), []), indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), @@ -146,6 +176,10 @@ export const useWithSource = ( defaultIndex.join(), get('data.source.status.indexFields', result) ), + docValueFields: getdocValueFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), indexPattern: getIndexFields( defaultIndex.join(), get('data.source.status.indexFields', result) diff --git a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts index 55e8b6ac02b128..bba6a15d739709 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts +++ b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts @@ -6,7 +6,7 @@ import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { BrowserFields } from '.'; +import { BrowserFields, DocValueFields } from '.'; import { sourceQuery } from './index.gql_query'; export const mocksSource = [ @@ -697,3 +697,14 @@ export const mockBrowserFields: BrowserFields = { }, }, }; + +export const mockDocValueFields: DocValueFields[] = [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 89f100992e1b9f..2849e8ffabd36e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -156,7 +156,13 @@ export const mockGlobalState: State = { }, inputs: { global: { - timerange: { kind: 'relative', fromStr: DEFAULT_FROM, toStr: DEFAULT_TO, from: 0, to: 1 }, + timerange: { + kind: 'relative', + fromStr: DEFAULT_FROM, + toStr: DEFAULT_TO, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }, linkTo: ['timeline'], queries: [], policy: { kind: DEFAULT_INTERVAL_TYPE, duration: DEFAULT_INTERVAL_VALUE }, @@ -167,7 +173,13 @@ export const mockGlobalState: State = { filters: [], }, timeline: { - timerange: { kind: 'relative', fromStr: DEFAULT_FROM, toStr: DEFAULT_TO, from: 0, to: 1 }, + timerange: { + kind: 'relative', + fromStr: DEFAULT_FROM, + toStr: DEFAULT_TO, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }, linkTo: ['global'], queries: [], policy: { kind: DEFAULT_INTERVAL_TYPE, duration: DEFAULT_INTERVAL_VALUE }, @@ -211,8 +223,8 @@ export const mockGlobalState: State = { templateTimelineVersion: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: false, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index b1df41a19aebe4..a415ab75f13ea5 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2091,8 +2091,8 @@ export const mockTimelineModel: TimelineModel = { ], dataProviders: [], dateRange: { - end: 1584539558929, - start: 1584539198929, + end: '2020-03-18T13:52:38.929Z', + start: '2020-03-18T13:46:38.929Z', }, deletedEventIds: [], description: 'This is a sample rule description', @@ -2154,7 +2154,7 @@ export const mockTimelineModel: TimelineModel = { export const mockTimelineResult: TimelineResult = { savedObjectId: 'ef579e40-jibber-jabber', columns: timelineDefaults.columns.filter((column) => column.id !== 'event.action'), - dateRange: { start: 1584539198929, end: 1584539558929 }, + dateRange: { start: '2020-03-18T13:46:38.929Z', end: '2020-03-18T13:52:38.929Z' }, description: 'This is a sample rule description', eventType: 'all', filters: [ @@ -2188,7 +2188,7 @@ export const mockTimelineApolloResult = { }; export const defaultTimelineProps: CreateTimelineProps = { - from: 1541444305937, + from: '2018-11-05T18:58:25.937Z', timeline: { columns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', width: 190 }, @@ -2212,7 +2212,7 @@ export const defaultTimelineProps: CreateTimelineProps = { queryMatch: { field: '_id', operator: ':', value: '1' }, }, ], - dateRange: { end: 1541444605937, start: 1541444305937 }, + dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' }, deletedEventIds: [], description: '', eventIdToNoteIds: {}, @@ -2251,6 +2251,6 @@ export const defaultTimelineProps: CreateTimelineProps = { version: null, width: 1100, }, - to: 1541444605937, + to: '2018-11-05T19:03:25.937Z', ruleNote: '# this is some markdown documentation', }; diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts index f8b8d0865d1203..efad0638b2971b 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts @@ -14,21 +14,21 @@ const actionCreator = actionCreatorFactory('x-pack/security_solution/local/input export const setAbsoluteRangeDatePicker = actionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>('SET_ABSOLUTE_RANGE_DATE_PICKER'); export const setTimelineRangeDatePicker = actionCreator<{ - from: number; - to: number; + from: string; + to: string; }>('SET_TIMELINE_RANGE_DATE_PICKER'); export const setRelativeRangeDatePicker = actionCreator<{ id: InputsModelId; fromStr: string; toStr: string; - from: number; - to: number; + from: string; + to: string; }>('SET_RELATIVE_RANGE_DATE_PICKER'); export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts b/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts index d23110b44ad434..b54d8ca20b0d1a 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/helpers.test.ts @@ -53,8 +53,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-48h', toStr: 'now', - from: 23, - to: 26, + from: '2020-07-06T08:00:00.000Z', + to: '2020-07-08T08:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('global', newTimerange, state); expect(newState.timeline.timerange).toEqual(newState.global.timerange); @@ -65,8 +65,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-68h', toStr: 'NOTnow', - from: 29, - to: 33, + from: '2020-07-05T22:00:00.000Z', + to: '2020-07-08T18:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('timeline', newTimerange, state); expect(newState.timeline.timerange).toEqual(newState.global.timerange); @@ -83,8 +83,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-48h', toStr: 'now', - from: 23, - to: 26, + from: '2020-07-06T08:00:00.000Z', + to: '2020-07-08T08:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('global', newTimerange, state); expect(newState.timeline.timerange).toEqual(state.timeline.timerange); @@ -96,8 +96,8 @@ describe('Inputs', () => { kind: 'relative', fromStr: 'now-68h', toStr: 'NOTnow', - from: 29, - to: 33, + from: '2020-07-05T22:00:00.000Z', + to: '2020-07-08T18:00:00.000Z', }; const newState: InputsModel = updateInputTimerange('timeline', newTimerange, state); expect(newState.timeline.timerange).toEqual(newTimerange); @@ -274,10 +274,10 @@ describe('Inputs', () => { }, ], timerange: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, query: { query: '', language: 'kuery' }, @@ -291,10 +291,10 @@ describe('Inputs', () => { }, queries: [], timerange: { - from: 0, + from: '2020-07-07T08:20:18.966Z', fromStr: 'now-24h', kind: 'relative', - to: 1, + to: '2020-07-08T08:20:18.966Z', toStr: 'now', }, query: { query: '', language: 'kuery' }, diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts index e851caf523eb4b..358124405c1469 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts @@ -13,16 +13,16 @@ export interface AbsoluteTimeRange { kind: 'absolute'; fromStr: undefined; toStr: undefined; - from: number; - to: number; + from: string; + to: string; } export interface RelativeTimeRange { kind: 'relative'; fromStr: string; toStr: string; - from: number; - to: number; + from: string; + to: string; } export const isRelativeTimeRange = ( @@ -35,10 +35,7 @@ export const isAbsoluteTimeRange = ( export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; -export type URLTimeRange = Omit & { - from: string | TimeRange['from']; - to: string | TimeRange['to']; -}; +export type URLTimeRange = TimeRange; export interface Policy { kind: 'manual' | 'interval'; diff --git a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts index 9fc5490b16cab3..c0e009c46a6b66 100644 --- a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts +++ b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.test.ts @@ -217,38 +217,38 @@ describe('getTimeRangeSettings', () => { test('should return DEFAULT_FROM', () => { mockTimeRange(); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return a custom from range', () => { const mockFrom = '2019-08-30T17:49:18.396Z'; mockTimeRange({ from: mockFrom }); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(mockFrom).valueOf()); + expect(from).toBe(new Date(mockFrom).toISOString()); }); test('should return the DEFAULT_FROM when the whole object is null', () => { mockTimeRange(null); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the whole object is undefined', () => { mockTimeRange(null); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the from value is null', () => { mockTimeRange({ from: null }); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the from value is undefined', () => { mockTimeRange({ from: undefined }); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); test('should return the DEFAULT_FROM when the from value is malformed', () => { @@ -256,7 +256,7 @@ describe('getTimeRangeSettings', () => { if (isMalformedTimeRange(malformedTimeRange)) { mockTimeRange(malformedTimeRange); const { from } = getTimeRangeSettings(); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); } else { throw Error('Was expecting an object to be used for the malformed time range'); } @@ -271,7 +271,7 @@ describe('getTimeRangeSettings', () => { it('is DEFAULT_FROM in epoch', () => { const { from } = getTimeRangeSettings(false); - expect(from).toBe(new Date(DEFAULT_FROM_DATE).valueOf()); + expect(from).toBe(new Date(DEFAULT_FROM_DATE).toISOString()); }); }); }); @@ -280,38 +280,38 @@ describe('getTimeRangeSettings', () => { test('should return DEFAULT_TO', () => { mockTimeRange(); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return a custom from range', () => { const mockTo = '2000-08-30T17:49:18.396Z'; mockTimeRange({ to: mockTo }); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(mockTo).valueOf()); + expect(to).toBe(new Date(mockTo).toISOString()); }); test('should return the DEFAULT_TO_DATE when the whole object is null', () => { mockTimeRange(null); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the whole object is undefined', () => { mockTimeRange(null); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the from value is null', () => { mockTimeRange({ from: null }); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the from value is undefined', () => { mockTimeRange({ from: undefined }); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); test('should return the DEFAULT_TO_DATE when the from value is malformed', () => { @@ -319,7 +319,7 @@ describe('getTimeRangeSettings', () => { if (isMalformedTimeRange(malformedTimeRange)) { mockTimeRange(malformedTimeRange); const { to } = getTimeRangeSettings(); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); } else { throw Error('Was expecting an object to be used for the malformed time range'); } @@ -334,7 +334,7 @@ describe('getTimeRangeSettings', () => { it('is DEFAULT_TO in epoch', () => { const { to } = getTimeRangeSettings(false); - expect(to).toBe(new Date(DEFAULT_TO_DATE).valueOf()); + expect(to).toBe(new Date(DEFAULT_TO_DATE).toISOString()); }); }); }); @@ -498,12 +498,12 @@ describe('getIntervalSettings', () => { '1930-05-31T13:03:54.234Z', moment('1950-05-31T13:03:54.234Z') ); - expect(value.valueOf()).toBe(new Date('1930-05-31T13:03:54.234Z').valueOf()); + expect(value.toISOString()).toBe(new Date('1930-05-31T13:03:54.234Z').toISOString()); }); test('should return the second value if the first is a bad string', () => { const value = parseDateWithDefault('trashed string', moment('1950-05-31T13:03:54.234Z')); - expect(value.valueOf()).toBe(new Date('1950-05-31T13:03:54.234Z').valueOf()); + expect(value.toISOString()).toBe(new Date('1950-05-31T13:03:54.234Z').toISOString()); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts index b8b4b23e20b859..148143bb00bead 100644 --- a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts +++ b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts @@ -49,8 +49,8 @@ export const getTimeRangeSettings = (uiSettings = true) => { const fromStr = (isString(timeRange?.from) && timeRange?.from) || DEFAULT_FROM; const toStr = (isString(timeRange?.to) && timeRange?.to) || DEFAULT_TO; - const from = parseDateWithDefault(fromStr, DEFAULT_FROM_MOMENT).valueOf(); - const to = parseDateWithDefault(toStr, DEFAULT_TO_MOMENT).valueOf(); + const from = parseDateWithDefault(fromStr, DEFAULT_FROM_MOMENT).toISOString(); + const to = parseDateWithDefault(toStr, DEFAULT_TO_MOMENT).toISOString(); return { from, fromStr, to, toStr }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx index 7f340b0bea37bd..09883e342f998e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx @@ -18,8 +18,8 @@ describe('AlertsHistogram', () => { legendItems={[]} loading={false} data={[]} - from={0} - to={1} + from={'2020-07-07T08:20:18.966Z'} + to={'2020-07-08T08:20:18.966Z'} updateDateRange={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx index 11dcbfa39d574e..ffd7f7918ec72b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx @@ -26,11 +26,11 @@ const DEFAULT_CHART_HEIGHT = 174; interface AlertsHistogramProps { chartHeight?: number; - from: number; + from: string; legendItems: LegendItem[]; legendPosition?: Position; loading: boolean; - to: number; + to: string; data: HistogramData[]; updateDateRange: UpdateDateRange; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx index 9d124201f022e1..0cbed86f18768b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { showAllOthersBucket } from '../../../../common/constants'; import { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; @@ -28,8 +29,8 @@ export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggre export const getAlertsHistogramQuery = ( stackByField: string, - from: number, - to: number, + from: string, + to: string, additionalFilters: Array<{ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> @@ -55,7 +56,7 @@ export const getAlertsHistogramQuery = ( alerts: { date_histogram: { field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, + fixed_interval: `${Math.floor(moment(to).diff(moment(from)) / 32)}ms`, min_doc_count: 0, extended_bounds: { min: from, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 59d97480418b79..4cbfa59aac5826 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -40,10 +40,10 @@ jest.mock('../../../common/components/navigation/use_get_url_search'); describe('AlertsHistogramPanel', () => { const defaultProps = { - from: 0, + from: '2020-07-07T08:20:18.966Z', signalIndexName: 'signalIndexName', setQuery: jest.fn(), - to: 1, + to: '2020-07-08T08:20:18.966Z', updateDateRange: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 24bfeaa4dae1a6..16d1a1481bc968 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -70,7 +70,7 @@ describe('alert actions', () => { updateTimelineIsLoading, }); const expected = { - from: 1541444305937, + from: '2018-11-05T18:58:25.937Z', timeline: { columns: [ { @@ -153,8 +153,8 @@ describe('alert actions', () => { ], dataProviders: [], dateRange: { - end: 1541444605937, - start: 1541444305937, + end: '2018-11-05T19:03:25.937Z', + start: '2018-11-05T18:58:25.937Z', }, deletedEventIds: [], description: 'This is a sample rule description', @@ -225,7 +225,7 @@ describe('alert actions', () => { version: null, width: 1100, }, - to: 1541444605937, + to: '2018-11-05T19:03:25.937Z', ruleNote: '# this is some markdown documentation', }; @@ -375,8 +375,8 @@ describe('alert actions', () => { }; const result = determineToAndFrom({ ecsData: ecsDataMock }); - expect(result.from).toEqual(1584726886349); - expect(result.to).toEqual(1584727186349); + expect(result.from).toEqual('2020-03-20T17:54:46.349Z'); + expect(result.to).toEqual('2020-03-20T17:59:46.349Z'); }); test('it uses current time timestamp if ecsData.timestamp is not provided', () => { @@ -385,8 +385,8 @@ describe('alert actions', () => { }; const result = determineToAndFrom({ ecsData: ecsDataMock }); - expect(result.from).toEqual(1583085286349); - expect(result.to).toEqual(1583085586349); + expect(result.from).toEqual('2020-03-01T17:54:46.349Z'); + expect(result.to).toEqual('2020-03-01T17:59:46.349Z'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 11c13c2358e940..7bebc9efbee157 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -97,8 +97,8 @@ export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { const from = moment(ecsData.timestamp ?? new Date()) .subtract(ellapsedTimeRule) - .valueOf(); - const to = moment(ecsData.timestamp ?? new Date()).valueOf(); + .toISOString(); + const to = moment(ecsData.timestamp ?? new Date()).toISOString(); return { to, from }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index f99a0256c0b3fe..563f2ea60cded5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -19,10 +19,10 @@ describe('AlertsTableComponent', () => { timelineId={TimelineId.test} canUserCRUD hasIndexWrite - from={0} + from={'2020-07-07T08:20:18.966Z'} loading signalsIndex="index" - to={1} + to={'2020-07-08T08:20:18.966Z'} globalQuery={{ query: 'query', language: 'language', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index b9b963a84e966f..391598ebda03d6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -62,10 +62,10 @@ interface OwnProps { canUserCRUD: boolean; defaultFilters?: Filter[]; hasIndexWrite: boolean; - from: number; + from: string; loading: boolean; signalsIndex: string; - to: number; + to: string; } type AlertsTableComponentProps = OwnProps & PropsFromRedux; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 34d18b4dedba6e..ebf1a6d3ed533e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -60,9 +60,9 @@ export interface SendAlertToTimelineActionProps { export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; export interface CreateTimelineProps { - from: number; + from: string; timeline: TimelineModel; - to: number; + to: string; ruleNote?: string; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 0204a2980b9fc7..d36c19a6a35c66 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -374,6 +374,16 @@ describe('useFetchIndexPatterns', () => { 'winlogbeat-*', ], indicesExists: true, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], indexPatterns: { fields: [ { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, @@ -441,6 +451,7 @@ describe('useFetchIndexPatterns', () => { expect(result.current).toEqual([ { browserFields: {}, + docValueFields: [], indexPatterns: { fields: [], title: '', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx index 640d6f9a17fd13..ab12f045cddbca 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -12,8 +12,10 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, + getdocValueFields, getIndexFields, sourceQuery, + DocValueFields, } from '../../../../common/containers/source'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { SourceQuery } from '../../../../graphql/types'; @@ -23,6 +25,7 @@ import * as i18n from './translations'; interface FetchIndexPatternReturn { browserFields: BrowserFields; + docValueFields: DocValueFields[]; isLoading: boolean; indices: string[]; indicesExists: boolean; @@ -31,18 +34,29 @@ interface FetchIndexPatternReturn { export type Return = [FetchIndexPatternReturn, Dispatch>]; +const DEFAULT_BROWSER_FIELDS = {}; +const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; +const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = []; + export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { const apolloClient = useApolloClient(); const [indices, setIndices] = useState(defaultIndices); - const [indicesExists, setIndicesExists] = useState(false); - const [indexPatterns, setIndexPatterns] = useState({ fields: [], title: '' }); - const [browserFields, setBrowserFields] = useState({}); - const [isLoading, setIsLoading] = useState(false); + + const [state, setState] = useState({ + browserFields: DEFAULT_BROWSER_FIELDS, + docValueFields: DEFAULT_DOC_VALUE_FIELDS, + indices: defaultIndices, + indicesExists: false, + indexPatterns: DEFAULT_INDEX_PATTERNS, + isLoading: false, + }); + const [, dispatchToaster] = useStateToaster(); useEffect(() => { if (!deepEqual(defaultIndices, indices)) { setIndices(defaultIndices); + setState((prevState) => ({ ...prevState, indices: defaultIndices })); } }, [defaultIndices, indices]); @@ -52,7 +66,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => async function fetchIndexPatterns() { if (apolloClient && !isEmpty(indices)) { - setIsLoading(true); + setState((prevState) => ({ ...prevState, isLoading: true })); apolloClient .query({ query: sourceQuery, @@ -70,19 +84,28 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => .then( (result) => { if (isSubscribed) { - setIsLoading(false); - setIndicesExists(get('data.source.status.indicesExist', result)); - setIndexPatterns( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); + setState({ + browserFields: getBrowserFields( + indices.join(), + get('data.source.status.indexFields', result) + ), + docValueFields: getdocValueFields( + indices.join(), + get('data.source.status.indexFields', result) + ), + indices, + isLoading: false, + indicesExists: get('data.source.status.indicesExist', result), + indexPatterns: getIndexFields( + indices.join(), + get('data.source.status.indexFields', result) + ), + }); } }, (error) => { if (isSubscribed) { - setIsLoading(false); + setState((prevState) => ({ ...prevState, isLoading: false })); errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); } } @@ -97,5 +120,5 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => // eslint-disable-next-line react-hooks/exhaustive-deps }, [indices]); - return [{ browserFields, isLoading, indices, indicesExists, indexPatterns }, setIndices]; + return [state, setIndices]; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index d5aa57ddd87547..f4004a66c8f80c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -19,9 +19,12 @@ jest.mock('../../components/user_info'); jest.mock('../../../common/containers/source'); jest.mock('../../../common/components/link_to'); jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 84cfc744312f91..cdff8ea4ab928a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -70,7 +70,11 @@ export const DetectionEnginePageComponent: React.FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 0a42602e5fbb28..f4b112d4652605 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -20,9 +20,12 @@ jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('../../../../../common/containers/source'); jest.mock('../../../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index c74a2a3cf993a4..45a1c89cec621c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -236,7 +236,11 @@ export const RuleDetailsPageComponent: FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 4716440c36e61a..4e91324ecc9ffe 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -735,6 +735,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -816,6 +838,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -867,6 +911,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -924,6 +990,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -1001,6 +1089,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -1105,6 +1215,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -1158,6 +1290,28 @@ } }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { "kind": "OBJECT", "name": "IpOverviewData", "ofType": null }, @@ -1817,6 +1971,28 @@ "description": "", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null + }, + { + "name": "docValueFields", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "ofType": null + } + } + } + }, + "defaultValue": null } ], "type": { @@ -2522,7 +2698,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "defaultValue": null }, @@ -2532,7 +2708,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "defaultValue": null } @@ -2592,6 +2768,37 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "docValueFieldsInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "field", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "format", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "AuthenticationsData", @@ -10219,7 +10426,7 @@ "name": "start", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -10227,7 +10434,7 @@ "name": "end", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -11705,13 +11912,13 @@ { "name": "start", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "defaultValue": null }, { "name": "end", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "defaultValue": null } ], diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 98addf3317ff4e..5f8595df23f9bb 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -24,9 +24,9 @@ export interface TimerangeInput { /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ interval: string; /** The end of the timerange */ - to: number; + to: string; /** The beginning of the timerange */ - from: number; + from: string; } export interface PaginationInputPaginated { @@ -40,6 +40,12 @@ export interface PaginationInputPaginated { querySize: number; } +export interface DocValueFieldsInput { + field: string; + + format: string; +} + export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -260,9 +266,9 @@ export interface KueryFilterQueryInput { } export interface DateRangePickerInput { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface SortTimelineInput { @@ -2093,9 +2099,9 @@ export interface QueryMatchResult { } export interface DateRangePickerResult { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface FavoriteTimelineResult { @@ -2332,6 +2338,8 @@ export interface AuthenticationsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2345,6 +2353,8 @@ export interface TimelineSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineDetailsSourceArgs { eventId: string; @@ -2352,6 +2362,8 @@ export interface TimelineDetailsSourceArgs { indexName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface LastEventTimeSourceArgs { id?: Maybe; @@ -2361,6 +2373,8 @@ export interface LastEventTimeSourceArgs { details: LastTimeDetails; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostsSourceArgs { id?: Maybe; @@ -2374,6 +2388,8 @@ export interface HostsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostOverviewSourceArgs { id?: Maybe; @@ -2390,6 +2406,8 @@ export interface HostFirstLastSeenSourceArgs { hostName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface IpOverviewSourceArgs { id?: Maybe; @@ -2399,6 +2417,8 @@ export interface IpOverviewSourceArgs { ip: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface UsersSourceArgs { filterQuery?: Maybe; @@ -2514,6 +2534,8 @@ export interface NetworkDnsHistogramSourceArgs { timerange: TimerangeInput; stackByField?: Maybe; + + docValueFields: DocValueFieldsInput[]; } export interface NetworkHttpSourceArgs { id?: Maybe; @@ -2632,6 +2654,7 @@ export namespace GetLastEventTimeQuery { indexKey: LastEventIndexKey; details: LastTimeDetails; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -2768,6 +2791,7 @@ export namespace GetAuthenticationsQuery { filterQuery?: Maybe; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -2904,6 +2928,7 @@ export namespace GetHostFirstLastSeenQuery { sourceId: string; hostName: string; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -2938,6 +2963,7 @@ export namespace GetHostsTableQuery { filterQuery?: Maybe; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -3379,6 +3405,7 @@ export namespace GetIpOverviewQuery { ip: string; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -4541,6 +4568,7 @@ export namespace GetTimelineDetailsQuery { eventId: string; indexName: string; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; }; export type Query = { @@ -4615,6 +4643,8 @@ export namespace GetTimelineQuery { filterQuery?: Maybe; defaultIndex: string[]; inspect: boolean; + docValueFields: DocValueFieldsInput[]; + timerange: TimerangeInput; }; export type Query = { @@ -5644,9 +5674,9 @@ export namespace GetOneTimeline { export type DateRange = { __typename?: 'DateRangePickerResult'; - start: Maybe; + start: Maybe; - end: Maybe; + end: Maybe; }; export type EventIdToNoteIds = { @@ -6030,9 +6060,9 @@ export namespace PersistTimelineMutation { export type DateRange = { __typename?: 'DateRangePickerResult'; - start: Maybe; + start: Maybe; - end: Maybe; + end: Maybe; }; export type Sort = { diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx index 09e253ae56747d..978bdcaa2bb019 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx @@ -14,8 +14,8 @@ import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; describe('kpiHostsComponent', () => { const ID = 'kpiHost'; - const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); - const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + const from = '2019-06-15T06:00:00.000Z'; + const to = '2019-06-18T06:00:00.000Z'; const narrowDateRange = () => {}; describe('render', () => { test('it should render spinner if it is loading', () => { diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index ba70df7d361d45..c39e86591013f9 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -21,10 +21,10 @@ import { UpdateDateRange } from '../../../common/components/charts/common'; const kpiWidgetHeight = 247; interface GenericKpiHostProps { - from: number; + from: string; id: string; loading: boolean; - to: number; + to: string; narrowDateRange: UpdateDateRange; } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts index eee35730cfdbbf..c68816b34c175b 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.gql_query.ts @@ -14,6 +14,7 @@ export const authenticationsQuery = gql` $filterQuery: String $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id @@ -22,6 +23,7 @@ export const authenticationsQuery = gql` pagination: $pagination filterQuery: $filterQuery defaultIndex: $defaultIndex + docValueFields: $docValueFields ) { totalCount edges { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index bfada0583f8e95..efd80c5c590ed6 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -63,6 +63,7 @@ class AuthenticationsComponentQuery extends QueryTemplatePaginated< const { activePage, children, + docValueFields, endDate, filterQuery, id = ID, @@ -84,6 +85,7 @@ class AuthenticationsComponentQuery extends QueryTemplatePaginated< filterQuery: createFilter(filterQuery), defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), inspect: isInspected, + docValueFields: docValueFields ?? [], }; return ( diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts index 7db4f138c77944..18cbcf516839f1 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts @@ -7,10 +7,19 @@ import gql from 'graphql-tag'; export const HostFirstLastSeenGqlQuery = gql` - query GetHostFirstLastSeenQuery($sourceId: ID!, $hostName: String!, $defaultIndex: [String!]!) { + query GetHostFirstLastSeenQuery( + $sourceId: ID! + $hostName: String! + $defaultIndex: [String!]! + $docValueFields: [docValueFieldsInput!]! + ) { source(id: $sourceId) { id - HostFirstLastSeen(hostName: $hostName, defaultIndex: $defaultIndex) { + HostFirstLastSeen( + hostName: $hostName + defaultIndex: $defaultIndex + docValueFields: $docValueFields + ) { firstSeen lastSeen } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts index a4f8fca23e8aa2..65e379b5ba2d82 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts @@ -13,7 +13,7 @@ import { useUiSetting$ } from '../../../../common/lib/kibana'; import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; import { inputsModel } from '../../../../common/store'; import { QueryTemplateProps } from '../../../../common/containers/query_template'; - +import { useWithSource } from '../../../../common/containers/source'; import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; export interface FirstLastSeenHostArgs { @@ -40,6 +40,7 @@ export function useFirstLastSeenHostQuery( const [lastSeen, updateLastSeen] = useState(null); const [errorMessage, updateErrorMessage] = useState(null); const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const { docValueFields } = useWithSource(sourceId); async function fetchFirstLastSeenHost(signal: AbortSignal) { updateLoading(true); @@ -51,6 +52,7 @@ export function useFirstLastSeenHostQuery( sourceId, hostName, defaultIndex, + docValueFields, }, context: { fetchOptions: { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts index 51e484ffbd8599..7f1b3d97eb5255 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts @@ -35,6 +35,7 @@ export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ sourceId: 'default', hostName: 'kibana-siem', defaultIndex: DEFAULT_INDEX_PATTERN, + docValueFields: [], }, }, result: { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts index 672ea70b09ad21..e93f3e379b30e6 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query.ts @@ -15,6 +15,7 @@ export const HostsTableQuery = gql` $filterQuery: String $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id @@ -24,6 +25,7 @@ export const HostsTableQuery = gql` sort: $sort filterQuery: $filterQuery defaultIndex: $defaultIndex + docValueFields: $docValueFields ) { totalCount edges { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 70f21b6f23cc0d..8af24e6e6abc1f 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -33,7 +33,7 @@ import { generateTablePaginationOptions } from '../../../common/components/pagin const ID = 'hostsQuery'; export interface HostsArgs { - endDate: number; + endDate: string; hosts: HostsEdges[]; id: string; inspect: inputsModel.InspectQuery; @@ -42,15 +42,15 @@ export interface HostsArgs { loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; - startDate: number; + startDate: string; totalCount: number; } export interface OwnProps extends QueryTemplatePaginatedProps { children: (args: HostsArgs) => React.ReactNode; type: hostsModel.HostsType; - startDate: number; - endDate: number; + startDate: string; + endDate: string; } export interface HostsComponentReduxProps { @@ -81,6 +81,7 @@ class HostsComponentQuery extends QueryTemplatePaginated< public render() { const { activePage, + docValueFields, id = ID, isInspected, children, @@ -110,6 +111,7 @@ class HostsComponentQuery extends QueryTemplatePaginated< pagination: generateTablePaginationOptions(activePage, limit), filterQuery: createFilter(filterQuery), defaultIndex, + docValueFields: docValueFields ?? [], inspect: isInspected, }; return ( diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx index 5267fff3a26d67..12a82c7980b61b 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/index.tsx @@ -27,8 +27,8 @@ export interface HostOverviewArgs { hostOverview: HostItem; loading: boolean; refetch: inputsModel.Refetch; - startDate: number; - endDate: number; + startDate: string; + endDate: string; } export interface HostOverviewReduxProps { @@ -38,8 +38,8 @@ export interface HostOverviewReduxProps { export interface OwnProps extends QueryTemplateProps { children: (args: HostOverviewArgs) => React.ReactNode; hostName: string; - startDate: number; - endDate: number; + startDate: string; + endDate: string; } type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index cce48a1e605b26..08fe48c0dd709d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -17,14 +17,19 @@ import { type } from './utils'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); + jest.mock('../../../common/containers/source', () => ({ useWithSource: jest.fn().mockReturnValue({ indicesExist: true, indexPattern: mockIndexPattern }), })); jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); // Test will fail because we will to need to mock some core services to make the test work @@ -73,17 +78,17 @@ describe('body', () => { @@ -91,10 +96,10 @@ describe('body', () => { // match against everything but the functions to ensure they are there as expected expect(wrapper.find(componentName).props()).toMatchObject({ - endDate: 0, + endDate: '2020-07-08T08:20:18.966Z', filterQuery, skip: false, - startDate: 0, + startDate: '2020-07-07T08:20:18.966Z', type: 'details', indexPattern: { fields: [ diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index acde0cbe1d42b7..4d4eead0e778aa 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -28,6 +28,7 @@ import { export const HostDetailsTabs = React.memo( ({ + docValueFields, pageFilters, filterQuery, detailName, @@ -54,7 +55,11 @@ export const HostDetailsTabs = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); @@ -76,7 +81,7 @@ export const HostDetailsTabs = React.memo( return ( - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index bb0317f0482b03..447d003625c8f0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -73,11 +73,15 @@ const HostDetailsComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); - const { indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern } = useWithSource(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -175,6 +179,7 @@ const HostDetailsComponent = React.memo( ; detailName: string; hostDetailsPagePath: string; @@ -56,6 +57,7 @@ export type HostDetailsNavTab = Record; export type HostDetailsTabsProps = HostBodyComponentDispatchProps & HostsQueryProps & { + docValueFields?: DocValueFields[]; pageFilters?: Filter[]; filterQuery: string; indexPattern: IIndexPattern; @@ -64,6 +66,6 @@ export type HostDetailsTabsProps = HostBodyComponentDispatchProps & export type SetAbsoluteRangeDatePicker = ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index a2f83bf0965f36..b37d91cc2be3b0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -62,11 +62,15 @@ export const HostsComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); - const { indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern } = useWithSource(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -125,6 +129,7 @@ export const HostsComponent = React.memo( ( ({ deleteQuery, + docValueFields, filterQuery, setAbsoluteRangeDatePicker, to, @@ -62,7 +63,11 @@ export const HostsTabs = memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ), @@ -71,10 +76,10 @@ export const HostsTabs = memo( return ( - + - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 41f5b7816205e8..88886a874a9494 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -61,6 +61,7 @@ const histogramConfigs: MatrixHisrogramConfigs = { export const AuthenticationsQueryTabBody = ({ deleteQuery, + docValueFields, endDate, filterQuery, skip, @@ -89,6 +90,7 @@ export const AuthenticationsQueryTabBody = ({ {...histogramConfigs} /> ( ; }; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index 76e197063fb8a3..d7e9d86916c6dd 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -26,11 +26,11 @@ describe('EmbeddedMapComponent', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( ); expect(wrapper).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 81aa4b1671fcaa..828e4d3eaaaa05 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -71,8 +71,8 @@ EmbeddableMap.displayName = 'EmbeddableMap'; export interface EmbeddedMapProps { query: Query; filters: Filter[]; - startDate: number; - endDate: number; + startDate: string; + endDate: string; setQuery: GlobalTimeArgs['setQuery']; } diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx index 50170f4f6ae9e4..0c6b90ec2b9ddd 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -35,8 +35,8 @@ describe('embedded_map_helpers', () => { [], [], { query: '', language: 'kuery' }, - 0, - 0, + '2020-07-07T08:20:18.966Z', + '2020-07-08T08:20:18.966Z', setQueryMock, createPortalNode(), mockEmbeddable @@ -50,8 +50,8 @@ describe('embedded_map_helpers', () => { [], [], { query: '', language: 'kuery' }, - 0, - 0, + '2020-07-07T08:20:18.966Z', + '2020-07-08T08:20:18.966Z', setQueryMock, createPortalNode(), mockEmbeddable diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap index fe34c584bafb7e..ca2ce4ee921c70 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap @@ -137,14 +137,14 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`] "interval": "day", } } - endDate={1560837600000} + endDate="2019-06-18T06:00:00.000Z" flowTarget="source" id="ipOverview" ip="10.10.10.10" isLoadingAnomaliesData={false} loading={false} narrowDateRange={[MockFunction]} - startDate={1560578400000} + startDate="2019-06-15T06:00:00.000Z" type="details" updateFlowTargetAction={[MockFunction]} /> diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx index b8d97f06bf85f8..b9d9279ae34f81 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx @@ -51,14 +51,14 @@ describe('IP Overview Component', () => { const mockProps = { anomaliesData: mockAnomalies, data: mockData.IpOverview, - endDate: new Date('2019-06-18T06:00:00.000Z').valueOf(), + endDate: '2019-06-18T06:00:00.000Z', flowTarget: FlowTarget.source, loading: false, id: 'ipOverview', ip: '10.10.10.10', isLoadingAnomaliesData: false, narrowDateRange: (jest.fn() as unknown) as NarrowDateRange, - startDate: new Date('2019-06-15T06:00:00.000Z').valueOf(), + startDate: '2019-06-15T06:00:00.000Z', type: networkModel.NetworkType.details, updateFlowTargetAction: (jest.fn() as unknown) as ActionCreator<{ flowTarget: FlowTarget; diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx index 56f6d27dc28ca4..cf08b084d21979 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx @@ -42,8 +42,8 @@ interface OwnProps { loading: boolean; isLoadingAnomaliesData: boolean; anomaliesData: Anomalies | null; - startDate: number; - endDate: number; + startDate: string; + endDate: string; type: networkModel.NetworkType; narrowDateRange: NarrowDateRange; } diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap index ee7649b00aed18..2f97e45b217f3f 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap @@ -32,11 +32,11 @@ exports[`KpiNetwork Component rendering it renders loading icons 1`] = ` ], } } - from={1560578400000} + from="2019-06-15T06:00:00.000Z" id="kpiNetwork" loading={true} narrowDateRange={[MockFunction]} - to={1560837600000} + to="2019-06-18T06:00:00.000Z" /> `; @@ -72,10 +72,10 @@ exports[`KpiNetwork Component rendering it renders the default widget 1`] = ` ], } } - from={1560578400000} + from="2019-06-15T06:00:00.000Z" id="kpiNetwork" loading={false} narrowDateRange={[MockFunction]} - to={1560837600000} + to="2019-06-18T06:00:00.000Z" /> `; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx index 8acd17d2ce7676..06f623e61c2809 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx @@ -21,8 +21,8 @@ import { mockData } from './mock'; describe('KpiNetwork Component', () => { const state: State = mockGlobalState; - const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); - const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + const from = '2019-06-15T06:00:00.000Z'; + const to = '2019-06-18T06:00:00.000Z'; const narrowDateRange = jest.fn(); const { storage } = createSecuritySolutionStorageMock(); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx index ac7381160515d8..dd8979bc02a615 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx @@ -37,10 +37,10 @@ const euiColorVis3 = euiVisColorPalette[3]; interface KpiNetworkProps { data: KpiNetworkData; - from: number; + from: string; id: string; loading: boolean; - to: number; + to: string; narrowDateRange: UpdateDateRange; } @@ -132,8 +132,8 @@ export const KpiNetworkBaseComponent = React.memo<{ fieldsMapping: Readonly; data: KpiNetworkData; id: string; - from: number; - to: number; + from: string; + to: string; narrowDateRange: UpdateDateRange; }>(({ fieldsMapping, data, id, from, to, narrowDateRange }) => { const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts index a8b04ff29f4b67..bd820d4ed367d5 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts @@ -220,11 +220,11 @@ export const mockEnableChartsData = { icon: 'visMapCoordinate', }, ], - from: 1560578400000, + from: '2019-06-15T06:00:00.000Z', grow: 2, id: 'statItem', index: 2, statKey: 'UniqueIps', - to: 1560837600000, + to: '2019-06-18T06:00:00.000Z', narrowDateRange: mockNarrowDateRange, }; diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts index 3733cd780a4f76..6ebb60ccb4ea6b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts @@ -13,10 +13,16 @@ export const ipOverviewQuery = gql` $ip: String! $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! ) { source(id: $sourceId) { id - IpOverview(filterQuery: $filterQuery, ip: $ip, defaultIndex: $defaultIndex) { + IpOverview( + filterQuery: $filterQuery + ip: $ip + defaultIndex: $defaultIndex + docValueFields: $docValueFields + ) { source { firstSeen lastSeen diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx index 551ecebf2c05a7..6c8b54cc795178 100644 --- a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx @@ -35,7 +35,7 @@ export interface IpOverviewProps extends QueryTemplateProps { } const IpOverviewComponentQuery = React.memo( - ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( + ({ id = ID, docValueFields, isInspected, children, filterQuery, skip, sourceId, ip }) => ( query={ipOverviewQuery} fetchPolicy={getDefaultFetchPolicy()} @@ -46,6 +46,7 @@ const IpOverviewComponentQuery = React.memo( filterQuery: createFilter(filterQuery), ip, defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + docValueFields: docValueFields ?? [], inspect: isInspected, }} > diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index a50f2a131b75b8..17506f9a01cb92 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -92,8 +92,8 @@ class TlsComponentQuery extends QueryTemplatePaginated< sourceId, timerange: { interval: '12h', - from: startDate ? startDate : 0, - to: endDate ? endDate : Date.now(), + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), }, }; return ( diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx index 92f39228f07a75..e2e458bcec2f54 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx @@ -34,9 +34,12 @@ type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/containers/source'); jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); // Test will fail because we will to need to mock some core services to make the test work @@ -67,8 +70,8 @@ const getMockHistory = (ip: string) => ({ listen: jest.fn(), }); -const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const to = '2018-03-23T18:49:23.132Z'; +const from = '2018-03-24T03:33:52.253Z'; const getMockProps = (ip: string) => ({ to, from, @@ -88,8 +91,8 @@ const getMockProps = (ip: string) => ({ match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>, setIpDetailsTablesActivePageToZero: (jest.fn() as unknown) as ActionCreator, }); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index 5eb7a1cec67608..e06f5489a3fc2f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -77,7 +77,7 @@ export const IPDetailsComponent: React.FC ( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 6986d10ad35233..183c760e40ab10 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -18,8 +18,8 @@ import { NarrowDateRange } from '../../../common/components/ml/types'; interface QueryTabBodyProps extends Pick { skip: boolean; type: networkModel.NetworkType; - startDate: number; - endDate: number; + startDate: string; + endDate: string; filterQuery?: string | ESTermQuery; narrowDateRange?: NarrowDateRange; } diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index af84e1d42b45b9..78521a980de402 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -58,8 +58,8 @@ const mockHistory = { listen: jest.fn(), }; -const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const to = '2018-03-23T18:49:23.132Z'; +const from = '2018-03-24T03:33:52.253Z'; const getMockProps = () => ({ networkPagePath: '', diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 5767951f9f6b31..f8927096c1a614 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -68,7 +68,11 @@ const NetworkComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, [setAbsoluteRangeDatePicker] ); diff --git a/x-pack/plugins/security_solution/public/network/pages/types.ts b/x-pack/plugins/security_solution/public/network/pages/types.ts index 54ff5a8d50b8e8..db3546409c8d94 100644 --- a/x-pack/plugins/security_solution/public/network/pages/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/types.ts @@ -10,8 +10,8 @@ import { InputsModelId } from '../../common/store/inputs/constants'; export type SetAbsoluteRangeDatePicker = ActionCreator<{ id: InputsModelId; - from: number; - to: number; + from: string; + to: string; }>; export type NetworkComponentProps = Partial> & { diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index d2d9861e0ae1a7..8d004829a34f07 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -26,8 +26,8 @@ jest.mock('../../../common/containers/matrix_histogram', () => { }); const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); -const from = new Date('2020-03-31T06:00:00.000Z').valueOf(); -const to = new Date('2019-03-31T06:00:00.000Z').valueOf(); +const from = '2020-03-31T06:00:00.000Z'; +const to = '2019-03-31T06:00:00.000Z'; describe('Alerts by category', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx index 95dd65f559470d..c4a941d845f167 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx @@ -16,8 +16,8 @@ import { EventCounts } from '.'; jest.mock('../../../common/components/link_to'); describe('EventCounts', () => { - const from = 1579553397080; - const to = 1579639797080; + const from = '2020-01-20T20:49:57.080Z'; + const to = '2020-01-21T20:49:57.080Z'; test('it filters the `Host events` widget with a `host.name` `exists` filter', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap index e5a4df59ac7e48..c9c34682519e28 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap @@ -192,11 +192,11 @@ exports[`Host Summary Component rendering it renders the default Host Summary 1` }, } } - endDate={1560837600000} + endDate="2019-06-18T06:00:00.000Z" id="hostOverview" isLoadingAnomaliesData={false} loading={false} narrowDateRange={[MockFunction]} - startDate={1560578400000} + startDate="2019-06-15T06:00:00.000Z" /> `; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 0286961fd78afe..71cf056f3eb626 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -19,12 +19,12 @@ describe('Host Summary Component', () => { ); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 0c679cc94f787a..0a15b039b96af2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -41,8 +41,8 @@ interface HostSummaryProps { loading: boolean; isLoadingAnomaliesData: boolean; anomaliesData: Anomalies | null; - startDate: number; - endDate: number; + startDate: string; + endDate: string; narrowDateRange: NarrowDateRange; } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index d019a480a8045d..5140137ce1b99b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -28,8 +28,8 @@ import { wait } from '../../../common/lib/helpers'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); -const startDate = 1579553397080; -const endDate = 1579639797080; +const startDate = '2020-01-20T20:49:57.080Z'; +const endDate = '2020-01-21T20:49:57.080Z'; interface MockedProvidedQuery { request: { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index c7f7c4f4af2545..d2d823f6256900 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -43,8 +43,8 @@ jest.mock('../../../common/lib/kibana', () => { }; }); -const startDate = 1579553397080; -const endDate = 1579639797080; +const startDate = '2020-01-20T20:49:57.080Z'; +const endDate = '2020-01-21T20:49:57.080Z'; interface MockedProvidedQuery { request: { diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 2fddb996ccef3b..fbfdefa13d7385 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -52,7 +52,11 @@ const SignalsByCategoryComponent: React.FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); + setAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [setAbsoluteRangeDatePicker] diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index 89761e104d70f2..76ea1f3b4af756 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -32,8 +32,8 @@ export interface OverviewHostArgs { export interface OverviewHostProps extends QueryTemplateProps { children: (args: OverviewHostArgs) => React.ReactNode; sourceId: string; - endDate: number; - startDate: number; + endDate: string; + startDate: string; } const OverviewHostComponentQuery = React.memo( diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index 86242adf3f47fa..38c035f6883b69 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -32,8 +32,8 @@ export interface OverviewNetworkArgs { export interface OverviewNetworkProps extends QueryTemplateProps { children: (args: OverviewNetworkArgs) => React.ReactNode; sourceId: string; - endDate: number; - startDate: number; + endDate: string; + startDate: string; } export const OverviewNetworkComponentQuery = React.memo( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 4262afd67ba036..f7c77bc2dfdf88 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -22,9 +22,12 @@ import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enable jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); jest.mock('../../common/containers/use_global_time', () => ({ - useGlobalTime: jest - .fn() - .mockReturnValue({ from: 0, isInitializing: false, to: 0, setQuery: jest.fn() }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), })); // Test will fail because we will to need to mock some core services to make the test work diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts index 34d763839003c6..89a6dbd496bc38 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/mocks.ts @@ -79,7 +79,7 @@ export const mockSelectedTimeline = [ }, }, title: 'duplicate timeline', - dateRange: { start: 1582538951145, end: 1582625351145 }, + dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1583866966262, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 89a35fb838a964..5759d96b95f9e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -39,6 +39,7 @@ import sinon from 'sinon'; import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/store/inputs/actions'); +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); jest.mock('../../store/timeline/actions'); jest.mock('../../../common/store/app/actions'); jest.mock('uuid', () => { @@ -262,10 +263,7 @@ describe('helpers', () => { }, ], dataProviders: [], - dateRange: { - end: 0, - start: 0, - }, + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', deletedEventIds: [], eventIdToNoteIds: {}, @@ -360,10 +358,7 @@ describe('helpers', () => { }, ], dataProviders: [], - dateRange: { - end: 0, - start: 0, - }, + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', deletedEventIds: [], eventIdToNoteIds: {}, @@ -498,6 +493,7 @@ describe('helpers', () => { ], version: '1', dataProviders: [], + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', deletedEventIds: [], eventIdToNoteIds: {}, @@ -526,10 +522,6 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -623,6 +615,7 @@ describe('helpers', () => { }, ], version: '1', + dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, dataProviders: [], description: '', deletedEventIds: [], @@ -695,10 +688,6 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -757,15 +746,15 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', }); }); @@ -773,8 +762,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); @@ -789,8 +778,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); @@ -803,8 +792,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, })(); @@ -826,8 +815,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimeline, })(); @@ -850,8 +839,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimeline, })(); @@ -879,8 +868,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: false, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [ { created: 1585233356356, @@ -913,8 +902,8 @@ describe('helpers', () => { timelineDispatch({ duplicate: true, id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', notes: [], timeline: mockTimelineModel, ruleNote: '# this would be some markdown', diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 03a6d475b3426e..04aef6f07c60af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -49,9 +49,9 @@ import { } from '../timeline/body/constants'; import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; -import { getTimeRangeSettings } from '../../../common/utils/default_date_settings'; import { createNote } from '../notes/helpers'; import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; +import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -313,10 +313,13 @@ export const queryTimelineById = ({ if (onOpenTimeline != null) { onOpenTimeline(timeline); } else if (updateTimeline) { - const { from, to } = getTimeRangeSettings(); + const { from, to } = normalizeTimeRange({ + from: getOr(null, 'dateRange.start', timeline), + to: getOr(null, 'dateRange.end', timeline), + }); updateTimeline({ duplicate, - from: getOr(from, 'dateRange.start', timeline), + from, id: 'timeline-1', notes, timeline: { @@ -324,7 +327,7 @@ export const queryTimelineById = ({ graphEventId, show: openTimeline, }, - to: getOr(to, 'dateRange.end', timeline), + to, })(); } }) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index a8485328e83933..eb5a03baad88c2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -189,10 +189,10 @@ export interface OpenTimelineProps { export interface UpdateTimeline { duplicate: boolean; id: string; - from: number; + from: string; notes: NoteResult[] | null | undefined; timeline: TimelineModel; - to: number; + to: string; ruleNote?: string; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 3508e12cb1be15..d76ddace40a5a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -804,7 +804,8 @@ In other use cases the message field can be used to concatenate different values }, ] } - end={1521862432253} + docValueFields={Array []} + end="2018-03-24T03:33:52.253Z" eventType="raw" filters={Array []} id="foo" @@ -901,6 +902,7 @@ In other use cases the message field can be used to concatenate different values } indexToAdd={Array []} isLive={false} + isLoadingSource={false} isSaving={false} itemsPerPage={5} itemsPerPageOptions={ @@ -928,7 +930,7 @@ In other use cases the message field can be used to concatenate different values "sortDirection": "desc", } } - start={1521830963132} + start="2018-03-23T18:49:23.132Z" status="active" timelineType="default" toggleColumn={[MockFunction]} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index fc892f5b8e6b13..9f0c4747db0572 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { BrowserFields } from '../../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { maxDelay } from '../../../../../common/lib/helpers/scheduler'; @@ -33,6 +33,7 @@ interface Props { columnRenderers: ColumnRenderer[]; containerElementRef: HTMLDivElement; data: TimelineItem[]; + docValueFields: DocValueFields[]; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; id: string; @@ -59,6 +60,7 @@ const EventsComponent: React.FC = ({ columnRenderers, containerElementRef, data, + docValueFields, eventIdToNoteIds, getNotesByIds, id, @@ -85,6 +87,7 @@ const EventsComponent: React.FC = ({ browserFields={browserFields} columnHeaders={columnHeaders} columnRenderers={columnRenderers} + docValueFields={docValueFields} event={event} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index d2175c728aa2a4..f93a152211a66e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -9,7 +9,7 @@ import { useSelector } from 'react-redux'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; -import { BrowserFields } from '../../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineDetailsQuery } from '../../../../containers/details'; import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler'; @@ -43,6 +43,7 @@ interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; + docValueFields: DocValueFields[]; event: TimelineItem; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; @@ -108,6 +109,7 @@ const StatefulEventComponent: React.FC = ({ containerElementRef, columnHeaders, columnRenderers, + docValueFields, event, eventIdToNoteIds, getNotesByIds, @@ -202,6 +204,7 @@ const StatefulEventComponent: React.FC = ({ if (isVisible) { return ( { columnHeaders: defaultHeaders, columnRenderers, data: mockTimelineData, + docValueFields: [], eventIdToNoteIds: {}, height: testBodyHeight, id: 'timeline-test', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 6bf2b5e2a391ea..86bb49fac7f3e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useRef } from 'react'; -import { BrowserFields } from '../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; @@ -40,6 +40,7 @@ export interface BodyProps { columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineItem[]; + docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; height?: number; @@ -75,6 +76,7 @@ export const Body = React.memo( columnHeaders, columnRenderers, data, + docValueFields, eventIdToNoteIds, getNotesByIds, graphEventId, @@ -183,6 +185,7 @@ export const Body = React.memo( columnHeaders={columnHeaders} columnRenderers={columnRenderers} data={data} + docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} id={id} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 141534f1dcb6f3..70971408e5003a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -11,7 +11,7 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; -import { BrowserFields } from '../../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; import { appSelectors, State } from '../../../../common/store'; @@ -41,6 +41,7 @@ import { plainRowRenderer } from './renderers/plain_row_renderer'; interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; + docValueFields: DocValueFields[]; height?: number; id: string; isEventViewer?: boolean; @@ -59,6 +60,7 @@ const StatefulBodyComponent = React.memo( browserFields, columnHeaders, data, + docValueFields, eventIdToNoteIds, excludedRowRendererIds, height, @@ -192,6 +194,7 @@ const StatefulBodyComponent = React.memo( columnHeaders={columnHeaders || emptyColumnHeaders} columnRenderers={columnRenderers} data={data} + docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} graphEventId={graphEventId} @@ -225,6 +228,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.data, nextProps.data) && deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx index 391d367ad3dc35..c371d1862be726 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx @@ -14,8 +14,8 @@ import { mockBrowserFields } from '../../../common/containers/source/mock'; import { EsQueryConfig, Filter, esFilters } from '../../../../../../../src/plugins/data/public'; const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); -const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const startDate = '2018-03-23T18:49:23.132Z'; +const endDate = '2018-03-24T03:33:52.253Z'; describe('Build KQL Query', () => { test('Build KQL query with one data provider', () => { @@ -54,6 +54,14 @@ describe('Build KQL Query', () => { expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); }); + test('Buld KQL query with one data provider as timestamp (numeric input as string)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '1521848183232'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + test('Build KQL query with one data provider as date type (string input)', () => { const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); dataProviders[0].queryMatch.field = 'event.end'; @@ -70,6 +78,14 @@ describe('Build KQL Query', () => { expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); }); + test('Buld KQL query with one data provider as date type (numeric input as string)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '1521848183232'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + test('Build KQL query with two data provider', () => { const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); @@ -244,8 +260,7 @@ describe('Combined Queries', () => { isEventViewer, }) ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', }); }); @@ -291,7 +306,7 @@ describe('Combined Queries', () => { }) ).toEqual({ filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', }); }); @@ -309,7 +324,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -329,7 +344,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -349,7 +364,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -369,7 +384,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -389,7 +404,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -406,7 +421,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -424,7 +439,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -442,7 +457,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); @@ -462,7 +477,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' ); }); @@ -482,7 +497,7 @@ describe('Combined Queries', () => { end: endDate, })!; expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index a0087ab638dbf5..b21ea3e4f86e96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, isNumber, get } from 'lodash/fp'; +import { isEmpty, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -23,6 +23,8 @@ import { Filter, } from '../../../../../../../src/plugins/data/public'; +const isNumber = (value: string | number) => !isNaN(Number(value)); + const convertDateFieldToQuery = (field: string, value: string | number) => `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; @@ -113,33 +115,28 @@ export const combineQueries = ({ filters: Filter[]; kqlQuery: Query; kqlMode: string; - start: number; - end: number; + start: string; + end: string; isEventViewer?: boolean; }): { filterQuery: string } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { return null; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { - kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; + kuery.query = `(${kqlQuery.query})`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { - kuery.query = `(${buildGlobalQuery( - dataProviders, - browserFields - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + kuery.query = `(${buildGlobalQuery(dataProviders, browserFields)})`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; @@ -148,7 +145,7 @@ export const combineQueries = ({ const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( kqlQuery.query as string - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + )})`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 50a7782012b76b..ce96e4e50dea07 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -35,6 +35,8 @@ jest.mock('../../../common/lib/kibana', () => { }; }); +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); + const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); @@ -56,8 +58,8 @@ describe('StatefulTimeline', () => { columnId: '@timestamp', sortDirection: Direction.desc, }; - const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); - const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + const startDate = '2018-03-23T18:49:23.132Z'; + const endDate = '2018-03-24T03:33:52.253Z'; const mocks = [ { request: { query: timelineQuery }, result: { data: { events: mockTimelineData } } }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index c4d89fa29cb324..2d7527d8a922c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -171,13 +171,17 @@ const StatefulTimelineComponent = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const { indexPattern, browserFields } = useWithSource('default', indexToAdd); + const { docValueFields, indexPattern, browserFields, loading: isLoadingSource } = useWithSource( + 'default', + indexToAdd + ); return ( ( indexPattern={indexPattern} indexToAdd={indexToAdd} isLive={isLive} + isLoadingSource={isLoadingSource} isSaving={isSaving} itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index 546f06b60cb56a..75f684c629c701 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -65,9 +65,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -107,9 +107,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -154,9 +154,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -199,9 +199,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -246,9 +246,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} @@ -291,9 +291,9 @@ describe('Timeline QueryBar ', () => { filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} + from={'2020-07-07T08:20:18.966Z'} fromStr={DEFAULT_FROM} - to={1} + to={'2020-07-08T08:20:18.966Z'} toStr={DEFAULT_TO} kqlMode="search" indexPattern={mockIndexPattern} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 967c5818a87225..74f21fecd0fdab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -37,7 +37,7 @@ export interface QueryBarTimelineComponentProps { filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; - from: number; + from: string; fromStr: string; kqlMode: KqlMode; indexPattern: IIndexPattern; @@ -48,7 +48,7 @@ export interface QueryBarTimelineComponentProps { setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; setSavedQueryId: (savedQueryId: string | null) => void; timelineId: string; - to: number; + to: string; toStr: string; updateReduxTime: DispatchUpdateReduxTime; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 4d90bd875efcc5..e04cef4ad8d934 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -51,7 +51,7 @@ interface Props { filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; - from: number; + from: string; fromStr: string; indexPattern: IIndexPattern; isRefreshPaused: boolean; @@ -64,7 +64,7 @@ interface Props { setSavedQueryId: (savedQueryId: string | null) => void; filters: Filter[]; savedQueryId: string | null; - to: number; + to: string; toStr: string; updateEventType: (eventType: EventType) => void; updateReduxTime: DispatchUpdateReduxTime; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 7711cb7ba620e0..58c46af5606f47 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -59,8 +59,8 @@ describe('Timeline', () => { columnId: '@timestamp', sortDirection: Direction.desc, }; - const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); - const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + const startDate = '2018-03-23T18:49:23.132Z'; + const endDate = '2018-03-24T03:33:52.253Z'; const indexPattern = mockIndexPattern; @@ -76,12 +76,14 @@ describe('Timeline', () => { columns: defaultHeaders, id: 'foo', dataProviders: mockDataProviders, + docValueFields: [], end: endDate, eventType: 'raw' as TimelineComponentProps['eventType'], filters: [], indexPattern, indexToAdd: [], isLive: false, + isLoadingSource: false, isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], @@ -155,6 +157,42 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); }); + test('it does NOT render the timeline table when the source is loading', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + }); + + test('it does NOT render the timeline table when start is empty', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + }); + + test('it does NOT render the timeline table when end is empty', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + }); + test('it does NOT render the paging footer when you do NOT have any data providers', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index c1e97dcaef86ab..c27af94addeab2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; -import { BrowserFields } from '../../../common/containers/source'; +import { BrowserFields, DocValueFields } from '../../../common/containers/source'; import { TimelineQuery } from '../../containers/index'; import { Direction } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; @@ -98,7 +98,8 @@ export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; - end: number; + docValueFields: DocValueFields[]; + end: string; eventType?: EventType; filters: Filter[]; graphEventId?: string; @@ -106,6 +107,7 @@ export interface Props { indexPattern: IIndexPattern; indexToAdd: string[]; isLive: boolean; + isLoadingSource: boolean; isSaving: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; @@ -121,7 +123,7 @@ export interface Props { onToggleDataProviderType: OnToggleDataProviderType; show: boolean; showCallOutUnauthorizedMsg: boolean; - start: number; + start: string; sort: Sort; status: TimelineStatusLiteral; toggleColumn: (column: ColumnHeaderOptions) => void; @@ -134,6 +136,7 @@ export const TimelineComponent: React.FC = ({ browserFields, columns, dataProviders, + docValueFields, end, eventType, filters, @@ -142,6 +145,7 @@ export const TimelineComponent: React.FC = ({ indexPattern, indexToAdd, isLive, + isLoadingSource, isSaving, itemsPerPage, itemsPerPageOptions, @@ -167,17 +171,47 @@ export const TimelineComponent: React.FC = ({ const dispatch = useDispatch(); const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const combinedQueries = combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, - kqlMode, - start, - end, - }); + const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(kibana.services.uiSettings), [ + kibana.services.uiSettings, + ]); + const kqlQuery = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [ + kqlQueryExpression, + ]); + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters, + kqlQuery, + kqlMode, + start, + end, + }), + [ + browserFields, + dataProviders, + esQueryConfig, + start, + end, + filters, + indexPattern, + kqlMode, + kqlQuery, + ] + ); + + const canQueryTimeline = useMemo( + () => + combinedQueries != null && + isLoadingSource != null && + !isLoadingSource && + !isEmpty(start) && + !isEmpty(end), + [isLoadingSource, combinedQueries, start, end] + ); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const timelineQueryFields = useMemo(() => columnsHeader.map((c) => c.id), [columnsHeader]); const timelineQuerySortField = useMemo( @@ -239,16 +273,19 @@ export const TimelineComponent: React.FC = ({ - {combinedQueries != null ? ( + {canQueryTimeline ? ( {({ events, @@ -277,6 +314,7 @@ export const TimelineComponent: React.FC = ({ React.ReactElement; + docValueFields: DocValueFields[]; indexName: string; eventId: string; executeQuery: boolean; @@ -34,12 +36,14 @@ const getDetailsEvent = memoizeOne( const TimelineDetailsQueryComponent: React.FC = ({ children, + docValueFields, indexName, eventId, executeQuery, sourceId, }) => { const variables: GetTimelineDetailsQuery.Variables = { + docValueFields, sourceId, indexName, eventId, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 6c90b39a8e688c..5a162fd2206a1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -15,6 +15,8 @@ export const timelineQuery = gql` $filterQuery: String $defaultIndex: [String!]! $inspect: Boolean! + $docValueFields: [docValueFieldsInput!]! + $timerange: TimerangeInput! ) { source(id: $sourceId) { id @@ -24,6 +26,8 @@ export const timelineQuery = gql` sortField: $sortField filterQuery: $filterQuery defaultIndex: $defaultIndex + docValueFields: $docValueFields + timerange: $timerange ) { totalCount inspect @include(if: $inspect) { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 164d34db16d87e..510d58dbe6a696 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -49,6 +49,7 @@ export interface CustomReduxProps { export interface OwnProps extends QueryTemplateProps { children?: (args: TimelineArgs) => React.ReactNode; + endDate: string; eventType?: EventType; id: string; indexPattern?: IIndexPattern; @@ -56,6 +57,7 @@ export interface OwnProps extends QueryTemplateProps { limit: number; sortField: SortField; fields: string[]; + startDate: string; } type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; @@ -77,6 +79,8 @@ class TimelineQueryComponent extends QueryTemplate< const { children, clearSignalsState, + docValueFields, + endDate, eventType = 'raw', id, indexPattern, @@ -88,6 +92,7 @@ class TimelineQueryComponent extends QueryTemplate< filterQuery, sourceId, sortField, + startDate, } = this.props; const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); const defaultIndex = @@ -101,9 +106,15 @@ class TimelineQueryComponent extends QueryTemplate< fieldRequested: fields, filterQuery: createFilter(filterQuery), sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, pagination: { limit, cursor: null, tiebreaker: null }, sortField, defaultIndex, + docValueFields: docValueFields ?? [], inspect: isInspected, }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 618de48091ce8a..faeef432ea4229 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -56,8 +56,8 @@ export const createTimeline = actionCreator<{ id: string; dataProviders?: DataProvider[]; dateRange?: { - start: number; - end: number; + start: string; + end: string; }; excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; @@ -209,7 +209,7 @@ export const updateProviders = actionCreator<{ id: string; providers: DataProvid 'UPDATE_PROVIDERS' ); -export const updateRange = actionCreator<{ id: string; start: number; end: number }>( +export const updateRange = actionCreator<{ id: string; start: string; end: string }>( 'UPDATE_RANGE' ); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index f4c4085715af9b..7980f62cff1718 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -9,11 +9,16 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline' import { Direction } from '../../../graphql/types'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; +import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { SubsetTimelineModel, TimelineModel } from './model'; +// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false +const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false); + export const timelineDefaults: SubsetTimelineModel & Pick = { columns: defaultHeaders, dataProviders: [], + dateRange: { start, end }, deletedEventIds: [], description: '', eventType: 'all', @@ -42,10 +47,6 @@ export const timelineDefaults: SubsetTimelineModel & Pick { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, - dateRange: { start: 1572469587644, end: 1572555987644 }, + dateRange: { start: '2019-10-30T21:06:27.644Z', end: '2019-10-31T21:06:27.644Z' }, savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685', selectedEventIds: {}, show: true, @@ -158,9 +158,9 @@ describe('Epic Timeline', () => { expect( convertTimelineAsInput(timelineModel, { kind: 'absolute', - from: 1572469587644, + from: '2019-10-30T21:06:27.644Z', fromStr: undefined, - to: 1572555987644, + to: '2019-10-31T21:06:27.644Z', toStr: undefined, }) ).toEqual({ @@ -228,8 +228,8 @@ describe('Epic Timeline', () => { }, ], dateRange: { - end: 1572555987644, - start: 1572469587644, + end: '2019-10-31T21:06:27.644Z', + start: '2019-10-30T21:06:27.644Z', }, description: '', eventType: 'all', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 7d65181db65fd8..bd1fac9b054742 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -65,8 +65,8 @@ describe('epicLocalStorage', () => { columnId: '@timestamp', sortDirection: Direction.desc, }; - const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); - const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + const startDate = '2018-03-23T18:49:23.132Z'; + const endDate = '2018-03-24T03:33:52.253Z'; const indexPattern = mockIndexPattern; @@ -83,12 +83,14 @@ describe('epicLocalStorage', () => { columns: defaultHeaders, id: 'foo', dataProviders: mockDataProviders, + docValueFields: [], end: endDate, eventType: 'raw' as TimelineComponentProps['eventType'], filters: [], indexPattern, indexToAdd: [], isLive: false, + isLoadingSource: false, isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 59f47297b1f65f..2d16892329e198 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -26,6 +26,7 @@ import { TimelineType, RowRendererId, } from '../../../../common/types/timeline'; +import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; @@ -131,8 +132,8 @@ interface AddNewTimelineParams { columns: ColumnHeaderOptions[]; dataProviders?: DataProvider[]; dateRange?: { - start: number; - end: number; + start: string; + end: string; }; excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; @@ -153,7 +154,7 @@ interface AddNewTimelineParams { export const addNewTimeline = ({ columns, dataProviders = [], - dateRange = { start: 0, end: 0 }, + dateRange: mayDateRange, excludedRowRendererIds = [], filters = timelineDefaults.filters, id, @@ -165,6 +166,8 @@ export const addNewTimeline = ({ timelineById, timelineType, }: AddNewTimelineParams): TimelineById => { + const { from: startDateRange, to: endDateRange } = normalizeTimeRange({ from: '', to: '' }); + const dateRange = mayDateRange ?? { start: startDateRange, end: endDateRange }; const templateTimelineInfo = timelineType === TimelineType.template ? { @@ -752,8 +755,8 @@ export const updateTimelineProviders = ({ interface UpdateTimelineRangeParams { id: string; - start: number; - end: number; + start: string; + end: string; timelineById: TimelineById; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 95d525c7eb59f4..9a8399d3669678 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -101,8 +101,8 @@ export interface TimelineModel { pinnedEventsSaveObject: Record; /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ dateRange: { - start: number; - end: number; + start: string; + end: string; }; savedQueryId?: string | null; /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 4cfc20eb817059..0197ccc7eec05a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -48,6 +48,8 @@ import { ColumnHeaderOptions } from './model'; import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; +jest.mock('../../../common/components/url_state/normalize_time_range.ts'); + const timelineByIdMock: TimelineById = { foo: { dataProviders: [ @@ -92,8 +94,8 @@ const timelineByIdMock: TimelineById = { pinnedEventIds: {}, pinnedEventsSaveObject: {}, dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1009,8 +1011,8 @@ describe('Timeline', () => { test('should return a new reference and not the same reference', () => { const update = updateTimelineRange({ id: 'foo', - start: 23, - end: 33, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', timelineById: timelineByIdMock, }); expect(update).not.toBe(timelineByIdMock); @@ -1019,16 +1021,16 @@ describe('Timeline', () => { test('should update the timeline range', () => { const update = updateTimelineRange({ id: 'foo', - start: 23, - end: 33, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', timelineById: timelineByIdMock, }); expect(update).toEqual( set( 'foo.dateRange', { - start: 23, - end: 33, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, timelineByIdMock ) @@ -1135,8 +1137,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1231,8 +1233,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1437,8 +1439,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1533,8 +1535,8 @@ describe('Timeline', () => { templateTimelineVersion: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1635,8 +1637,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1738,8 +1740,8 @@ describe('Timeline', () => { templateTimelineVersion: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -1933,8 +1935,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -2013,8 +2015,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, @@ -2117,8 +2119,8 @@ describe('Timeline', () => { templateTimelineId: null, noteIds: [], dateRange: { - start: 0, - end: 0, + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', }, selectedEventIds: {}, show: true, diff --git a/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts index 20935ce9ed03fc..648a65fa246827 100644 --- a/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/authentications/schema.gql.ts @@ -41,6 +41,7 @@ export const authenticationsSchema = gql` pagination: PaginationInputPaginated! filterQuery: String defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): AuthenticationsData! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts index a9ef6bc682c845..ef28ac523ff85e 100644 --- a/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/events/resolvers.ts @@ -58,6 +58,7 @@ export const createEventsResolvers = ( async LastEventTime(source, args, { req }) { const options: LastEventTimeRequestOptions = { defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields, sourceConfiguration: source.configuration, indexKey: args.indexKey, details: args.details, diff --git a/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts index 3b71977bc0d478..eee4bc3e3a33fd 100644 --- a/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/events/schema.gql.ts @@ -76,17 +76,20 @@ export const eventsSchema = gql` timerange: TimerangeInput filterQuery: String defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): TimelineData! TimelineDetails( eventId: String! indexName: String! defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): TimelineDetailsData! LastEventTime( id: String indexKey: LastEventIndexKey! details: LastTimeDetails! defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): LastEventTimeData! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts index e37ade585e8bed..181ee3c2b4e949 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/resolvers.ts @@ -71,6 +71,7 @@ export const createHostsResolvers = ( sourceConfiguration: source.configuration, hostName: args.hostName, defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields, }; return libs.hosts.getHostFirstLastSeen(req, options); }, diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index 02f8341cd6fd9b..48bb0cbe37afdd 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts @@ -99,6 +99,7 @@ export const hostsSchema = gql` sort: HostsSortField! filterQuery: String defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): HostsData! HostOverview( id: String @@ -106,6 +107,11 @@ export const hostsSchema = gql` timerange: TimerangeInput! defaultIndex: [String!]! ): HostItem! - HostFirstLastSeen(id: String, hostName: String!, defaultIndex: [String!]!): FirstLastSeenHost! + HostFirstLastSeen( + id: String + hostName: String! + defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! + ): FirstLastSeenHost! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts index 4684449c1b80ff..2531f8d169327b 100644 --- a/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts @@ -38,6 +38,7 @@ const ipOverviewSchema = gql` filterQuery: String ip: String! defaultIndex: [String!]! + docValueFields: [docValueFieldsInput!]! ): IpOverviewData } `; diff --git a/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts index 15e2d832a73c9d..9bb8a48c12f0d9 100644 --- a/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/network/schema.gql.ts @@ -238,6 +238,7 @@ export const networkSchema = gql` defaultIndex: [String!]! timerange: TimerangeInput! stackByField: String + docValueFields: [docValueFieldsInput!]! ): NetworkDsOverTimeData! NetworkHttp( id: String diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 7cbeea67b27509..fce81e2f0dce09 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -34,8 +34,8 @@ const kueryFilterQuery = ` `; const dateRange = ` - start: Float - end: Float + start: ToAny + end: ToAny `; const favoriteTimeline = ` diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 1eaf47ad43812a..f8a614e86f28e9 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -26,9 +26,9 @@ export interface TimerangeInput { /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ interval: string; /** The end of the timerange */ - to: number; + to: string; /** The beginning of the timerange */ - from: number; + from: string; } export interface PaginationInputPaginated { @@ -42,6 +42,12 @@ export interface PaginationInputPaginated { querySize: number; } +export interface DocValueFieldsInput { + field: string; + + format: string; +} + export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -262,9 +268,9 @@ export interface KueryFilterQueryInput { } export interface DateRangePickerInput { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface SortTimelineInput { @@ -2095,9 +2101,9 @@ export interface QueryMatchResult { } export interface DateRangePickerResult { - start?: Maybe; + start?: Maybe; - end?: Maybe; + end?: Maybe; } export interface FavoriteTimelineResult { @@ -2334,6 +2340,8 @@ export interface AuthenticationsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2347,6 +2355,8 @@ export interface TimelineSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface TimelineDetailsSourceArgs { eventId: string; @@ -2354,6 +2364,8 @@ export interface TimelineDetailsSourceArgs { indexName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface LastEventTimeSourceArgs { id?: Maybe; @@ -2363,6 +2375,8 @@ export interface LastEventTimeSourceArgs { details: LastTimeDetails; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostsSourceArgs { id?: Maybe; @@ -2376,6 +2390,8 @@ export interface HostsSourceArgs { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface HostOverviewSourceArgs { id?: Maybe; @@ -2392,6 +2408,8 @@ export interface HostFirstLastSeenSourceArgs { hostName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface IpOverviewSourceArgs { id?: Maybe; @@ -2401,6 +2419,8 @@ export interface IpOverviewSourceArgs { ip: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export interface UsersSourceArgs { filterQuery?: Maybe; @@ -2516,6 +2536,8 @@ export interface NetworkDnsHistogramSourceArgs { timerange: TimerangeInput; stackByField?: Maybe; + + docValueFields: DocValueFieldsInput[]; } export interface NetworkHttpSourceArgs { id?: Maybe; @@ -3054,6 +3076,8 @@ export namespace SourceResolvers { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type TimelineResolver< @@ -3073,6 +3097,8 @@ export namespace SourceResolvers { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type TimelineDetailsResolver< @@ -3086,6 +3112,8 @@ export namespace SourceResolvers { indexName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type LastEventTimeResolver< @@ -3101,6 +3129,8 @@ export namespace SourceResolvers { details: LastTimeDetails; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type HostsResolver = Resolver< @@ -3121,6 +3151,8 @@ export namespace SourceResolvers { filterQuery?: Maybe; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type HostOverviewResolver< @@ -3149,6 +3181,8 @@ export namespace SourceResolvers { hostName: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type IpOverviewResolver< @@ -3164,6 +3198,8 @@ export namespace SourceResolvers { ip: string; defaultIndex: string[]; + + docValueFields: DocValueFieldsInput[]; } export type UsersResolver = Resolver< @@ -3334,6 +3370,8 @@ export namespace SourceResolvers { timerange: TimerangeInput; stackByField?: Maybe; + + docValueFields: DocValueFieldsInput[]; } export type NetworkHttpResolver< @@ -8559,18 +8597,18 @@ export namespace QueryMatchResultResolvers { export namespace DateRangePickerResultResolvers { export interface Resolvers { - start?: StartResolver, TypeParent, TContext>; + start?: StartResolver, TypeParent, TContext>; - end?: EndResolver, TypeParent, TContext>; + end?: EndResolver, TypeParent, TContext>; } export type StartResolver< - R = Maybe, + R = Maybe, Parent = DateRangePickerResult, TContext = SiemContext > = Resolver; export type EndResolver< - R = Maybe, + R = Maybe, Parent = DateRangePickerResult, TContext = SiemContext > = Resolver; diff --git a/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts index b9ed88e91f87d1..b6b72cd37efaa0 100644 --- a/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/authentications/query.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { createQueryFilterClauses } from '../../utils/build_query'; import { reduceFields } from '../../utils/build_query/reduce_fields'; import { hostFieldsMap, sourceFieldsMap } from '../ecs_fields'; @@ -26,6 +28,7 @@ export const buildQuery = ({ timerange: { from, to }, pagination: { querySize }, defaultIndex, + docValueFields, sourceConfiguration: { fields: { timestamp }, }, @@ -40,6 +43,7 @@ export const buildQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -58,6 +62,7 @@ export const buildQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...agg, group_by_users: { diff --git a/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts index 6ad18c5578f936..aabb18d4190988 100644 --- a/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts @@ -84,7 +84,7 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { request: FrameworkRequest, options: RequestDetailsOptions ): Promise { - const dsl = buildDetailsQuery(options.indexName, options.eventId); + const dsl = buildDetailsQuery(options.indexName, options.eventId, options.docValueFields ?? []); const searchResponse = await this.framework.callWithRequest( request, 'search', diff --git a/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts index bc95fe56294495..143ef1e9d5bf0e 100644 --- a/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/events/query.dsl.ts @@ -3,74 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; -import { SortField, TimerangeInput } from '../../graphql/types'; +import { SortField, TimerangeInput, DocValueFieldsInput } from '../../graphql/types'; import { createQueryFilterClauses } from '../../utils/build_query'; -import { RequestOptions, RequestOptionsPaginated } from '../framework'; +import { RequestOptions } from '../framework'; import { SortRequest } from '../types'; import { TimerangeFilter } from './types'; -export const buildQuery = (options: RequestOptionsPaginated) => { - const { querySize } = options.pagination; - const { fields, filterQuery } = options; - const filterClause = [...createQueryFilterClauses(filterQuery)]; - const defaultIndex = options.defaultIndex; - - const getTimerangeFilter = (timerange: TimerangeInput | undefined): TimerangeFilter[] => { - if (timerange) { - const { to, from } = timerange; - return [ - { - range: { - [options.sourceConfiguration.fields.timestamp]: { - gte: from, - lte: to, - }, - }, - }, - ]; - } - return []; - }; - - const filter = [...filterClause, ...getTimerangeFilter(options.timerange), { match_all: {} }]; - - const getSortField = (sortField: SortField) => { - if (sortField.sortFieldId) { - const field: string = - sortField.sortFieldId === 'timestamp' ? '@timestamp' : sortField.sortFieldId; - - return [ - { [field]: sortField.direction }, - { [options.sourceConfiguration.fields.tiebreaker]: sortField.direction }, - ]; - } - return []; - }; - - const sort: SortRequest = getSortField(options.sortField!); - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - query: { - bool: { - filter, - }, - }, - size: querySize, - track_total_hits: true, - sort, - _source: fields, - }, - }; - - return dslQuery; -}; - export const buildTimelineQuery = (options: RequestOptions) => { const { limit, cursor, tiebreaker } = options.pagination; const { fields, filterQuery } = options; @@ -86,6 +27,7 @@ export const buildTimelineQuery = (options: RequestOptions) => { [options.sourceConfiguration.fields.timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -116,6 +58,7 @@ export const buildTimelineQuery = (options: RequestOptions) => { index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(options.docValueFields) ? { docvalue_fields: options.docValueFields } : {}), query: { bool: { filter, @@ -141,11 +84,16 @@ export const buildTimelineQuery = (options: RequestOptions) => { return dslQuery; }; -export const buildDetailsQuery = (indexName: string, id: string) => ({ +export const buildDetailsQuery = ( + indexName: string, + id: string, + docValueFields: DocValueFieldsInput[] +) => ({ allowNoIndices: true, index: indexName, ignoreUnavailable: true, body: { + docvalue_fields: docValueFields, query: { terms: { _id: [id], diff --git a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts index 86491876673c99..6c443fed3c99d4 100644 --- a/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/events/query.last_event_time.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { LastEventTimeRequestOptions } from './types'; import { LastEventIndexKey } from '../../graphql/types'; import { assertUnreachable } from '../../utils/build_query'; @@ -16,6 +18,7 @@ export const buildLastEventTimeQuery = ({ indexKey, details, defaultIndex, + docValueFields, }: LastEventTimeRequestOptions) => { const indicesToQuery: EventIndices = { hosts: defaultIndex, @@ -35,6 +38,7 @@ export const buildLastEventTimeQuery = ({ index: indicesToQuery.network, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { last_seen_event: { max: { field: '@timestamp' } }, }, @@ -52,6 +56,7 @@ export const buildLastEventTimeQuery = ({ index: indicesToQuery.hosts, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { last_seen_event: { max: { field: '@timestamp' } }, }, @@ -69,6 +74,7 @@ export const buildLastEventTimeQuery = ({ index: indicesToQuery[indexKey], ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { last_seen_event: { max: { field: '@timestamp' } }, }, diff --git a/x-pack/plugins/security_solution/server/lib/events/types.ts b/x-pack/plugins/security_solution/server/lib/events/types.ts index 3a4a8705f73873..aae2360e42e65d 100644 --- a/x-pack/plugins/security_solution/server/lib/events/types.ts +++ b/x-pack/plugins/security_solution/server/lib/events/types.ts @@ -11,6 +11,7 @@ import { SourceConfiguration, TimelineData, TimelineDetailsData, + DocValueFieldsInput, } from '../../graphql/types'; import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework'; import { SearchHit } from '../types'; @@ -61,13 +62,15 @@ export interface LastEventTimeRequestOptions { details: LastTimeDetails; sourceConfiguration: SourceConfiguration; defaultIndex: string[]; + docValueFields: DocValueFieldsInput[]; } export interface TimerangeFilter { range: { [timestamp: string]: { - gte: number; - lte: number; + gte: string; + lte: string; + format: string; }; }; } @@ -76,6 +79,7 @@ export interface RequestDetailsOptions { indexName: string; eventId: string; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } interface EventsOverTimeHistogramData { diff --git a/x-pack/plugins/security_solution/server/lib/framework/types.ts b/x-pack/plugins/security_solution/server/lib/framework/types.ts index abe572df87063f..03c82ceb02e68d 100644 --- a/x-pack/plugins/security_solution/server/lib/framework/types.ts +++ b/x-pack/plugins/security_solution/server/lib/framework/types.ts @@ -18,6 +18,7 @@ import { TimerangeInput, Maybe, HistogramType, + DocValueFieldsInput, } from '../../graphql/types'; export * from '../../utils/typed_resolvers'; @@ -115,6 +116,7 @@ export interface RequestBasicOptions { timerange: TimerangeInput; filterQuery: ESQuery | undefined; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export interface MatrixHistogramRequestOptions extends RequestBasicOptions { diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts index 0f6bc5c1b0e0c8..44767563c6b754 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts @@ -24,7 +24,7 @@ export const mockGetHostsOptions: HostsRequestOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, + timerange: { interval: '12h', to: '2019-04-09T15:37:54.610Z', from: '2019-04-08T15:37:54.610Z' }, sort: { field: HostsFields.lastSeen, direction: Direction.asc }, pagination: { activePage: 0, @@ -295,7 +295,7 @@ export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1554824274610, from: 1554737874610 }, + timerange: { interval: '12h', to: '2019-04-09T15:37:54.610Z', from: '2019-04-08T15:37:54.610Z' }, defaultIndex: DEFAULT_INDEX_PATTERN, fields: [ '_id', diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts index 70f57769362f58..013afd5cd58f5a 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.hosts.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { Direction, HostsFields, HostsSortField } from '../../graphql/types'; import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; @@ -11,6 +13,7 @@ import { HostsRequestOptions } from '.'; export const buildHostsQuery = ({ defaultIndex, + docValueFields, fields, filterQuery, pagination: { querySize }, @@ -27,6 +30,7 @@ export const buildHostsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -39,6 +43,7 @@ export const buildHostsQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...agg, host_data: { diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts index d7ab22100b2460..3bdaee58917ea0 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.last_first_seen_host.dsl.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import { HostLastFirstSeenRequestOptions } from './types'; export const buildLastFirstSeenHostQuery = ({ hostName, defaultIndex, + docValueFields, }: HostLastFirstSeenRequestOptions) => { const filter = [{ term: { 'host.name': hostName } }]; @@ -17,6 +19,7 @@ export const buildLastFirstSeenHostQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { firstSeen: { min: { field: '@timestamp' } }, lastSeen: { max: { field: '@timestamp' } }, diff --git a/x-pack/plugins/security_solution/server/lib/hosts/types.ts b/x-pack/plugins/security_solution/server/lib/hosts/types.ts index e52cfe9d7feebd..fc621f81a4f5fc 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/types.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/types.ts @@ -14,6 +14,7 @@ import { OsEcsFields, SourceConfiguration, TimerangeInput, + DocValueFieldsInput, } from '../../graphql/types'; import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { Hit, Hits, SearchHit, TotalValue } from '../types'; @@ -50,6 +51,7 @@ export interface HostLastFirstSeenRequestOptions { hostName: string; sourceConfiguration: SourceConfiguration; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export interface HostOverviewRequestOptions extends HostLastFirstSeenRequestOptions { diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts index 5803b832a334b2..d9c8f32d0b465c 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import { IpOverviewRequestOptions } from './index'; const getAggs = (type: string, ip: string) => { @@ -95,12 +96,17 @@ const getHostAggs = (ip: string) => { }; }; -export const buildOverviewQuery = ({ defaultIndex, ip }: IpOverviewRequestOptions) => { +export const buildOverviewQuery = ({ + defaultIndex, + docValueFields, + ip, +}: IpOverviewRequestOptions) => { const dslQuery = { allowNoIndices: true, index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggs: { ...getAggs('source', ip), ...getAggs('destination', ip), @@ -115,5 +121,6 @@ export const buildOverviewQuery = ({ defaultIndex, ip }: IpOverviewRequestOption track_total_hits: false, }, }; + return dslQuery; }; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts index b2453325256943..10678dc033eb5d 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts @@ -23,7 +23,11 @@ export const buildUsersQuery = ({ }: UsersRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, { term: { [`${flowTarget}.ip`]: ip } }, ]; diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts index a5affea2842a6c..876d2f9c16bed6 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/mock.ts @@ -7,8 +7,8 @@ import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; import { RequestBasicOptions } from '../framework/types'; -const FROM = new Date('2019-05-03T13:24:00.660Z').valueOf(); -const TO = new Date('2019-05-04T13:24:00.660Z').valueOf(); +const FROM = '2019-05-03T13:24:00.660Z'; +const TO = '2019-05-04T13:24:00.660Z'; export const mockKpiHostsOptions: RequestBasicOptions = { defaultIndex: DEFAULT_INDEX_PATTERN, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts index 0b7803d0071947..ee9e6cd5a66c5c 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_authentication.dsl.ts @@ -33,6 +33,7 @@ export const buildAuthQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts index 87ebf0cf0e6e7a..0c1d7d4ae9de7d 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_hosts.dsl.ts @@ -22,6 +22,7 @@ export const buildHostsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts index 72833aaf9ea5bb..9813f73101235d 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_hosts/query_unique_ips.dsl.ts @@ -22,6 +22,7 @@ export const buildUniqueIpsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts index cc0849ccdf1d24..fc9b64ae0746f2 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/mock.ts @@ -19,7 +19,7 @@ export const mockOptions: RequestBasicOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, filterQuery: {}, }; @@ -28,7 +28,11 @@ export const mockRequest = { operationName: 'GetKpiNetworkQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1557445721842, to: 1557532121842 }, + timerange: { + interval: '12h', + from: '2019-05-09T23:48:41.842Z', + to: '2019-05-10T23:48:41.842Z', + }, filterQuery: '', }, query: diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts index 01771ad973b5d3..b3dba9b1d0fab9 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_dns.dsl.ts @@ -51,6 +51,7 @@ export const buildDnsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts index 1a87aff047a257..17f705fe98d031 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_network_events.ts @@ -25,6 +25,7 @@ export const buildNetworkEventsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts index 09bc0eae642e44..5032863e7d324d 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_tls_handshakes.dsl.ts @@ -51,6 +51,7 @@ export const buildTlsHandshakeQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts index 4581b889cc9ef5..fb717df2b4608d 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_flow.ts @@ -25,6 +25,7 @@ export const buildUniqueFlowIdsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts index f12ab2a3072ae9..77d6efdcfdaa0c 100644 --- a/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/kpi_network/query_unique_private_ips.dsl.ts @@ -77,6 +77,7 @@ export const buildUniquePrvateIpQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts index 38e8387f43ffde..fb4e666cda964a 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; @@ -20,6 +22,7 @@ export const buildAnomaliesOverTimeQuery = ({ timestamp: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -34,8 +37,8 @@ export const buildAnomaliesOverTimeQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts index 34a3804f974ded..174cc907214a9c 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; @@ -33,6 +35,7 @@ export const buildAuthenticationsOverTimeQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -47,8 +50,8 @@ export const buildAuthenticationsOverTimeQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts index 63649a1064b025..fa7c1b9e55b9e9 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query.events_over_time.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { showAllOthersBucket } from '../../../common/constants'; import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; @@ -26,6 +28,7 @@ export const buildEventsOverTimeQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -40,8 +43,8 @@ export const buildEventsOverTimeQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts index 4963f01d67a4f0..dd451096724807 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_alerts.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; + import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { buildTimelineQuery } from '../events/query.dsl'; import { RequestOptions, MatrixHistogramRequestOptions } from '../framework'; @@ -62,6 +64,7 @@ export const buildAlertsHistogramQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -76,8 +79,8 @@ export const buildAlertsHistogramQuery = ({ fixed_interval: interval, min_doc_count: 0, extended_bounds: { - min: from, - max: to, + min: moment(from).valueOf(), + max: moment(to).valueOf(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts index a6c75fe01eb151..7e712639889572 100644 --- a/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/matrix_histogram/query_dns_histogram.dsl.ts @@ -23,6 +23,7 @@ export const buildDnsHistogramQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/network/mock.ts b/x-pack/plugins/security_solution/server/lib/network/mock.ts index 38e82a4f19dca9..b421f7af566039 100644 --- a/x-pack/plugins/security_solution/server/lib/network/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/network/mock.ts @@ -21,7 +21,7 @@ export const mockOptions: NetworkTopNFlowRequestOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-11T02:26:46.071Z' }, pagination: { activePage: 0, cursorStart: 0, diff --git a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts index 96b5d260b15443..e7c86e1d3d66be 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_dns.dsl.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { Direction, NetworkDnsFields, NetworkDnsSortField } from '../../graphql/types'; import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; @@ -57,6 +59,7 @@ const createIncludePTRFilter = (isPtrIncluded: boolean) => export const buildDnsQuery = ({ defaultIndex, + docValueFields, filterQuery, isPtrIncluded, networkDnsSortField, @@ -74,6 +77,7 @@ export const buildDnsQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -84,6 +88,7 @@ export const buildDnsQuery = ({ index: defaultIndex, ignoreUnavailable: true, body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...getCountAgg(), dns_name_query_count: { diff --git a/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts index 3e33b5af80a855..a2d1963414be1a 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_http.dsl.ts @@ -29,7 +29,11 @@ export const buildHttpQuery = ({ }: NetworkHttpRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, { exists: { field: 'http.request.method' } }, ]; diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts index 40bee7eee81554..93ffc35161fa96 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_countries.dsl.ts @@ -36,7 +36,11 @@ export const buildTopCountriesQuery = ({ }: NetworkTopCountriesRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, ]; const dslQuery = { diff --git a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts index 47bbabf5505cad..7cb8b76e7b5244 100644 --- a/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/network/query_top_n_flow.dsl.ts @@ -36,7 +36,11 @@ export const buildTopNFlowQuery = ({ }: NetworkTopNFlowRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, ]; const dslQuery = { diff --git a/x-pack/plugins/security_solution/server/lib/overview/mock.ts b/x-pack/plugins/security_solution/server/lib/overview/mock.ts index 51d8a258569a8b..2621c795ecd6b8 100644 --- a/x-pack/plugins/security_solution/server/lib/overview/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/overview/mock.ts @@ -19,7 +19,7 @@ export const mockOptionsNetwork: RequestBasicOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, filterQuery: {}, }; @@ -28,7 +28,11 @@ export const mockRequestNetwork = { operationName: 'GetOverviewNetworkQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + timerange: { + interval: '12h', + from: '2019-02-10T02:30:30.772Z', + to: '2019-02-11T02:30:30.772Z', + }, filterQuery: '', }, query: @@ -90,7 +94,7 @@ export const mockOptionsHost: RequestBasicOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, filterQuery: {}, }; @@ -99,7 +103,11 @@ export const mockRequestHost = { operationName: 'GetOverviewHostQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + timerange: { + interval: '12h', + from: '2019-02-10T02:30:30.772Z', + to: '2019-02-11T02:30:30.772Z', + }, filterQuery: '', }, query: diff --git a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts index 30656c011ee21d..8ac8233a86b82f 100644 --- a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts @@ -21,6 +21,7 @@ export const buildOverviewNetworkQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, @@ -120,6 +121,7 @@ export const buildOverviewHostQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 2afe3197d6d645..0b10018de5bba5 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -21,7 +21,7 @@ export const mockParsedObjects = [ kqlMode: 'filter', kqlQuery: { filterQuery: [Object] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -80,7 +80,7 @@ export const mockUniqueParsedObjects = [ kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -139,7 +139,7 @@ export const mockGetTimelineValue = { kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', timelineType: TimelineType.default, - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -176,7 +176,7 @@ export const mockGetDraftTimelineValue = { kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, @@ -236,7 +236,7 @@ export const mockCreatedTimeline = { kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', - dateRange: { start: 1584523907294, end: 1584610307294 }, + dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1584828930463, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index a314d5fb36c6df..e3aeff280678ff 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -65,7 +65,7 @@ export const inputTimeline: SavedTimeline = { timelineType: TimelineType.default, templateTimelineId: null, templateTimelineVersion: 1, - dateRange: { start: 1585227005527, end: 1585313405527 }, + dateRange: { start: '2020-03-26T12:50:05.527Z', end: '2020-03-27T12:50:05.527Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, }; @@ -281,7 +281,7 @@ export const mockTimelines = () => ({ }, }, title: 'test no.2', - dateRange: { start: 1582538951145, end: 1582625351145 }, + dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1582625382448, @@ -363,7 +363,7 @@ export const mockTimelines = () => ({ }, }, title: 'test no.3', - dateRange: { start: 1582538951145, end: 1582625351145 }, + dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, created: 1582642817439, diff --git a/x-pack/plugins/security_solution/server/lib/tls/mock.ts b/x-pack/plugins/security_solution/server/lib/tls/mock.ts index b97a6fa509ef28..62d5e1e61570a7 100644 --- a/x-pack/plugins/security_solution/server/lib/tls/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/tls/mock.ts @@ -458,7 +458,7 @@ export const mockOptions = { timestamp: '@timestamp', }, }, - timerange: { interval: '12h', to: 1570801871626, from: 1570715471626 }, + timerange: { interval: '12h', to: '2019-10-11T13:51:11.626Z', from: '2019-10-10T13:51:11.626Z' }, pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, filterQuery: {}, fields: [ diff --git a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts index bc65be642dabc0..82f16ff58d135e 100644 --- a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts @@ -62,7 +62,11 @@ export const buildTlsQuery = ({ }: TlsRequestOptions) => { const defaultFilter = [ ...createQueryFilterClauses(filterQuery), - { range: { [timestamp]: { gte: from, lte: to } } }, + { + range: { + [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, ]; const filter = ip ? [...defaultFilter, { term: { [`${flowTarget}.ip`]: ip } }] : defaultFilter; diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts index 24cae53d5d3534..4563c769cdc31b 100644 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts @@ -28,6 +28,7 @@ export const buildQuery = ({ [timestamp]: { gte: from, lte: to, + format: 'strict_date_optional_time', }, }, }, diff --git a/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts index 78aadf75e54c36..ded37db677d6d3 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/calculate_timeseries_interval.ts @@ -89,6 +89,6 @@ export const calculateAuto = { }), }; -export const calculateTimeSeriesInterval = (from: number, to: number) => { - return `${Math.floor((to - from) / 32)}ms`; +export const calculateTimeSeriesInterval = (from: string, to: string) => { + return `${Math.floor(moment(to).diff(moment(from)) / 32)}ms`; }; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts b/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts index 5ca67ad6ae51f7..e83ca7418ad3d4 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/create_options.test.ts @@ -34,9 +34,19 @@ describe('createOptions', () => { pagination: { limit: 5, }, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], timerange: { - from: 10, - to: 0, + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', interval: '12 hours ago', }, sortField: { sortFieldId: 'sort-1', direction: Direction.asc }, @@ -73,10 +83,20 @@ describe('createOptions', () => { limit: 5, }, filterQuery: {}, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], fields: [], timerange: { - from: 10, - to: 0, + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', interval: '12 hours ago', }, }; @@ -102,10 +122,51 @@ describe('createOptions', () => { limit: 5, }, filterQuery: {}, + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + ], + fields: [], + timerange: { + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', + interval: '12 hours ago', + }, + }; + expect(options).toEqual(expected); + }); + + test('should create options given all input except docValueFields', () => { + const argsWithoutSort: Args = omit('docValueFields', args); + const options = createOptions(source, argsWithoutSort, info); + const expected: RequestOptions = { + defaultIndex: DEFAULT_INDEX_PATTERN, + sourceConfiguration: { + fields: { + host: 'host-1', + container: 'container-1', + message: ['message-1'], + pod: 'pod-1', + tiebreaker: 'tiebreaker', + timestamp: 'timestamp-1', + }, + }, + sortField: { sortFieldId: 'sort-1', direction: Direction.asc }, + pagination: { + limit: 5, + }, + filterQuery: {}, + docValueFields: [], fields: [], timerange: { - from: 10, - to: 0, + from: '2020-07-08T08:00:00.000Z', + to: '2020-07-08T20:00:00.000Z', interval: '12 hours ago', }, }; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts b/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts index 5a5aff2a2d54e0..5895c0a4041361 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/create_options.ts @@ -13,6 +13,7 @@ import { SortField, Source, TimerangeInput, + DocValueFieldsInput, } from '../../graphql/types'; import { RequestOptions, RequestOptionsPaginated } from '../../lib/framework'; import { parseFilterQuery } from '../serialized_query'; @@ -32,6 +33,7 @@ export interface Args { filterQuery?: string | null; sortField?: SortField | null; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export interface ArgsPaginated { timerange?: TimerangeInput | null; @@ -39,6 +41,7 @@ export interface ArgsPaginated { filterQuery?: string | null; sortField?: SortField | null; defaultIndex: string[]; + docValueFields?: DocValueFieldsInput[]; } export const createOptions = ( @@ -50,6 +53,7 @@ export const createOptions = ( const fields = getFields(getOr([], 'fieldNodes[0]', info)); return { defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields ?? [], sourceConfiguration: source.configuration, timerange: args.timerange!, pagination: args.pagination!, @@ -70,6 +74,7 @@ export const createOptionsPaginated = ( const fields = getFields(getOr([], 'fieldNodes[0]', info)); return { defaultIndex: args.defaultIndex, + docValueFields: args.docValueFields ?? [], sourceConfiguration: source.configuration, timerange: args.timerange!, pagination: args.pagination!, diff --git a/x-pack/test/api_integration/apis/security_solution/authentications.ts b/x-pack/test/api_integration/apis/security_solution/authentications.ts index 90784ec786d48f..277ac7316e92dd 100644 --- a/x-pack/test/api_integration/apis/security_solution/authentications.ts +++ b/x-pack/test/api_integration/apis/security_solution/authentications.ts @@ -10,8 +10,8 @@ import { authenticationsQuery } from '../../../../plugins/security_solution/publ import { GetAuthenticationsQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const HOST_NAME = 'zeek-newyork-sha-aa8df15'; @@ -44,6 +44,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -73,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 2, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/hosts.ts b/x-pack/test/api_integration/apis/security_solution/hosts.ts index 9ee85f7ff03dc2..2904935719d2ca 100644 --- a/x-pack/test/api_integration/apis/security_solution/hosts.ts +++ b/x-pack/test/api_integration/apis/security_solution/hosts.ts @@ -18,8 +18,8 @@ import { HostFirstLastSeenGqlQuery } from '../../../../plugins/security_solution import { HostsTableQuery } from '../../../../plugins/security_solution/public/hosts/containers/hosts/hosts_table.gql_query'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const HOST_NAME = 'Ubuntu'; @@ -47,6 +47,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], sort: { field: HostsFields.lastSeen, direction: Direction.asc, @@ -84,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { direction: Direction.asc, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], pagination: { activePage: 2, cursorStart: 1, @@ -150,6 +152,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -167,6 +170,7 @@ export default function ({ getService }: FtrProviderContext) { sourceId: 'default', hostName: 'zeek-sensor-san-francisco', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/ip_overview.ts b/x-pack/test/api_integration/apis/security_solution/ip_overview.ts index 1dc0f6390ce7e9..6493c076179917 100644 --- a/x-pack/test/api_integration/apis/security_solution/ip_overview.ts +++ b/x-pack/test/api_integration/apis/security_solution/ip_overview.ts @@ -25,6 +25,7 @@ export default function ({ getService }: FtrProviderContext) { sourceId: 'default', ip: '151.205.0.17', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -52,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { sourceId: 'default', ip: '185.53.91.88', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts b/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts index 4b296078ff4438..c446fbb149e3a5 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_host_details.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiHostDetailsData', authSuccess: 0, @@ -86,6 +86,7 @@ export default function ({ getService }: FtrProviderContext) { }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], hostName: 'zeek-sensor-san-francisco', + docValueFields: [], inspect: false, }, }) @@ -167,6 +168,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], hostName: 'zeek-sensor-san-francisco', inspect: false, }, diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts b/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts index 30a0eac386c9df..dcea52edcddf97 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_hosts.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiHostsData', hosts: 1, @@ -108,6 +108,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -122,8 +123,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiHostsData', hosts: 1, @@ -212,6 +213,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/kpi_network.ts b/x-pack/test/api_integration/apis/security_solution/kpi_network.ts index 6d6eee7d3468de..654607913d44af 100644 --- a/x-pack/test/api_integration/apis/security_solution/kpi_network.ts +++ b/x-pack/test/api_integration/apis/security_solution/kpi_network.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiNetworkData', networkEvents: 6158, @@ -85,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -99,8 +100,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('packetbeat/default')); after(() => esArchiver.unload('packetbeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { __typename: 'KpiNetworkData', networkEvents: 6158, @@ -166,6 +167,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/network_dns.ts b/x-pack/test/api_integration/apis/security_solution/network_dns.ts index 9d88c7bc2389b2..e5f3ed18d32ea4 100644 --- a/x-pack/test/api_integration/apis/security_solution/network_dns.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_dns.ts @@ -21,8 +21,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('packetbeat/dns')); after(() => esArchiver.unload('packetbeat/dns')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; it('Make sure that we get Dns data and sorting by uniqueDomains ascending', () => { return client @@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) { query: networkDnsQuery, variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, isPtrIncluded: false, pagination: { @@ -65,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) { query: networkDnsQuery, variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], isDnsHistogram: false, inspect: false, isPtrIncluded: false, diff --git a/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts b/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts index bbe934d840debe..6033fdfefa4db7 100644 --- a/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_top_n_flow.ts @@ -24,8 +24,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2019-02-09T01:57:24.870Z').valueOf(); - const TO = new Date('2019-02-12T01:57:24.870Z').valueOf(); + const FROM = '2019-02-09T01:57:24.870Z'; + const TO = '2019-02-12T01:57:24.870Z'; it('Make sure that we get Source NetworkTopNFlow data with bytes_in descending sort', () => { return client @@ -47,6 +47,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -84,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -121,6 +123,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -155,6 +158,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 20, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/overview_host.ts b/x-pack/test/api_integration/apis/security_solution/overview_host.ts index 1224fe3bd7dddb..ffbf9d89fc112e 100644 --- a/x-pack/test/api_integration/apis/security_solution/overview_host.ts +++ b/x-pack/test/api_integration/apis/security_solution/overview_host.ts @@ -19,8 +19,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/overview')); after(() => esArchiver.unload('auditbeat/overview')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatAuditd: 2194, auditbeatFIM: 4, @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: DEFAULT_INDEX_PATTERN, + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/overview_network.ts b/x-pack/test/api_integration/apis/security_solution/overview_network.ts index b7f4184f2eeca1..6976b225a4d2ad 100644 --- a/x-pack/test/api_integration/apis/security_solution/overview_network.ts +++ b/x-pack/test/api_integration/apis/security_solution/overview_network.ts @@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('filebeat/default')); after(() => esArchiver.unload('filebeat/default')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatSocket: 0, @@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -59,8 +60,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('packetbeat/overview')); after(() => esArchiver.unload('packetbeat/overview')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatSocket: 0, filebeatCisco: 0, @@ -86,6 +87,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -100,8 +102,8 @@ export default function ({ getService }: FtrProviderContext) { before(() => esArchiver.load('auditbeat/overview')); after(() => esArchiver.unload('auditbeat/overview')); - const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); - const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const FROM = '2000-01-01T00:00:00.000Z'; + const TO = '3000-01-01T00:00:00.000Z'; const expectedResult = { auditbeatSocket: 0, filebeatCisco: 0, @@ -127,6 +129,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts index 12e2378037c0a6..10ba9621c04305 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, title: 'some title', - dateRange: { start: 1560195800755, end: 1560282200756 }, + dateRange: { start: '2019-06-10T19:43:20.755Z', end: '2019-06-11T19:43:20.756Z' }, sort: { columnId: '@timestamp', sortDirection: 'desc' }, }; const response = await client.mutate({ diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index 7b4df5e23ca263..a9bbf09a9e6f96 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -25,6 +25,7 @@ export default function ({ getService }: FtrProviderContext) { variables: { sourceId: 'default', defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline.ts b/x-pack/test/api_integration/apis/security_solution/timeline.ts index 9d4084a0e41b08..5bd015a130a5a4 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline.ts @@ -13,8 +13,8 @@ import { } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const LTE = new Date('3000-01-01T00:00:00.000Z').valueOf(); -const GTE = new Date('2000-01-01T00:00:00.000Z').valueOf(); +const TO = '3000-01-01T00:00:00.000Z'; +const FROM = '2000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const DATA_COUNT = 2; @@ -37,13 +37,13 @@ const FILTER_VALUE = { filter: [ { bool: { - should: [{ range: { '@timestamp': { gte: GTE } } }], + should: [{ range: { '@timestamp': { gte: FROM } } }], minimum_should_match: 1, }, }, { bool: { - should: [{ range: { '@timestamp': { lte: LTE } } }], + should: [{ range: { '@timestamp': { lte: TO } } }], minimum_should_match: 1, }, }, @@ -80,7 +80,13 @@ export default function ({ getService }: FtrProviderContext) { }, fieldRequested: ['@timestamp', 'host.name'], defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, }, }) .then((resp) => { @@ -110,7 +116,13 @@ export default function ({ getService }: FtrProviderContext) { }, fieldRequested: ['@timestamp', 'host.name'], defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 3524d7bf2db075..35f419fde894d6 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -314,6 +314,7 @@ export default function ({ getService }: FtrProviderContext) { indexName: INDEX_NAME, eventId: ID, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], }, }) .then((resp) => { diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index cbddcf6b0f9353..e5f6233d50d594 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -14,8 +14,8 @@ import { } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; const SOURCE_IP = '10.128.0.35'; const DESTINATION_IP = '74.125.129.95'; @@ -117,6 +117,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -149,6 +150,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -186,6 +188,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) @@ -217,6 +220,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 10, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }) 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 a08ba8d8a7cd15..f1e064bcc37bb3 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 @@ -10,8 +10,8 @@ import { uncommonProcessesQuery } from '../../../../plugins/security_solution/pu import { GetUncommonProcessesQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const TOTAL_COUNT = 3; @@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); @@ -72,6 +73,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 2, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); @@ -99,6 +101,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); @@ -126,6 +129,7 @@ export default function ({ getService }: FtrProviderContext) { querySize: 1, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], inspect: false, }, }); diff --git a/x-pack/test/api_integration/apis/security_solution/users.ts b/x-pack/test/api_integration/apis/security_solution/users.ts index eb7fba88a6a469..abb2c5b2f5bbdd 100644 --- a/x-pack/test/api_integration/apis/security_solution/users.ts +++ b/x-pack/test/api_integration/apis/security_solution/users.ts @@ -14,8 +14,8 @@ import { } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); -const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); +const FROM = '2000-01-01T00:00:00.000Z'; +const TO = '3000-01-01T00:00:00.000Z'; const IP = '0.0.0.0'; export default function ({ getService }: FtrProviderContext) { @@ -38,6 +38,7 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + docValueFields: [], ip: IP, flowTarget: FlowTarget.destination, sort: { field: UsersFields.name, direction: Direction.asc }, From 8da80fe82781bdf86f8e3c369dd66bab75102a71 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 14 Jul 2020 15:39:26 -0600 Subject: [PATCH 18/26] [Security] Adds field mapping support to rule creation Part II (#71402) ## Summary Followup to https://github.com/elastic/kibana/pull/70288, which includes: - [X] Rule Execution logic for: - [X] Severity Override - [X] Risk Score Override - [X] Rule Name Override - [X] Timestamp Override - [X] Support for toggling display of Building Block Rules: - [X] Main Detections Page - [X] Rule Details Page - [X] Integrates `AutocompleteField` for: - [X] Severity Override - [X] Risk Score Override - [X] Rule Name Override - [X] Timestamp Override - [X] Fixes rehydration of `EditAboutStep` in `Edit Rule` - [X] Fixes `Rule Details` Description rollup Additional followup cleanup: - [ ] Adds risk_score` to `risk_score_mapping` - [ ] Improves field validation - [ ] Disables override fields for ML Rules - [ ] Orders `SeverityMapping` by `severity` on create/update - [ ] Allow unbounded max-signals ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - Syncing w/ @benskelker - [X] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ### For maintainers - [X] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../schemas/common/schemas.ts | 1 + .../common/components/autocomplete/field.tsx | 16 ++- .../autocomplete/field_value_match.tsx | 3 + .../common/components/utility_bar/index.ts | 1 + .../common/components/utility_bar/styles.tsx | 33 ++++- .../utility_bar/utility_bar_group.tsx | 8 +- .../utility_bar/utility_bar_section.tsx | 8 +- .../utility_bar/utility_bar_spacer.tsx | 19 +++ .../alerts_utility_bar/index.test.tsx | 2 + .../alerts_table/alerts_utility_bar/index.tsx | 42 ++++++- .../alerts_utility_bar/translations.ts | 14 +++ .../alerts_table/default_config.tsx | 19 +++ .../components/alerts_table/index.test.tsx | 2 + .../components/alerts_table/index.tsx | 8 ++ .../components/alerts_table/translations.ts | 6 +- .../rules/autocomplete_field/index.tsx | 75 +++++++++++ .../rules/description_step/helpers.test.tsx | 17 ++- .../rules/description_step/helpers.tsx | 75 ++++++++++- .../rules/description_step/index.test.tsx | 4 +- .../rules/description_step/index.tsx | 15 +-- .../rules/risk_score_mapping/index.tsx | 103 ++++++++++----- .../rules/risk_score_mapping/translations.tsx | 7 ++ .../rules/severity_mapping/index.tsx | 119 ++++++++++++++---- .../rules/severity_mapping/translations.tsx | 7 ++ .../rules/step_about_rule/index.tsx | 69 +++++----- .../rules/step_about_rule/translations.ts | 6 + .../detection_engine/detection_engine.tsx | 27 +++- .../rules/create/helpers.test.ts | 8 -- .../detection_engine/rules/create/helpers.ts | 4 +- .../detection_engine/rules/details/index.tsx | 29 ++++- .../detection_engine/rules/edit/index.tsx | 3 +- .../pages/detection_engine/rules/types.ts | 4 +- .../signals/build_bulk_body.ts | 1 + .../signals/build_events_query.test.ts | 6 + .../signals/build_events_query.ts | 11 +- .../signals/build_rule.test.ts | 5 +- .../detection_engine/signals/build_rule.ts | 34 ++++- .../signals/find_threshold_signals.ts | 1 + .../build_risk_score_from_mapping.test.ts | 26 ++++ .../mappings/build_risk_score_from_mapping.ts | 42 +++++++ .../build_rule_name_from_mapping.test.ts | 26 ++++ .../mappings/build_rule_name_from_mapping.ts | 40 ++++++ .../build_severity_from_mapping.test.ts | 26 ++++ .../mappings/build_severity_from_mapping.ts | 50 ++++++++ .../signals/search_after_bulk_create.ts | 1 + .../signals/single_search_after.test.ts | 3 + .../signals/single_search_after.ts | 4 + 47 files changed, 874 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 542cbe89160329..273ea72a2ffe30 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -255,6 +255,7 @@ export const severity_mapping_item = t.exact( severity, }) ); +export type SeverityMappingItem = t.TypeOf; export const severity_mapping = t.array(severity_mapping_item); export type SeverityMapping = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx index 8a6f049c960372..ed844b5130c77f 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx @@ -17,6 +17,7 @@ interface OperatorProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldTypeFilter?: string[]; fieldInputWidth?: number; onChange: (a: IFieldType[]) => void; } @@ -28,13 +29,22 @@ export const FieldComponent: React.FC = ({ isLoading = false, isDisabled = false, isClearable = false, + fieldTypeFilter = [], fieldInputWidth = 190, onChange, }): JSX.Element => { const getLabel = useCallback((field): string => field.name, []); - const optionsMemo = useMemo((): IFieldType[] => (indexPattern ? indexPattern.fields : []), [ - indexPattern, - ]); + const optionsMemo = useMemo((): IFieldType[] => { + if (indexPattern != null) { + if (fieldTypeFilter.length > 0) { + return indexPattern.fields.filter((f) => fieldTypeFilter.includes(f.type)); + } else { + return indexPattern.fields; + } + } else { + return []; + } + }, [fieldTypeFilter, indexPattern]); const selectedOptionsMemo = useMemo((): IFieldType[] => (selectedField ? [selectedField] : []), [ selectedField, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 4d96d6638132b3..32a82af114baed 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -22,6 +22,7 @@ interface AutocompleteFieldMatchProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldInputWidth?: number; onChange: (arg: string) => void; } @@ -33,6 +34,7 @@ export const AutocompleteFieldMatchComponent: React.FC { const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ @@ -97,6 +99,7 @@ export const AutocompleteFieldMatchComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts b/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts index b07fe8bb847c7b..44e19a951b6acd 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts @@ -8,4 +8,5 @@ export { UtilityBar } from './utility_bar'; export { UtilityBarAction } from './utility_bar_action'; export { UtilityBarGroup } from './utility_bar_group'; export { UtilityBarSection } from './utility_bar_section'; +export { UtilityBarSpacer } from './utility_bar_spacer'; export { UtilityBarText } from './utility_bar_text'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx index e1554da491a8b3..dd6b66350052e7 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx @@ -14,6 +14,14 @@ export interface BarProps { border?: boolean; } +export interface BarSectionProps { + grow?: boolean; +} + +export interface BarGroupProps { + grow?: boolean; +} + export const Bar = styled.aside.attrs({ className: 'siemUtilityBar', })` @@ -36,8 +44,8 @@ Bar.displayName = 'Bar'; export const BarSection = styled.div.attrs({ className: 'siemUtilityBar__section', -})` - ${({ theme }) => css` +})` + ${({ grow, theme }) => css` & + & { margin-top: ${theme.eui.euiSizeS}; } @@ -53,14 +61,18 @@ export const BarSection = styled.div.attrs({ margin-left: ${theme.eui.euiSize}; } } + ${grow && + css` + flex: 1; + `} `} `; BarSection.displayName = 'BarSection'; export const BarGroup = styled.div.attrs({ className: 'siemUtilityBar__group', -})` - ${({ theme }) => css` +})` + ${({ grow, theme }) => css` align-items: flex-start; display: flex; flex-wrap: wrap; @@ -93,6 +105,10 @@ export const BarGroup = styled.div.attrs({ margin-right: 0; } } + ${grow && + css` + flex: 1; + `} `} `; BarGroup.displayName = 'BarGroup'; @@ -118,3 +134,12 @@ export const BarAction = styled.div.attrs({ `} `; BarAction.displayName = 'BarAction'; + +export const BarSpacer = styled.div.attrs({ + className: 'siemUtilityBar__spacer', +})` + ${() => css` + flex: 1; + `} +`; +BarSpacer.displayName = 'BarSpacer'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx index 723035df672a93..d67be4882ceec1 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx @@ -6,14 +6,14 @@ import React from 'react'; -import { BarGroup } from './styles'; +import { BarGroup, BarGroupProps } from './styles'; -export interface UtilityBarGroupProps { +export interface UtilityBarGroupProps extends BarGroupProps { children: React.ReactNode; } -export const UtilityBarGroup = React.memo(({ children }) => ( - {children} +export const UtilityBarGroup = React.memo(({ grow, children }) => ( + {children} )); UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx index 42532c03556073..d88ec35f977c34 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx @@ -6,14 +6,14 @@ import React from 'react'; -import { BarSection } from './styles'; +import { BarSection, BarSectionProps } from './styles'; -export interface UtilityBarSectionProps { +export interface UtilityBarSectionProps extends BarSectionProps { children: React.ReactNode; } -export const UtilityBarSection = React.memo(({ children }) => ( - {children} +export const UtilityBarSection = React.memo(({ grow, children }) => ( + {children} )); UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx new file mode 100644 index 00000000000000..f57b300266f7b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { BarSpacer } from './styles'; + +export interface UtilityBarSpacerProps { + dataTestSubj?: string; +} + +export const UtilityBarSpacer = React.memo(({ dataTestSubj }) => ( + +)); + +UtilityBarSpacer.displayName = 'UtilityBarSpacer'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 7c884d773209ad..cbbe43cc03568f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -24,6 +24,8 @@ describe('AlertsUtilityBar', () => { currentFilter="closed" selectAll={jest.fn()} showClearSelection={true} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateAlertsStatus={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 6533be1a9b09c9..bedc23790541c2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -8,8 +8,9 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback } from 'react'; import numeral from '@elastic/numeral'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; import styled from 'styled-components'; + import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Link } from '../../../../common/components/link_icon'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; @@ -18,6 +19,7 @@ import { UtilityBarAction, UtilityBarGroup, UtilityBarSection, + UtilityBarSpacer, UtilityBarText, } from '../../../../common/components/utility_bar'; import * as i18n from './translations'; @@ -34,6 +36,8 @@ interface AlertsUtilityBarProps { currentFilter: Status; selectAll: () => void; selectedEventIds: Readonly>; + showBuildingBlockAlerts: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; showClearSelection: boolean; totalCount: number; updateAlertsStatus: UpdateAlertsStatus; @@ -52,6 +56,8 @@ const AlertsUtilityBarComponent: React.FC = ({ selectedEventIds, currentFilter, selectAll, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, showClearSelection, updateAlertsStatus, }) => { @@ -125,17 +131,36 @@ const AlertsUtilityBarComponent: React.FC = ({ ); + const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( + + + ) => { + closePopover(); + onShowBuildingBlockAlertsChanged(e.target.checked); + }} + checked={showBuildingBlockAlerts} + color="text" + data-test-subj="showBuildingBlockAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} + /> + + + ); + return ( <> - + {i18n.SHOWING_ALERTS(formattedTotalCount, totalCount)} - + {canUserCRUD && hasIndexWrite && ( <> @@ -174,6 +199,17 @@ const AlertsUtilityBarComponent: React.FC = ({ )} + + + {i18n.ADDITIONAL_FILTERS_ACTIONS} + diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts index 51e1b6f6e4c46a..eb4ca405b084e2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts @@ -27,6 +27,20 @@ export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: num 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', }); +export const ADDITIONAL_FILTERS_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle', + { + defaultMessage: 'Additional filters', + } +); + +export const ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle', + { + defaultMessage: 'Include building block alerts', + } +); + export const CLEAR_SELECTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 6f1f2e46dce3d9..71cf5c10de7644 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -81,6 +81,25 @@ export const buildAlertsRuleIdFilter = (ruleId: string): Filter[] => [ }, ]; +export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): Filter[] => [ + ...(showBuildingBlockAlerts + ? [] + : [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'exists', + key: 'signal.rule.building_block_type', + value: 'exists', + }, + // @ts-ignore TODO: Rework parent typings to support ExistsFilter[] + exists: { field: 'signal.rule.building_block_type' }, + }, + ]), +]; + export const alertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index 563f2ea60cded5..cc3a47017a835a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -37,6 +37,8 @@ describe('AlertsTableComponent', () => { clearEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} clearEventsDeleted={jest.fn()} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateTimelineIsLoading={jest.fn()} updateTimeline={jest.fn()} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 391598ebda03d6..87c631b80e38ba 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -64,6 +64,8 @@ interface OwnProps { hasIndexWrite: boolean; from: string; loading: boolean; + showBuildingBlockAlerts: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; signalsIndex: string; to: string; } @@ -94,6 +96,8 @@ export const AlertsTableComponent: React.FC = ({ selectedEventIds, setEventsDeleted, setEventsLoading, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, signalsIndex, to, updateTimeline, @@ -302,6 +306,8 @@ export const AlertsTableComponent: React.FC = ({ currentFilter={filterGroup} selectAll={selectAllCallback} selectedEventIds={selectedEventIds} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} showClearSelection={showClearSelectionAction} totalCount={totalCount} updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} @@ -313,6 +319,8 @@ export const AlertsTableComponent: React.FC = ({ hasIndexWrite, clearSelectionCallback, filterGroup, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, loadingEventIds.length, selectAllCallback, selectedEventIds, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 0f55469bbfda22..e5e8635b9e7999 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -20,21 +20,21 @@ export const ALERTS_DOCUMENT_TYPE = i18n.translate( export const OPEN_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.openAlertsTitle', { - defaultMessage: 'Open alerts', + defaultMessage: 'Open', } ); export const CLOSED_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.closedAlertsTitle', { - defaultMessage: 'Closed alerts', + defaultMessage: 'Closed', } ); export const IN_PROGRESS_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.inProgressAlertsTitle', { - defaultMessage: 'In progress alerts', + defaultMessage: 'In progress', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx new file mode 100644 index 00000000000000..03465118741043 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; + +interface AutocompleteFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + indices: IIndexPattern; + isDisabled: boolean; + fieldType: string; + placeholder?: string; +} + +export const AutocompleteField = ({ + dataTestSubj, + field, + idAria, + indices, + isDisabled, + fieldType, + placeholder, +}: AutocompleteFieldProps) => { + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { + // TODO: Update onChange type in FieldComponent as newField can be undefined + field.setValue(newField?.name ?? ''); + }, + [field] + ); + + const selectedField = useMemo(() => { + const existingField = (field.value as string) ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const fieldTypeFilter = useMemo(() => [fieldType], [fieldType]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 41ee91845a8ec8..2a6cd3fc5bb7a4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { EuiLoadingSpinner } from '@elastic/eui'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; @@ -328,10 +328,19 @@ describe('helpers', () => { describe('buildSeverityDescription', () => { test('returns ListItem with passed in label and SeverityBadge component', () => { - const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + const result: ListItems[] = buildSeverityDescription({ + value: 'low', + mapping: [{ field: 'host.name', operator: 'equals', value: 'hello', severity: 'high' }], + }); - expect(result[0].title).toEqual('Test label'); - expect(result[0].description).toEqual(); + expect(result[0].title).toEqual('Severity'); + expect(result[0].description).toEqual(); + expect(result[1].title).toEqual('Severity override'); + + const wrapper = mount(result[1].description as React.ReactElement); + expect(wrapper.find('[data-test-subj="severityOverrideSeverity0"]').first().text()).toEqual( + 'High' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 8393f2230dcfef..1110c8c098988b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -13,12 +13,16 @@ import { EuiSpacer, EuiLink, EuiText, + EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import * as i18nSeverity from '../severity_mapping/translations'; +import * as i18nRiskScore from '../risk_score_mapping/translations'; import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; import { RuleType } from '../../../../../common/detection_engine/types'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -30,6 +34,7 @@ import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './t import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; import { assertUnreachable } from '../../../../common/lib/helpers'; +import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; const NoteDescriptionContainer = styled(EuiFlexItem)` height: 105px; @@ -219,11 +224,75 @@ export const buildStringArrayDescription = ( return []; }; -export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ +const OverrideColumn = styled(EuiFlexItem)` + width: 125px; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const buildSeverityDescription = (severity: AboutStepSeverity): ListItems[] => [ { - title: label, - description: , + title: i18nSeverity.DEFAULT_SEVERITY, + description: , + }, + ...severity.mapping.map((severityItem, index) => { + return { + title: index === 0 ? i18nSeverity.SEVERITY_MAPPING : '', + description: ( + + + + <>{severityItem.field} + + + + <>{severityItem.value} + + + + + + + + + ), + }; + }), +]; + +export const buildRiskScoreDescription = (riskScore: AboutStepRiskScore): ListItems[] => [ + { + title: i18nRiskScore.RISK_SCORE, + description: riskScore.value, }, + ...riskScore.mapping.map((riskScoreItem, index) => { + return { + title: index === 0 ? i18nRiskScore.RISK_SCORE_MAPPING : '', + description: ( + + + + <>{riskScoreItem.field} + + + + + + {'signal.rule.risk_score'} + + ), + }; + }), ]; const MyRefUrlLink = styled(EuiLink)` diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 5a2a44a284e3b4..4a2d17ec126fb7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -450,7 +450,7 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual('Severity label'); + expect(result[0].title).toEqual('Severity'); expect(React.isValidElement(result[0].description)).toBeTruthy(); }); }); @@ -464,7 +464,7 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual('Risk score label'); + expect(result[0].title).toEqual('Risk score'); expect(result[0].description).toEqual(21); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 51624d04cb58b1..0b341050fa9d58 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -34,6 +34,7 @@ import { buildUnorderedListArrayDescription, buildUrlsDescription, buildNoteDescription, + buildRiskScoreDescription, buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; @@ -192,18 +193,12 @@ export const getDescriptionItem = ( } else if (Array.isArray(get(field, data))) { const values: string[] = get(field, data); return buildStringArrayDescription(label, field, values); - // TODO: Add custom UI for Risk/Severity Mappings (and fix missing label) } else if (field === 'riskScore') { - const val: AboutStepRiskScore = get(field, data); - return [ - { - title: label, - description: val.value, - }, - ]; + const values: AboutStepRiskScore = get(field, data); + return buildRiskScoreDescription(values); } else if (field === 'severity') { - const val: AboutStepSeverity = get(field, data); - return buildSeverityDescription(label, val.value); + const values: AboutStepSeverity = get(field, data); + return buildSeverityDescription(values); } else if (field === 'timeline') { const timeline = get(field, data) as FieldValueTimeline; return [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index bdf1ac600faef6..c9e2cb1a8ca24f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,12 +14,15 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; const NestedContent = styled.div` margin-left: 24px; @@ -38,20 +40,47 @@ interface RiskScoreFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; + placeholder?: string; } -export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskScoreFieldProps) => { - const [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected] = useState(false); +export const RiskScoreField = ({ + dataTestSubj, + field, + idAria, + indices, + placeholder, +}: RiskScoreFieldProps) => { + const [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); - const updateRiskScoreMapping = useCallback( - (event) => { + const fieldTypeFilter = useMemo(() => ['number'], []); + + useEffect(() => { + if ( + !isRiskScoreMappingChecked && + initialFieldCheck && + (field.value as AboutStepRiskScore).mapping?.length > 0 + ) { + setIsRiskScoreMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isRiskScoreMappingChecked, + setIsRiskScoreMappingChecked, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { const values = field.value as AboutStepRiskScore; field.setValue({ value: values.value, mapping: [ { - field: event.target.value, + field: newField?.name ?? '', operator: 'equals', value: '', }, @@ -61,11 +90,23 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco [field] ); - const severityLabel = useMemo(() => { + const selectedField = useMemo(() => { + const existingField = (field.value as AboutStepRiskScore).mapping?.[0]?.field ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const handleRiskScoreMappingChecked = useCallback(() => { + setIsRiskScoreMappingChecked(!isRiskScoreMappingChecked); + }, [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked]); + + const riskScoreLabel = useMemo(() => { return (
- {i18n.RISK_SCORE} + {i18n.DEFAULT_RISK_SCORE} {i18n.RISK_SCORE_DESCRIPTION} @@ -73,19 +114,15 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco ); }, []); - const severityMappingLabel = useMemo(() => { + const riskScoreMappingLabel = useMemo(() => { return (
- setIsRiskScoreMappingSelected(!isRiskScoreMappingSelected)} - > + setIsRiskScoreMappingSelected(e.target.checked)} + checked={isRiskScoreMappingChecked} + onChange={handleRiskScoreMappingChecked} /> {i18n.RISK_SCORE_MAPPING} @@ -96,13 +133,13 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco
); - }, [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected]); + }, [handleRiskScoreMappingChecked, isRiskScoreMappingChecked]); return ( {i18n.RISK_SCORE_MAPPING_DETAILS} ) : ( '' @@ -147,7 +184,7 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco > - {isRiskScoreMappingSelected && ( + {isRiskScoreMappingChecked && ( @@ -156,7 +193,7 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco - {i18n.RISK_SCORE} + {i18n.DEFAULT_RISK_SCORE} @@ -164,12 +201,18 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco - diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx index a75bf19b5b3c4f..24e82a8f95a6b6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx @@ -8,6 +8,13 @@ import { i18n } from '@kbn/i18n'; export const RISK_SCORE = i18n.translate( 'xpack.securitySolution.alerts.riskScoreMapping.riskScoreTitle', + { + defaultMessage: 'Risk score', + } +); + +export const DEFAULT_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle', { defaultMessage: 'Default risk score', } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 47c45a6bdf88da..579c60579b32ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,14 +14,23 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; +import { + IFieldType, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; +const SeverityMappingParentContainer = styled(EuiFlexItem)` + max-width: 471px; +`; const NestedContent = styled.div` margin-left: 24px; `; @@ -39,7 +47,7 @@ interface SeverityFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; options: SeverityOptionItem[]; } @@ -47,13 +55,32 @@ export const SeverityField = ({ dataTestSubj, field, idAria, - indices, // TODO: To be used with autocomplete fields once https://github.com/elastic/kibana/pull/67013 is merged + indices, options, }: SeverityFieldProps) => { const [isSeverityMappingChecked, setIsSeverityMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); + const fieldValueInputWidth = 160; - const updateSeverityMapping = useCallback( - (index: number, severity: string, mappingField: string, event) => { + useEffect(() => { + if ( + !isSeverityMappingChecked && + initialFieldCheck && + (field.value as AboutStepSeverity).mapping?.length > 0 + ) { + setIsSeverityMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isSeverityMappingChecked, + setIsSeverityMappingChecked, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + (index: number, severity: string, [newField]: IFieldType[]): void => { const values = field.value as AboutStepSeverity; field.setValue({ value: values.value, @@ -61,7 +88,7 @@ export const SeverityField = ({ ...values.mapping.slice(0, index), { ...values.mapping[index], - [mappingField]: event.target.value, + field: newField?.name ?? '', operator: 'equals', severity, }, @@ -72,6 +99,41 @@ export const SeverityField = ({ [field] ); + const handleFieldMatchValueChange = useCallback( + (index: number, severity: string, newMatchValue: string): void => { + const values = field.value as AboutStepSeverity; + field.setValue({ + value: values.value, + mapping: [ + ...values.mapping.slice(0, index), + { + ...values.mapping[index], + value: newMatchValue, + operator: 'equals', + severity, + }, + ...values.mapping.slice(index + 1), + ], + }); + }, + [field] + ); + + const selectedState = useMemo(() => { + return ( + (field.value as AboutStepSeverity).mapping?.map((mapping) => { + const [newSelectedField] = indices.fields.filter( + ({ name }) => mapping.field != null && mapping.field === name + ); + return { field: newSelectedField, value: mapping.value }; + }) ?? [] + ); + }, [field.value, indices]); + + const handleSeverityMappingSelected = useCallback(() => { + setIsSeverityMappingChecked(!isSeverityMappingChecked); + }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + const severityLabel = useMemo(() => { return (
@@ -87,16 +149,12 @@ export const SeverityField = ({ const severityMappingLabel = useMemo(() => { return (
- setIsSeverityMappingChecked(!isSeverityMappingChecked)} - > + setIsSeverityMappingChecked(e.target.checked)} + onChange={handleSeverityMappingSelected} /> {i18n.SEVERITY_MAPPING} @@ -107,7 +165,7 @@ export const SeverityField = ({
); - }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + }, [handleSeverityMappingSelected, isSeverityMappingChecked]); return ( @@ -137,7 +195,7 @@ export const SeverityField = ({ - + - {i18n.SEVERITY} + {i18n.DEFAULT_SEVERITY} @@ -177,22 +235,33 @@ export const SeverityField = ({ - - @@ -208,7 +277,7 @@ export const SeverityField = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx index 9c9784bac6b63a..f0bfc5f4637ab1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx @@ -13,6 +13,13 @@ export const SEVERITY = i18n.translate( } ); +export const DEFAULT_SEVERITY = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.defaultSeverityTitle', + { + defaultMessage: 'Severity', + } +); + export const SOURCE_FIELD = i18n.translate( 'xpack.securitySolution.alerts.severityMapping.sourceFieldTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 7f7ee94ed85b7a..3616643874a0ae 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -38,6 +38,8 @@ import { MarkdownEditorForm } from '../../../../common/components/markdown_edito import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; +import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; +import { AutocompleteField } from '../autocomplete_field'; const CommonUseField = getUseField({ component: Field }); @@ -90,6 +92,9 @@ const StepAboutRuleComponent: FC = ({ setStepData, }) => { const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + defineRuleData?.index ?? [] + ); const { form } = useForm({ defaultValue: myStepData, @@ -149,7 +154,6 @@ const StepAboutRuleComponent: FC = ({ }} /> - = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleSeverityField', idAria: 'detectionEngineStepAboutRuleSeverityField', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, options: severityOptions, - indices: defineRuleData?.index ?? [], + indices: indexPatterns, }} /> @@ -184,7 +188,8 @@ const StepAboutRuleComponent: FC = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleRiskScore', idAria: 'detectionEngineStepAboutRuleRiskScore', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, + indices: indexPatterns, }} /> @@ -196,7 +201,7 @@ const StepAboutRuleComponent: FC = ({ 'data-test-subj': 'detectionEngineStepAboutRuleTags', euiFieldProps: { fullWidth: true, - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, placeholder: '', }, }} @@ -277,7 +282,7 @@ const StepAboutRuleComponent: FC = ({ }} /> - + = ({ /> - - - + - - - + {({ severity }) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index c179128c56d924..3a5aa3c56c3df3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -26,6 +26,12 @@ export const ADD_FALSE_POSITIVE = i18n.translate( defaultMessage: 'Add false positive example', } ); +export const BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel', + { + defaultMessage: 'Building block', + } +); export const LOW = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index cdff8ea4ab928a..aef9f2adcbcc80 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,7 +5,7 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -39,6 +39,7 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; +import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; export const DetectionEnginePageComponent: React.FC = ({ filters, @@ -62,6 +63,7 @@ export const DetectionEnginePageComponent: React.FC = ({ const history = useHistory(); const [lastAlerts] = useAlertInfo({}); const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; const updateDateRangeCallback = useCallback( @@ -87,6 +89,24 @@ export const DetectionEnginePageComponent: React.FC = ({ [history] ); + const alertsHistogramDefaultFilters = useMemo( + () => [...filters, ...buildShowBuildingBlockFilter(showBuildingBlockAlerts)], + [filters, showBuildingBlockAlerts] + ); + + // AlertsTable manages global filters itself, so not including `filters` + const alertsTableDefaultFilters = useMemo( + () => buildShowBuildingBlockFilter(showBuildingBlockAlerts), + [showBuildingBlockAlerts] + ); + + const onShowBuildingBlockAlertsChangedCallback = useCallback( + (newShowBuildingBlockAlerts: boolean) => { + setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); + }, + [setShowBuildingBlockAlerts] + ); + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); @@ -145,7 +165,7 @@ export const DetectionEnginePageComponent: React.FC = ({ = ({ hasIndexWrite={hasIndexWrite ?? false} canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} from={from} + defaultFilters={alertsTableDefaultFilters} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} signalsIndex={signalIndexName ?? ''} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index f402303c4c6212..745518b90df005 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -348,7 +348,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -369,7 +368,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -392,7 +390,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -413,7 +410,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -434,7 +430,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -455,7 +450,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -508,7 +502,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -519,7 +512,6 @@ describe('helpers', () => { technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 4bb7196e17db57..c419dd142cfbe6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -167,7 +167,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule references: references.filter((item) => !isEmpty(item)), risk_score: riskScore.value, risk_score_mapping: riskScore.mapping, - rule_name_override: ruleNameOverride, + rule_name_override: ruleNameOverride !== '' ? ruleNameOverride : undefined, severity: severity.value, severity_mapping: severity.mapping, threat: threat @@ -180,7 +180,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule return { id, name, reference }; }), })), - timestamp_override: timestampOverride, + timestamp_override: timestampOverride !== '' ? timestampOverride : undefined, ...(!isEmpty(note) ? { note } : {}), ...rest, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 45a1c89cec621c..2e7ef1180f4e34 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC, memo, useCallback, useMemo, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -48,7 +48,10 @@ import { OverviewEmpty } from '../../../../../overview/components/overview_empty import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; -import { buildAlertsRuleIdFilter } from '../../../../components/alerts_table/default_config'; +import { + buildAlertsRuleIdFilter, + buildShowBuildingBlockFilter, +} from '../../../../components/alerts_table/default_config'; import { NoWriteAlertsCallOut } from '../../../../components/no_write_alerts_callout'; import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../../../../components/rules/read_only_callout'; @@ -134,6 +137,7 @@ export const RuleDetailsPageComponent: FC = ({ scheduleRuleData: null, }; const [lastAlerts] = useAlertInfo({ ruleId }); + const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); @@ -184,9 +188,17 @@ export const RuleDetailsPageComponent: FC = ({ [isLoading, rule] ); + // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts + useEffect(() => { + setShowBuildingBlockAlerts(rule?.building_block_type != null); + }, [rule]); + const alertDefaultFilters = useMemo( - () => (ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), - [ruleId] + () => [ + ...(ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ], + [ruleId, showBuildingBlockAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ @@ -262,6 +274,13 @@ export const RuleDetailsPageComponent: FC = ({ [history, ruleId] ); + const onShowBuildingBlockAlertsChangedCallback = useCallback( + (newShowBuildingBlockAlerts: boolean) => { + setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); + }, + [setShowBuildingBlockAlerts] + ); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); const exceptionLists = useMemo((): { @@ -447,6 +466,8 @@ export const RuleDetailsPageComponent: FC = ({ hasIndexWrite={hasIndexWrite ?? false} from={from} loading={loading} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} signalsIndex={signalIndexName ?? ''} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 87cb5e77697b5f..0900cdb8f4789b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -160,12 +160,13 @@ const EditRulePageComponent: FC = () => { <> - {myAboutRuleForm.data != null && ( + {myAboutRuleForm.data != null && myDefineRuleForm.data != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index e7daff0947b0d5..b501536e5b387f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -145,10 +145,10 @@ export interface AboutStepRuleJson { risk_score_mapping: RiskScoreMapping; references: string[]; false_positives: string[]; - rule_name_override: RuleNameOverride; + rule_name_override?: RuleNameOverride; tags: string[]; threat: IMitreEnterpriseAttack[]; - timestamp_override: TimestampOverride; + timestamp_override?: TimestampOverride; note?: string; } 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 75c4d75cedf1db..218750ac30a2aa 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 @@ -51,6 +51,7 @@ export const buildBulkBody = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index 452ba958876d69..ccf8a9bec3159a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -15,6 +15,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -85,6 +86,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: '', + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -156,6 +158,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: fakeSortId, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -228,6 +231,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: fakeSortIdNumber, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -299,6 +303,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -377,6 +382,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, 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 dcf3a90364a401..96db7e1eb53b7b 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,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; + interface BuildEventsSearchQuery { aggregations?: unknown; index: string[]; @@ -12,6 +14,7 @@ interface BuildEventsSearchQuery { filter: unknown; size: number; searchAfterSortId: string | number | undefined; + timestampOverride: TimestampOverrideOrUndefined; } export const buildEventsSearchQuery = ({ @@ -22,7 +25,9 @@ export const buildEventsSearchQuery = ({ filter, size, searchAfterSortId, + timestampOverride, }: BuildEventsSearchQuery) => { + const timestamp = timestampOverride ?? '@timestamp'; const filterWithTime = [ filter, { @@ -33,7 +38,7 @@ export const buildEventsSearchQuery = ({ should: [ { range: { - '@timestamp': { + [timestamp]: { gte: from, }, }, @@ -47,7 +52,7 @@ export const buildEventsSearchQuery = ({ should: [ { range: { - '@timestamp': { + [timestamp]: { lte: to, }, }, @@ -79,7 +84,7 @@ export const buildEventsSearchQuery = ({ ...(aggregations ? { aggregations } : {}), sort: [ { - '@timestamp': { + [timestamp]: { order: 'asc', }, }, 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 ed632ee2576dc8..7257e5952ff055 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 @@ -5,7 +5,7 @@ */ import { buildRule } from './build_rule'; -import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; +import { sampleDocNoSortId, sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -29,6 +29,7 @@ describe('buildRule', () => { ]; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -97,6 +98,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -154,6 +156,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, 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 9e118f77a73e79..e02a0154d63c9b 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 @@ -8,6 +8,10 @@ import { pickBy } from 'lodash/fp'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; +import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapping'; +import { SignalSourceHit } from './types'; +import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping'; +import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping'; interface BuildRuleParams { ruleParams: RuleTypeParams; @@ -17,6 +21,7 @@ interface BuildRuleParams { enabled: boolean; createdAt: string; createdBy: string; + doc: SignalSourceHit; updatedAt: string; updatedBy: string; interval: string; @@ -32,12 +37,33 @@ export const buildRule = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, tags, throttle, }: BuildRuleParams): Partial => { + const { riskScore, riskScoreMeta } = buildRiskScoreFromMapping({ + doc, + riskScore: ruleParams.riskScore, + riskScoreMapping: ruleParams.riskScoreMapping, + }); + + const { severity, severityMeta } = buildSeverityFromMapping({ + doc, + severity: ruleParams.severity, + severityMapping: ruleParams.severityMapping, + }); + + const { ruleName, ruleNameMeta } = buildRuleNameFromMapping({ + doc, + ruleName: name, + ruleNameMapping: ruleParams.ruleNameOverride, + }); + + const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta }; + return pickBy((value: unknown) => value != null, { id, rule_id: ruleParams.ruleId ?? '(unknown rule_id)', @@ -48,9 +74,9 @@ export const buildRule = ({ saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, timeline_title: ruleParams.timelineTitle, - meta: ruleParams.meta, + meta: Object.keys(meta).length > 0 ? meta : undefined, max_signals: ruleParams.maxSignals, - risk_score: ruleParams.riskScore, // TODO: Risk Score Override via risk_score_mapping + risk_score: riskScore, risk_score_mapping: ruleParams.riskScoreMapping ?? [], output_index: ruleParams.outputIndex, description: ruleParams.description, @@ -61,11 +87,11 @@ export const buildRule = ({ interval, language: ruleParams.language, license: ruleParams.license, - name, // TODO: Rule Name Override via rule_name_override + name: ruleName, query: ruleParams.query, references: ruleParams.references, rule_name_override: ruleParams.ruleNameOverride, - severity: ruleParams.severity, // TODO: Severity Override via severity_mapping + severity, severity_mapping: ruleParams.severityMapping ?? [], tags, type: ruleParams.type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index a9a199f210da0f..251c043adb58be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -50,6 +50,7 @@ export const findThresholdSignals = async ({ return singleSearchAfter({ aggregations, searchAfterSortId: undefined, + timestampOverride: undefined, index: inputIndexPattern, from, to, 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 new file mode 100644 index 00000000000000..e1d9c7f7c8a5ca --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRiskScoreFromMapping } from './build_risk_score_from_mapping'; + +describe('buildRiskScoreFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('risk score defaults to provided if mapping is incomplete', () => { + const riskScore = buildRiskScoreFromMapping({ + doc: sampleDocNoSortId(), + riskScore: 57, + riskScoreMapping: undefined, + }); + + expect(riskScore).toEqual({ riskScore: 57, riskScoreMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts new file mode 100644 index 00000000000000..356cf95fc0d24f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + RiskScore, + RiskScoreMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; +import { RiskScore as RiskScoreIOTS } from '../../../../../common/detection_engine/schemas/types'; + +interface BuildRiskScoreFromMappingProps { + doc: SignalSourceHit; + riskScore: RiskScore; + riskScoreMapping: RiskScoreMappingOrUndefined; +} + +interface BuildRiskScoreFromMappingReturn { + riskScore: RiskScore; + riskScoreMeta: Meta; // TODO: Stricter types +} + +export const buildRiskScoreFromMapping = ({ + doc, + riskScore, + riskScoreMapping, +}: BuildRiskScoreFromMappingProps): BuildRiskScoreFromMappingReturn => { + // MVP support is for mapping from a single field + if (riskScoreMapping != null && riskScoreMapping.length > 0) { + const mappedField = riskScoreMapping[0].field; + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mappedField, doc._source); + // TODO: This doesn't seem to validate...identified riskScore > 100 😬 + if (RiskScoreIOTS.is(mappedValue)) { + return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } }; + } + } + return { riskScore, riskScoreMeta: {} }; +}; 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 new file mode 100644 index 00000000000000..b509020646d1b5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRuleNameFromMapping } from './build_rule_name_from_mapping'; + +describe('buildRuleNameFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('rule name defaults to provided if mapping is incomplete', () => { + const ruleName = buildRuleNameFromMapping({ + doc: sampleDocNoSortId(), + ruleName: 'rule-name', + ruleNameMapping: 'message', + }); + + expect(ruleName).toEqual({ ruleName: 'rule-name', ruleNameMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts new file mode 100644 index 00000000000000..af540ed1454adf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.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; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { get } from 'lodash/fp'; +import { + Meta, + Name, + RuleNameOverrideOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildRuleNameFromMappingProps { + doc: SignalSourceHit; + ruleName: Name; + ruleNameMapping: RuleNameOverrideOrUndefined; +} + +interface BuildRuleNameFromMappingReturn { + ruleName: Name; + ruleNameMeta: Meta; // TODO: Stricter types +} + +export const buildRuleNameFromMapping = ({ + doc, + ruleName, + ruleNameMapping, +}: BuildRuleNameFromMappingProps): BuildRuleNameFromMappingReturn => { + if (ruleNameMapping != null) { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(ruleNameMapping, doc._source); + if (t.string.is(mappedValue)) { + return { ruleName: mappedValue, ruleNameMeta: { ruleNameOverridden: true } }; + } + } + + return { ruleName, ruleNameMeta: {} }; +}; 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 new file mode 100644 index 00000000000000..80950335934f4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildSeverityFromMapping } from './build_severity_from_mapping'; + +describe('buildSeverityFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('severity defaults to provided if mapping is incomplete', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocNoSortId(), + severity: 'low', + severityMapping: undefined, + }); + + expect(severity).toEqual({ severity: 'low', severityMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts new file mode 100644 index 00000000000000..a3c4f47b491be1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + Severity, + SeverityMappingItem, + severity as SeverityIOTS, + SeverityMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildSeverityFromMappingProps { + doc: SignalSourceHit; + severity: Severity; + severityMapping: SeverityMappingOrUndefined; +} + +interface BuildSeverityFromMappingReturn { + severity: Severity; + severityMeta: Meta; // TODO: Stricter types +} + +export const buildSeverityFromMapping = ({ + doc, + severity, + severityMapping, +}: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { + if (severityMapping != null && severityMapping.length > 0) { + let severityMatch: SeverityMappingItem | undefined; + severityMapping.forEach((mapping) => { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mapping.field, doc._source); + if (mapping.value === mappedValue) { + severityMatch = { ...mapping }; + } + }); + + if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { + return { + severity: severityMatch.severity, + severityMeta: { severityOverrideField: severityMatch.field }, + }; + } + } + return { severity, severityMeta: {} }; +}; 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 f3025ead69a05f..2a0e39cbbf237f 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 @@ -144,6 +144,7 @@ export const searchAfterAndBulkCreate = async ({ logger, filter, pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + timestampOverride: ruleParams.timestampOverride, } ); toReturn.searchAfterTimes.push(searchDuration); 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 50b0cb27990f8f..250b891eb1f2cc 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 @@ -31,6 +31,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId); }); @@ -46,6 +47,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId); }); @@ -64,6 +66,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }) ).rejects.toThrow('Fake Error'); }); 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 daea277f143682..5667f2e47b6d71 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 @@ -10,6 +10,7 @@ import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; import { makeFloatString } from './utils'; +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { aggregations?: unknown; @@ -21,6 +22,7 @@ interface SingleSearchAfterParams { logger: Logger; pageSize: number; filter: unknown; + timestampOverride: TimestampOverrideOrUndefined; } // utilize search_after for paging results into bulk. @@ -34,6 +36,7 @@ export const singleSearchAfter = async ({ filter, logger, pageSize, + timestampOverride, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -47,6 +50,7 @@ export const singleSearchAfter = async ({ filter, size: pageSize, searchAfterSortId, + timestampOverride, }); const start = performance.now(); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( From 06b1820df71632d5ce30d0b5c60201e6d8c72063 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 14 Jul 2020 17:50:22 -0400 Subject: [PATCH 19/26] [Monitoring] Out of the box alerting (#68805) * First draft, not quite working but a good start * More working * Support configuring throttle * Get the other alerts working too * More * Separate into individual files * Menu support as well as better integration in existing UIs * Red borders! * New overview style, and renamed alert * more visual updates * Update cpu usage and improve settings configuration in UI * Convert cluster health and license expiration alert to use legacy data model * Remove most of the custom UI and use the flyout * Add the actual alerts * Remove more code * Fix formatting * Fix up some errors * Remove unnecessary code * Updates * add more links here * Fix up linkage * Added nodes changed alert * Most of the version mismatch working * Add kibana mismatch * UI tweaks * Add timestamp * Support actions in the enable api * Move this around * Better support for changing legacy alerts * Add missing files * Update alerts * Enable alerts whenever any page is visited in SM * Tweaks * Use more practical default * Remove the buggy renderer and ensure setup mode can show all alerts * Updates * Remove unnecessary code * Remove some dead code * Cleanup * Fix snapshot * Fixes * Fixes * Fix test * Add alerts to kibana and logstash listing pages * Fix test * Add disable/mute options * Tweaks * Fix linting * Fix i18n * Adding a couple tests * Fix localization * Use http * Ensure we properly handle when an alert is resolved * Fix tests * Hide legacy alerts if not the right license * Design tweaks * Fix tests * PR feedback * Moar tests * Fix i18n * Ensure we have a control over the messaging * Fix translations * Tweaks * More localization * Copy changes * Type --- x-pack/legacy/plugins/monitoring/index.ts | 4 - x-pack/plugins/monitoring/common/constants.ts | 51 +- .../{server/alerts => common}/enums.ts | 16 +- .../plugins/monitoring/common/formatting.js | 4 +- x-pack/plugins/monitoring/common/types.ts | 48 ++ x-pack/plugins/monitoring/kibana.json | 13 +- .../monitoring/public/alerts/badge.tsx | 179 +++++++ .../monitoring/public/alerts/callout.tsx | 81 ++++ .../cpu_usage_alert/cpu_usage_alert.tsx | 28 ++ .../alerts/cpu_usage_alert/expression.tsx | 61 +++ .../cpu_usage_alert/index.ts} | 2 +- .../alerts/cpu_usage_alert/validation.tsx | 35 ++ .../alert_param_duration.tsx | 98 ++++ .../alert_param_percentage.tsx | 41 ++ .../legacy_alert}/index.ts | 2 +- .../alerts/legacy_alert/legacy_alert.tsx | 39 ++ .../public/alerts/lib/replace_tokens.tsx | 93 ++++ .../alerts/lib/should_show_alert_badge.ts | 15 + .../monitoring/public/alerts/panel.tsx | 225 +++++++++ .../monitoring/public/alerts/status.tsx | 99 ++++ .../monitoring/public/angular/app_modules.ts | 12 +- .../monitoring/public/angular/index.ts | 6 +- .../alerts/__snapshots__/status.test.tsx.snap | 65 --- .../alerts/__tests__/map_severity.js | 65 --- .../public/components/alerts/alerts.js | 191 -------- .../__snapshots__/configuration.test.tsx.snap | 121 ----- .../__snapshots__/step1.test.tsx.snap | 301 ------------ .../__snapshots__/step2.test.tsx.snap | 49 -- .../__snapshots__/step3.test.tsx.snap | 95 ---- .../configuration/configuration.test.tsx | 140 ------ .../alerts/configuration/configuration.tsx | 193 -------- .../alerts/configuration/step1.test.tsx | 331 ------------- .../components/alerts/configuration/step1.tsx | 334 ------------- .../alerts/configuration/step2.test.tsx | 51 -- .../components/alerts/configuration/step2.tsx | 38 -- .../alerts/configuration/step3.test.tsx | 48 -- .../components/alerts/configuration/step3.tsx | 47 -- .../components/alerts/formatted_alert.js | 63 --- .../components/alerts/manage_email_action.tsx | 301 ------------ .../public/components/alerts/map_severity.js | 75 --- .../public/components/alerts/status.test.tsx | 85 ---- .../public/components/alerts/status.tsx | 207 -------- .../chart/monitoring_timeseries_container.js | 79 +-- .../cluster/listing/alerts_indicator.js | 87 ---- .../components/cluster/listing/listing.js | 10 +- .../cluster/overview/alerts_panel.js | 201 -------- .../cluster/overview/elasticsearch_panel.js | 168 +++++-- .../components/cluster/overview/helpers.js | 18 +- .../components/cluster/overview/index.js | 29 +- .../cluster/overview/kibana_panel.js | 26 +- .../cluster/overview/license_text.js | 42 -- .../cluster/overview/logstash_panel.js | 30 +- .../elasticsearch/cluster_status/index.js | 3 +- .../components/elasticsearch/node/node.js | 32 +- .../elasticsearch/node_detail_status/index.js | 6 +- .../components/elasticsearch/nodes/nodes.js | 67 ++- .../components/kibana/cluster_status/index.js | 3 +- .../components/kibana/instances/instances.js | 57 +-- .../monitoring/public/components/logs/logs.js | 2 +- .../public/components/logs/logs.test.js | 4 +- .../logstash/cluster_status/index.js | 4 +- .../__snapshots__/listing.test.js.snap | 14 + .../components/logstash/listing/listing.js | 19 +- .../public/components/renderers/setup_mode.js | 2 +- .../summary_status/summary_status.js | 15 + .../plugins/monitoring/public/legacy_shims.ts | 27 +- .../monitoring/public/lib/setup_mode.tsx | 11 + x-pack/plugins/monitoring/public/plugin.ts | 53 +- .../monitoring/public/services/clusters.js | 59 ++- x-pack/plugins/monitoring/public/types.ts | 4 +- x-pack/plugins/monitoring/public/url_state.ts | 6 +- .../monitoring/public/views/alerts/index.html | 3 - .../monitoring/public/views/alerts/index.js | 126 ----- x-pack/plugins/monitoring/public/views/all.js | 1 - .../public/views/base_controller.js | 35 +- .../public/views/cluster/overview/index.js | 18 +- .../public/views/elasticsearch/node/index.js | 14 +- .../public/views/elasticsearch/nodes/index.js | 15 +- .../public/views/kibana/instance/index.js | 10 +- .../public/views/kibana/instances/index.js | 13 +- .../public/views/logstash/node/index.js | 10 +- .../public/views/logstash/nodes/index.js | 13 +- .../server/alerts/alerts_factory.test.ts | 68 +++ .../server/alerts/alerts_factory.ts | 68 +++ .../server/alerts/base_alert.test.ts | 138 ++++++ .../monitoring/server/alerts/base_alert.ts | 339 +++++++++++++ .../alerts/cluster_health_alert.test.ts | 261 ++++++++++ .../server/alerts/cluster_health_alert.ts | 273 +++++++++++ .../server/alerts/cluster_state.test.ts | 175 ------- .../monitoring/server/alerts/cluster_state.ts | 135 ------ .../server/alerts/cpu_usage_alert.test.ts | 376 +++++++++++++++ .../server/alerts/cpu_usage_alert.ts | 451 ++++++++++++++++++ ...asticsearch_version_mismatch_alert.test.ts | 251 ++++++++++ .../elasticsearch_version_mismatch_alert.ts | 263 ++++++++++ .../plugins/monitoring/server/alerts/index.ts | 15 + .../kibana_version_mismatch_alert.test.ts | 253 ++++++++++ .../alerts/kibana_version_mismatch_alert.ts | 253 ++++++++++ .../server/alerts/license_expiration.test.ts | 188 -------- .../server/alerts/license_expiration.ts | 151 ------ .../alerts/license_expiration_alert.test.ts | 281 +++++++++++ .../server/alerts/license_expiration_alert.ts | 262 ++++++++++ .../logstash_version_mismatch_alert.test.ts | 250 ++++++++++ .../alerts/logstash_version_mismatch_alert.ts | 257 ++++++++++ .../server/alerts/nodes_changed_alert.test.ts | 261 ++++++++++ .../server/alerts/nodes_changed_alert.ts | 278 +++++++++++ .../monitoring/server/alerts/types.d.ts | 105 ++-- .../lib/alerts/cluster_state.lib.test.ts | 70 --- .../server/lib/alerts/cluster_state.lib.ts | 88 ---- .../lib/alerts/fetch_cluster_state.test.ts | 39 -- .../server/lib/alerts/fetch_cluster_state.ts | 53 -- .../server/lib/alerts/fetch_clusters.ts | 7 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 228 +++++++++ .../lib/alerts/fetch_cpu_usage_node_stats.ts | 137 ++++++ .../fetch_default_email_address.test.ts | 17 - .../lib/alerts/fetch_default_email_address.ts | 13 - .../lib/alerts/fetch_legacy_alerts.test.ts | 93 ++++ .../server/lib/alerts/fetch_legacy_alerts.ts | 93 ++++ .../server/lib/alerts/fetch_licenses.test.ts | 60 --- .../server/lib/alerts/fetch_licenses.ts | 57 --- .../server/lib/alerts/fetch_status.test.ts | 167 +++++-- .../server/lib/alerts/fetch_status.ts | 92 ++-- .../lib/alerts/get_prepared_alert.test.ts | 163 ------- .../server/lib/alerts/get_prepared_alert.ts | 87 ---- .../lib/alerts/license_expiration.lib.test.ts | 64 --- .../lib/alerts/license_expiration.lib.ts | 88 ---- .../lib/alerts/map_legacy_severity.test.ts | 15 + .../server/lib/alerts/map_legacy_severity.ts | 14 + .../lib/cluster/get_clusters_from_request.js | 96 ++-- .../server/lib/errors/handle_error.js | 2 +- .../monitoring/server/license_service.ts | 2 +- x-pack/plugins/monitoring/server/plugin.ts | 116 ++--- .../server/routes/api/v1/alerts/alerts.js | 140 ------ .../server/routes/api/v1/alerts/enable.ts | 73 +++ .../server/routes/api/v1/alerts/index.js | 4 +- .../routes/api/v1/alerts/legacy_alerts.js | 57 --- .../server/routes/api/v1/alerts/status.ts | 61 +++ .../server/routes/{index.js => index.ts} | 8 +- x-pack/plugins/monitoring/server/types.ts | 93 ++++ .../translations/translations/ja-JP.json | 91 ---- .../translations/translations/zh-CN.json | 91 ---- .../triggers_actions_ui/public/index.ts | 1 + .../cluster/fixtures/multicluster.json | 11 +- .../monitoring/cluster/fixtures/overview.json | 16 - .../standalone_cluster/fixtures/cluster.json | 3 - .../standalone_cluster/fixtures/clusters.json | 6 +- .../apps/monitoring/cluster/alerts.js | 208 -------- .../apps/monitoring/cluster/overview.js | 8 - .../test/functional/apps/monitoring/index.js | 1 - .../monitoring/elasticsearch_nodes.js | 12 +- 149 files changed, 7524 insertions(+), 5861 deletions(-) rename x-pack/plugins/monitoring/{server/alerts => common}/enums.ts (54%) create mode 100644 x-pack/plugins/monitoring/common/types.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/badge.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/callout.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx rename x-pack/plugins/monitoring/public/{components/alerts/index.js => alerts/cpu_usage_alert/index.ts} (79%) create mode 100644 x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx rename x-pack/plugins/monitoring/public/{components/alerts/configuration => alerts/legacy_alert}/index.ts (81%) create mode 100644 x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/panel.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/status.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/alerts.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/map_severity.js delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/status.test.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/alerts/status.tsx delete mode 100644 x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js delete mode 100644 x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js delete mode 100644 x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js delete mode 100644 x-pack/plugins/monitoring/public/views/alerts/index.html delete mode 100644 x-pack/plugins/monitoring/public/views/alerts/index.js create mode 100644 x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/alerts_factory.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/base_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/base_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/cluster_state.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/index.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts create mode 100644 x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts rename x-pack/plugins/monitoring/server/routes/{index.js => index.ts} (67%) delete mode 100644 x-pack/test/functional/apps/monitoring/cluster/alerts.js diff --git a/x-pack/legacy/plugins/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts index ee31a3037a0cb2..f03e1ebc009f5a 100644 --- a/x-pack/legacy/plugins/monitoring/index.ts +++ b/x-pack/legacy/plugins/monitoring/index.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import { config } from './config'; -import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/constants'; /** * Invokes plugin modules to instantiate the Monitoring plugin for Kibana @@ -14,9 +13,6 @@ import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/cons * @return {Object} Monitoring UI Kibana plugin object */ const deps = ['kibana', 'elasticsearch', 'xpack_main']; -if (KIBANA_ALERTING_ENABLED) { - deps.push(...['alerts', 'actions']); -} export const monitoring = (kibana: any) => { return new kibana.Plugin({ require: deps, diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index eeed7b4d5acf61..2c714080969e46 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -139,7 +139,7 @@ export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*'; export const INDEX_PATTERN_KIBANA = '.monitoring-kibana-6-*,.monitoring-kibana-7-*'; export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logstash-7-*'; export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; -export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7'; +export const INDEX_ALERTS = '.monitoring-alerts-6*,.monitoring-alerts-7*'; export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; // This is the unique token that exists in monitoring indices collected by metricbeat @@ -222,41 +222,54 @@ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; * as the only way to see the new UI and actually run Kibana alerts. It will * be false until all alerts have been migrated, then it will be removed */ -export const KIBANA_ALERTING_ENABLED = false; +export const KIBANA_CLUSTER_ALERTS_ENABLED = false; /** * The prefix for all alert types used by monitoring */ -export const ALERT_TYPE_PREFIX = 'monitoring_'; +export const ALERT_PREFIX = 'monitoring_'; +export const ALERT_LICENSE_EXPIRATION = `${ALERT_PREFIX}alert_license_expiration`; +export const ALERT_CLUSTER_HEALTH = `${ALERT_PREFIX}alert_cluster_health`; +export const ALERT_CPU_USAGE = `${ALERT_PREFIX}alert_cpu_usage`; +export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`; +export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`; +export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`; +export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`; /** - * This is the alert type id for the license expiration alert - */ -export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; -/** - * This is the alert type id for the cluster state alert + * A listing of all alert types */ -export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`; +export const ALERTS = [ + ALERT_LICENSE_EXPIRATION, + ALERT_CLUSTER_HEALTH, + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_LOGSTASH_VERSION_MISMATCH, +]; /** - * A listing of all alert types + * A list of all legacy alerts, which means they are powered by watcher */ -export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE]; +export const LEGACY_ALERTS = [ + ALERT_LICENSE_EXPIRATION, + ALERT_CLUSTER_HEALTH, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_LOGSTASH_VERSION_MISMATCH, +]; /** * Matches the id for the built-in in email action type * See x-pack/plugins/actions/server/builtin_action_types/email.ts */ export const ALERT_ACTION_TYPE_EMAIL = '.email'; - -/** - * The number of alerts that have been migrated - */ -export const NUMBER_OF_MIGRATED_ALERTS = 2; - /** - * The advanced settings config name for the email address + * Matches the id for the built-in in log action type + * See x-pack/plugins/actions/server/builtin_action_types/log.ts */ -export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; +export const ALERT_ACTION_TYPE_LOG = '.server-log'; export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/plugins/monitoring/server/alerts/enums.ts b/x-pack/plugins/monitoring/common/enums.ts similarity index 54% rename from x-pack/plugins/monitoring/server/alerts/enums.ts rename to x-pack/plugins/monitoring/common/enums.ts index ccff588743af1b..74711b31756beb 100644 --- a/x-pack/plugins/monitoring/server/alerts/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -4,13 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum AlertClusterStateState { +export enum AlertClusterHealthType { Green = 'green', Red = 'red', Yellow = 'yellow', } -export enum AlertCommonPerClusterMessageTokenType { +export enum AlertSeverity { + Success = 'success', + Danger = 'danger', + Warning = 'warning', +} + +export enum AlertMessageTokenType { Time = 'time', Link = 'link', + DocLink = 'docLink', +} + +export enum AlertParamType { + Duration = 'duration', + Percentage = 'percentage', } diff --git a/x-pack/plugins/monitoring/common/formatting.js b/x-pack/plugins/monitoring/common/formatting.js index a3b3ce07c8c760..b2a67b3cd48da5 100644 --- a/x-pack/plugins/monitoring/common/formatting.js +++ b/x-pack/plugins/monitoring/common/formatting.js @@ -17,10 +17,10 @@ export const LARGE_ABBREVIATED = '0,0.[0]a'; * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date, useUTC = false) { +export function formatDateTimeLocal(date, useUTC = false, timezone = null) { return useUTC ? moment.utc(date).format('LL LTS') - : moment.tz(date, moment.tz.guess()).format('LL LTS'); + : moment.tz(date, timezone || moment.tz.guess()).format('LL LTS'); } /** diff --git a/x-pack/plugins/monitoring/common/types.ts b/x-pack/plugins/monitoring/common/types.ts new file mode 100644 index 00000000000000..f5dc85dce32e18 --- /dev/null +++ b/x-pack/plugins/monitoring/common/types.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { Alert } from '../../alerts/common'; +import { AlertParamType } from './enums'; + +export interface CommonBaseAlert { + type: string; + label: string; + paramDetails: CommonAlertParamDetails; + rawAlert: Alert; + isLegacy: boolean; +} + +export interface CommonAlertStatus { + exists: boolean; + enabled: boolean; + states: CommonAlertState[]; + alert: CommonBaseAlert; +} + +export interface CommonAlertState { + firing: boolean; + state: any; + meta: any; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CommonAlertFilter {} + +export interface CommonAlertCpuUsageFilter extends CommonAlertFilter { + nodeUuid: string; +} + +export interface CommonAlertParamDetail { + label: string; + type: AlertParamType; +} + +export interface CommonAlertParamDetails { + [name: string]: CommonAlertParamDetail; +} + +export interface CommonAlertParams { + [name: string]: string | number; +} diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 65dd4b373a71aa..3b9e60124b0343 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -3,8 +3,17 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["monitoring"], - "requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"], - "optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"], + "requiredPlugins": [ + "licensing", + "features", + "data", + "navigation", + "kibanaLegacy", + "triggers_actions_ui", + "alerts", + "actions" + ], + "optionalPlugins": ["infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"], "server": true, "ui": true, "requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement"] diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx new file mode 100644 index 00000000000000..4518d2c56cabb9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiContextMenu, + EuiPopover, + EuiBadge, + EuiFlexGrid, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { CommonAlertStatus, CommonAlertState } from '../../common/types'; +import { AlertSeverity } from '../../common/enums'; +// @ts-ignore +import { formatDateTimeLocal } from '../../common/formatting'; +import { AlertState } from '../../server/alerts/types'; +import { AlertPanel } from './panel'; +import { Legacy } from '../legacy_shims'; +import { isInSetupMode } from '../lib/setup_mode'; + +function getDateFromState(states: CommonAlertState[]) { + const timestamp = states[0].state.ui.triggeredMS; + const tz = Legacy.shims.uiSettings.get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, false, tz === 'Browser' ? null : tz); +} + +export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`; + +interface Props { + alerts: { [alertTypeId: string]: CommonAlertStatus }; +} +export const AlertsBadge: React.FC = (props: Props) => { + const [showPopover, setShowPopover] = React.useState(null); + const inSetupMode = isInSetupMode(); + const alerts = Object.values(props.alerts).filter(Boolean); + + if (alerts.length === 0) { + return null; + } + + const badges = []; + + if (inSetupMode) { + const button = ( + setShowPopover(true)} + > + {numberOfAlertsLabel(alerts.length)} + + ); + const panels = [ + { + id: 0, + title: i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { + defaultMessage: 'Alerts', + }), + items: alerts.map(({ alert }, index) => { + return { + name: {alert.label}, + panel: index + 1, + }; + }), + }, + ...alerts.map((alertStatus, index) => { + return { + id: index + 1, + title: alertStatus.alert.label, + width: 400, + content: , + }; + }), + ]; + + badges.push( + setShowPopover(null)} + panelPaddingSize="none" + withTitle + anchorPosition="downLeft" + > + + + ); + } else { + const byType = { + [AlertSeverity.Danger]: [] as CommonAlertStatus[], + [AlertSeverity.Warning]: [] as CommonAlertStatus[], + [AlertSeverity.Success]: [] as CommonAlertStatus[], + }; + + for (const alert of alerts) { + for (const alertState of alert.states) { + const state = alertState.state as AlertState; + byType[state.ui.severity].push(alert); + } + } + + const typesToShow = [AlertSeverity.Danger, AlertSeverity.Warning]; + for (const type of typesToShow) { + const list = byType[type]; + if (list.length === 0) { + continue; + } + + const button = ( + setShowPopover(type)} + > + {numberOfAlertsLabel(list.length)} + + ); + + const panels = [ + { + id: 0, + title: `Alerts`, + items: list.map(({ alert, states }, index) => { + return { + name: ( + + +

{getDateFromState(states)}

+
+ {alert.label} +
+ ), + panel: index + 1, + }; + }), + }, + ...list.map((alertStatus, index) => { + return { + id: index + 1, + title: getDateFromState(alertStatus.states), + width: 400, + content: , + }; + }), + ]; + + badges.push( + setShowPopover(null)} + panelPaddingSize="none" + withTitle + anchorPosition="downLeft" + > + + + ); + } + } + + return ( + + {badges.map((badge, index) => ( + + {badge} + + ))} + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx new file mode 100644 index 00000000000000..748ec257ea7650 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { CommonAlertStatus } from '../../common/types'; +import { AlertSeverity } from '../../common/enums'; +import { replaceTokens } from './lib/replace_tokens'; +import { AlertMessage } from '../../server/alerts/types'; + +const TYPES = [ + { + severity: AlertSeverity.Warning, + color: 'warning', + label: i18n.translate('xpack.monitoring.alerts.callout.warningLabel', { + defaultMessage: 'Warning alert(s)', + }), + }, + { + severity: AlertSeverity.Danger, + color: 'danger', + label: i18n.translate('xpack.monitoring.alerts.callout.dangerLabel', { + defaultMessage: 'DAnger alert(s)', + }), + }, +]; + +interface Props { + alerts: { [alertTypeId: string]: CommonAlertStatus }; +} +export const AlertsCallout: React.FC = (props: Props) => { + const { alerts } = props; + + const callouts = TYPES.map((type) => { + const list = []; + for (const alertTypeId of Object.keys(alerts)) { + const alertInstance = alerts[alertTypeId]; + for (const { state } of alertInstance.states) { + if (state.ui.severity === type.severity) { + list.push(state); + } + } + } + + if (list.length) { + return ( + + +
    + {list.map((state, index) => { + const nextStepsUi = + state.ui.message.nextSteps && state.ui.message.nextSteps.length ? ( +
      + {state.ui.message.nextSteps.map( + (step: AlertMessage, nextStepIndex: number) => ( +
    • {replaceTokens(step)}
    • + ) + )} +
    + ) : null; + + return ( +
  • + {replaceTokens(state.ui.message)} + {nextStepsUi} +
  • + ); + })} +
+
+ +
+ ); + } + }); + return {callouts}; +}; diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx new file mode 100644 index 00000000000000..56cba83813a630 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { validate } from './validation'; +import { ALERT_CPU_USAGE } from '../../../common/constants'; +import { Expression } from './expression'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CpuUsageAlert } from '../../../server/alerts'; + +export function createCpuUsageAlertType(): AlertTypeModel { + const alert = new CpuUsageAlert(); + return { + id: ALERT_CPU_USAGE, + name: alert.label, + iconClass: 'bell', + alertParamsExpression: (props: any) => ( + + ), + validate, + defaultActionMessage: '{{context.internalFullMessage}}', + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx new file mode 100644 index 00000000000000..7dc6155de529ee --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/expression.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { EuiForm, EuiSpacer } from '@elastic/eui'; +import { CommonAlertParamDetails } from '../../../common/types'; +import { AlertParamDuration } from '../flyout_expressions/alert_param_duration'; +import { AlertParamType } from '../../../common/enums'; +import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage'; + +export interface Props { + alertParams: { [property: string]: any }; + setAlertParams: (property: string, value: any) => void; + setAlertProperty: (property: string, value: any) => void; + errors: { [key: string]: string[] }; + paramDetails: CommonAlertParamDetails; +} + +export const Expression: React.FC = (props) => { + const { alertParams, paramDetails, setAlertParams, errors } = props; + + const alertParamsUi = Object.keys(alertParams).map((alertParamName) => { + const details = paramDetails[alertParamName]; + const value = alertParams[alertParamName]; + + switch (details.type) { + case AlertParamType.Duration: + return ( + + ); + case AlertParamType.Percentage: + return ( + + ); + } + }); + + return ( + + {alertParamsUi} + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/index.js b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/index.ts similarity index 79% rename from x-pack/plugins/monitoring/public/components/alerts/index.js rename to x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/index.ts index c4eda37c2b2520..6ef31ee472c610 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/index.js +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Alerts } from './alerts'; +export { createCpuUsageAlertType } from './cpu_usage_alert'; diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx new file mode 100644 index 00000000000000..577ec12e634edc --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/validation.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../triggers_actions_ui/public/types'; + +export function validate(opts: any): ValidationResult { + const validationResult = { errors: {} }; + + const errors: { [key: string]: string[] } = { + duration: [], + threshold: [], + }; + if (!opts.duration) { + errors.duration.push( + i18n.translate('xpack.monitoring.alerts.cpuUsage.validation.duration', { + defaultMessage: 'A valid duration is required.', + }) + ); + } + if (isNaN(opts.threshold)) { + errors.threshold.push( + i18n.translate('xpack.monitoring.alerts.cpuUsage.validation.threshold', { + defaultMessage: 'A valid number is required.', + }) + ); + } + + validationResult.errors = errors; + return validationResult; +} diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx new file mode 100644 index 00000000000000..23a9ea1facbc9c --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexItem, EuiFlexGroup, EuiFieldNumber, EuiSelect, EuiFormRow } from '@elastic/eui'; + +enum TIME_UNITS { + SECOND = 's', + MINUTE = 'm', + HOUR = 'h', + DAY = 'd', +} +function getTimeUnitLabel(timeUnit = TIME_UNITS.SECOND, timeValue = '0') { + switch (timeUnit) { + case TIME_UNITS.SECOND: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.secondLabel', { + defaultMessage: '{timeValue, plural, one {second} other {seconds}}', + values: { timeValue }, + }); + case TIME_UNITS.MINUTE: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.minuteLabel', { + defaultMessage: '{timeValue, plural, one {minute} other {minutes}}', + values: { timeValue }, + }); + case TIME_UNITS.HOUR: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.hourLabel', { + defaultMessage: '{timeValue, plural, one {hour} other {hours}}', + values: { timeValue }, + }); + case TIME_UNITS.DAY: + return i18n.translate('xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel', { + defaultMessage: '{timeValue, plural, one {day} other {days}}', + values: { timeValue }, + }); + } +} + +// TODO: WHY does this not work? +// import { getTimeUnitLabel, TIME_UNITS } from '../../../triggers_actions_ui/public'; + +interface Props { + name: string; + duration: string; + label: string; + errors: string[]; + setAlertParams: (property: string, value: any) => void; +} + +const parseRegex = /(\d+)(\smhd)/; +export const AlertParamDuration: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors } = props; + const parsed = parseRegex.exec(props.duration); + const defaultValue = parsed && parsed[1] ? parseInt(parsed[1], 10) : 1; + const defaultUnit = parsed && parsed[2] ? parsed[2] : TIME_UNITS.MINUTE; + const [value, setValue] = React.useState(defaultValue); + const [unit, setUnit] = React.useState(defaultUnit); + + const timeUnits = Object.values(TIME_UNITS).map((timeUnit) => ({ + value: timeUnit, + text: getTimeUnitLabel(timeUnit), + })); + + React.useEffect(() => { + setAlertParams(name, `${value}${unit}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [unit, value]); + + return ( + 0}> + + + { + let newValue = parseInt(e.target.value, 10); + if (isNaN(newValue)) { + newValue = 0; + } + setValue(newValue); + }} + /> + + + setUnit(e.target.value)} + options={timeUnits} + /> + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx new file mode 100644 index 00000000000000..352fb725574988 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_percentage.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiFormRow, EuiFieldNumber, EuiText } from '@elastic/eui'; + +interface Props { + name: string; + percentage: number; + label: string; + errors: string[]; + setAlertParams: (property: string, value: any) => void; +} +export const AlertParamPercentage: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors } = props; + const [value, setValue] = React.useState(props.percentage); + + return ( + 0}> + + % + + } + onChange={(e) => { + let newValue = parseInt(e.target.value, 10); + if (isNaN(newValue)) { + newValue = 0; + } + setValue(newValue); + setAlertParams(name, newValue); + }} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/plugins/monitoring/public/alerts/legacy_alert/index.ts similarity index 81% rename from x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts rename to x-pack/plugins/monitoring/public/alerts/legacy_alert/index.ts index 7a96c6e324ab36..6370ed66f0c300 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AlertsConfiguration } from './configuration'; +export { createLegacyAlertTypes } from './legacy_alert'; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx new file mode 100644 index 00000000000000..58b37e43085ffc --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTextColor, EuiSpacer } from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { LEGACY_ALERTS } from '../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BY_TYPE } from '../../../server/alerts'; + +export function createLegacyAlertTypes(): AlertTypeModel[] { + return LEGACY_ALERTS.map((legacyAlert) => { + const alertCls = BY_TYPE[legacyAlert]; + const alert = new alertCls(); + return { + id: legacyAlert, + name: alert.label, + iconClass: 'bell', + alertParamsExpression: (props: any) => ( + + + + {i18n.translate('xpack.monitoring.alerts.legacyAlert.expressionText', { + defaultMessage: 'There is nothing to configure.', + })} + + + + ), + defaultActionMessage: '{{context.internalFullMessage}}', + validate: () => ({ errors: {} }), + requiresAppContext: false, + }; + }); +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx new file mode 100644 index 00000000000000..29e0822ad684d6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import moment from 'moment'; +import { EuiLink } from '@elastic/eui'; +import { + AlertMessage, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertMessageDocLinkToken, +} from '../../../server/alerts/types'; +// @ts-ignore +import { formatTimestampToDuration } from '../../../common'; +import { CALCULATE_DURATION_UNTIL } from '../../../common/constants'; +import { AlertMessageTokenType } from '../../../common/enums'; +import { Legacy } from '../../legacy_shims'; + +export function replaceTokens(alertMessage: AlertMessage): JSX.Element | string | null { + if (!alertMessage) { + return null; + } + + let text = alertMessage.text; + if (!alertMessage.tokens || !alertMessage.tokens.length) { + return text; + } + + const timeTokens = alertMessage.tokens.filter( + (token) => token.type === AlertMessageTokenType.Time + ); + const linkTokens = alertMessage.tokens.filter( + (token) => token.type === AlertMessageTokenType.Link + ); + const docLinkTokens = alertMessage.tokens.filter( + (token) => token.type === AlertMessageTokenType.DocLink + ); + + for (const token of timeTokens) { + const timeToken = token as AlertMessageTimeToken; + text = text.replace( + timeToken.startToken, + timeToken.isRelative + ? formatTimestampToDuration(timeToken.timestamp, CALCULATE_DURATION_UNTIL) + : moment.tz(timeToken.timestamp, moment.tz.guess()).format('LLL z') + ); + } + + let element: JSX.Element = {text}; + for (const token of linkTokens) { + const linkToken = token as AlertMessageLinkToken; + const linkPart = new RegExp(`${linkToken.startToken}(.+?)${linkToken.endToken}`).exec(text); + if (!linkPart || linkPart.length < 2) { + continue; + } + const index = text.indexOf(linkPart[0]); + const preString = text.substring(0, index); + const postString = text.substring(index + linkPart[0].length); + element = ( + + {preString} + {linkPart[1]} + {postString} + + ); + } + + for (const token of docLinkTokens) { + const linkToken = token as AlertMessageDocLinkToken; + const linkPart = new RegExp(`${linkToken.startToken}(.+?)${linkToken.endToken}`).exec(text); + if (!linkPart || linkPart.length < 2) { + continue; + } + + const url = linkToken.partialUrl + .replace('{elasticWebsiteUrl}', Legacy.shims.docLinks.ELASTIC_WEBSITE_URL) + .replace('{docLinkVersion}', Legacy.shims.docLinks.DOC_LINK_VERSION); + const index = text.indexOf(linkPart[0]); + const preString = text.substring(0, index); + const postString = text.substring(index + linkPart[0].length); + element = ( + + {preString} + {linkPart[1]} + {postString} + + ); + } + + return element; +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts new file mode 100644 index 00000000000000..c6773e9ca0156a --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isInSetupMode } from '../../lib/setup_mode'; +import { CommonAlertStatus } from '../../../common/types'; + +export function shouldShowAlertBadge( + alerts: { [alertTypeId: string]: CommonAlertStatus }, + alertTypeIds: string[] +) { + const inSetupMode = isInSetupMode(); + return inSetupMode || alertTypeIds.find((name) => alerts[name] && alerts[name].states.length); +} diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx new file mode 100644 index 00000000000000..3c5a4ef55a96bb --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiTitle, + EuiHorizontalRule, + EuiListGroup, + EuiListGroupItem, +} from '@elastic/eui'; + +import { CommonAlertStatus } from '../../common/types'; +import { AlertMessage } from '../../server/alerts/types'; +import { Legacy } from '../legacy_shims'; +import { replaceTokens } from './lib/replace_tokens'; +import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertEdit } from '../../../triggers_actions_ui/public'; +import { isInSetupMode, hideBottomBar, showBottomBar } from '../lib/setup_mode'; +import { BASE_ALERT_API_PATH } from '../../../alerts/common'; + +interface Props { + alert: CommonAlertStatus; +} +export const AlertPanel: React.FC = (props: Props) => { + const { + alert: { states, alert }, + } = props; + const [showFlyout, setShowFlyout] = React.useState(false); + const [isEnabled, setIsEnabled] = React.useState(alert.rawAlert.enabled); + const [isMuted, setIsMuted] = React.useState(alert.rawAlert.muteAll); + const [isSaving, setIsSaving] = React.useState(false); + const inSetupMode = isInSetupMode(); + + if (!alert.rawAlert) { + return null; + } + + async function disableAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_disable`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', { + defaultMessage: `Unable to disable alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function enableAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_enable`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', { + defaultMessage: `Unable to enable alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function muteAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_mute_all`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', { + defaultMessage: `Unable to mute alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function unmuteAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.rawAlert.id}/_unmute_all`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', { + defaultMessage: `Unable to unmute alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + + const flyoutUi = showFlyout ? ( + {}, + capabilities: Legacy.shims.capabilities, + }} + > + { + setShowFlyout(false); + showBottomBar(); + }} + /> + + ) : null; + + const configurationUi = ( + + + + { + setShowFlyout(true); + hideBottomBar(); + }} + > + {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { + defaultMessage: `Edit alert`, + })} + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(); + } else { + setIsEnabled(true); + await enableAlert(); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(); + } else { + setIsMuted(true); + await muteAlert(); + } + }} + label={ + + } + /> + + + {flyoutUi} + + ); + + if (inSetupMode) { + return
{configurationUi}
; + } + + const firingStates = states.filter((state) => state.firing); + if (!firingStates.length) { + return
{configurationUi}
; + } + + const firingState = firingStates[0]; + const nextStepsUi = + firingState.state.ui.message.nextSteps && firingState.state.ui.message.nextSteps.length ? ( + + {firingState.state.ui.message.nextSteps.map((step: AlertMessage, index: number) => ( + + ))} + + ) : null; + + return ( + +
+ +
{replaceTokens(firingState.state.ui.message)}
+
+ {nextStepsUi ? : null} + {nextStepsUi} +
+ +
{configurationUi}
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx new file mode 100644 index 00000000000000..d15dcc99748636 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiToolTip, EuiHealth } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { CommonAlertStatus } from '../../common/types'; +import { AlertSeverity } from '../../common/enums'; +import { AlertState } from '../../server/alerts/types'; +import { AlertsBadge } from './badge'; + +interface Props { + alerts: { [alertTypeId: string]: CommonAlertStatus }; + showBadge: boolean; + showOnlyCount: boolean; +} +export const AlertsStatus: React.FC = (props: Props) => { + const { alerts, showBadge = false, showOnlyCount = false } = props; + + let atLeastOneDanger = false; + const count = Object.values(alerts).reduce((cnt, alertStatus) => { + if (alertStatus.states.length) { + if (!atLeastOneDanger) { + for (const state of alertStatus.states) { + if ((state.state as AlertState).ui.severity === AlertSeverity.Danger) { + atLeastOneDanger = true; + break; + } + } + } + cnt++; + } + return cnt; + }, 0); + + if (count === 0) { + return ( + + + {showOnlyCount ? ( + count + ) : ( + + )} + + + ); + } + + if (showBadge) { + return ; + } + + const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning; + + const tooltipText = (() => { + switch (severity) { + case AlertSeverity.Danger: + return i18n.translate('xpack.monitoring.alerts.status.highSeverityTooltip', { + defaultMessage: 'There are some critical issues that require your immediate attention!', + }); + case AlertSeverity.Warning: + return i18n.translate('xpack.monitoring.alerts.status.mediumSeverityTooltip', { + defaultMessage: 'There are some issues that might have impact on the stack.', + }); + default: + // might never show + return i18n.translate('xpack.monitoring.alerts.status.lowSeverityTooltip', { + defaultMessage: 'There are some low-severity issues.', + }); + } + })(); + + return ( + + + {showOnlyCount ? ( + count + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 9ebb074ec7c3b6..f3d77b196b26ec 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -18,7 +18,7 @@ import { createTopNavDirective, createTopNavHelper, } from '../../../../../src/plugins/kibana_legacy/public'; -import { MonitoringPluginDependencies } from '../types'; +import { MonitoringStartPluginDependencies } from '../types'; import { GlobalState } from '../url_state'; import { getSafeForExternalLink } from '../lib/get_safe_for_external_link'; @@ -60,7 +60,7 @@ export const localAppModule = ({ data: { query }, navigation, externalConfig, -}: MonitoringPluginDependencies) => { +}: MonitoringStartPluginDependencies) => { createLocalI18nModule(); createLocalPrivateModule(); createLocalStorage(); @@ -90,7 +90,9 @@ export const localAppModule = ({ return appModule; }; -function createMonitoringAppConfigConstants(keys: MonitoringPluginDependencies['externalConfig']) { +function createMonitoringAppConfigConstants( + keys: MonitoringStartPluginDependencies['externalConfig'] +) { let constantsModule = angular.module('monitoring/constants', []); keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value))); } @@ -173,7 +175,7 @@ function createMonitoringAppFilters() { }); } -function createLocalConfigModule(core: MonitoringPluginDependencies['core']) { +function createLocalConfigModule(core: MonitoringStartPluginDependencies['core']) { angular.module('monitoring/Config', []).provider('config', function () { return { $get: () => ({ @@ -201,7 +203,7 @@ function createLocalPrivateModule() { angular.module('monitoring/Private', []).provider('Private', PrivateProvider); } -function createLocalTopNavModule({ ui }: MonitoringPluginDependencies['navigation']) { +function createLocalTopNavModule({ ui }: MonitoringStartPluginDependencies['navigation']) { angular .module('monitoring/TopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) diff --git a/x-pack/plugins/monitoring/public/angular/index.ts b/x-pack/plugins/monitoring/public/angular/index.ts index 69d97a5e3bdc35..da57c028643a54 100644 --- a/x-pack/plugins/monitoring/public/angular/index.ts +++ b/x-pack/plugins/monitoring/public/angular/index.ts @@ -10,13 +10,13 @@ import { Legacy } from '../legacy_shims'; import { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; import { localAppModule, appModuleName } from './app_modules'; -import { MonitoringPluginDependencies } from '../types'; +import { MonitoringStartPluginDependencies } from '../types'; const APP_WRAPPER_CLASS = 'monApplicationWrapper'; export class AngularApp { private injector?: angular.auto.IInjectorService; - constructor(deps: MonitoringPluginDependencies) { + constructor(deps: MonitoringStartPluginDependencies) { const { core, element, @@ -25,6 +25,7 @@ export class AngularApp { isCloud, pluginInitializerContext, externalConfig, + triggersActionsUi, kibanaLegacy, } = deps; const app: IModule = localAppModule(deps); @@ -40,6 +41,7 @@ export class AngularApp { pluginInitializerContext, externalConfig, kibanaLegacy, + triggersActionsUi, }, this.injector ); diff --git a/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap deleted file mode 100644 index 5562d4bae9b145..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap +++ /dev/null @@ -1,65 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Status should render a flyout when clicking the link 1`] = ` - - - -

- Monitoring alerts -

-
- -

- Configure an email server and email address to receive alerts. -

-
-
- - - -
-`; - -exports[`Status should render a success message if all alerts have been migrated and in setup mode 1`] = ` - -

- - Want to make changes? Click here. - -

-
-`; - -exports[`Status should render without setup mode 1`] = ` - - -

- - Migrate cluster alerts to our new alerting platform. - -

-
- -
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js deleted file mode 100644 index 8f454e7d765c45..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { mapSeverity } from '../map_severity'; - -describe('mapSeverity', () => { - it('maps [0, 1000) as low', () => { - const low = { - value: 'low', - color: 'warning', - iconType: 'iInCircle', - title: 'Low severity alert', - }; - - expect(mapSeverity(-1)).to.not.eql(low); - expect(mapSeverity(0)).to.eql(low); - expect(mapSeverity(1)).to.eql(low); - expect(mapSeverity(500)).to.eql(low); - expect(mapSeverity(998)).to.eql(low); - expect(mapSeverity(999)).to.eql(low); - expect(mapSeverity(1000)).to.not.eql(low); - }); - - it('maps [1000, 2000) as medium', () => { - const medium = { - value: 'medium', - color: 'warning', - iconType: 'alert', - title: 'Medium severity alert', - }; - - expect(mapSeverity(999)).to.not.eql(medium); - expect(mapSeverity(1000)).to.eql(medium); - expect(mapSeverity(1001)).to.eql(medium); - expect(mapSeverity(1500)).to.eql(medium); - expect(mapSeverity(1998)).to.eql(medium); - expect(mapSeverity(1999)).to.eql(medium); - expect(mapSeverity(2000)).to.not.eql(medium); - }); - - it('maps (-INF, 0) and [2000, +INF) as high', () => { - const high = { - value: 'high', - color: 'danger', - iconType: 'bell', - title: 'High severity alert', - }; - - expect(mapSeverity(-123412456)).to.eql(high); - expect(mapSeverity(-1)).to.eql(high); - expect(mapSeverity(0)).to.not.eql(high); - expect(mapSeverity(1999)).to.not.eql(high); - expect(mapSeverity(2000)).to.eql(high); - expect(mapSeverity(2001)).to.eql(high); - expect(mapSeverity(2500)).to.eql(high); - expect(mapSeverity(2998)).to.eql(high); - expect(mapSeverity(2999)).to.eql(high); - expect(mapSeverity(3000)).to.eql(high); - expect(mapSeverity(123412456)).to.eql(high); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js deleted file mode 100644 index 59e838c449a3b8..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/alerts.js +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { Legacy } from '../../legacy_shims'; -import { upperFirst, get } from 'lodash'; -import { formatDateTimeLocal } from '../../../common/formatting'; -import { formatTimestampToDuration } from '../../../common'; -import { - CALCULATE_DURATION_SINCE, - EUI_SORT_DESCENDING, - ALERT_TYPE_LICENSE_EXPIRATION, - ALERT_TYPE_CLUSTER_STATE, -} from '../../../common/constants'; -import { mapSeverity } from './map_severity'; -import { FormattedAlert } from '../../components/alerts/formatted_alert'; -import { EuiMonitoringTable } from '../../components/table'; -import { EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const linkToCategories = { - 'elasticsearch/nodes': 'Elasticsearch Nodes', - 'elasticsearch/indices': 'Elasticsearch Indices', - 'kibana/instances': 'Kibana Instances', - 'logstash/instances': 'Logstash Nodes', - [ALERT_TYPE_LICENSE_EXPIRATION]: 'License expiration', - [ALERT_TYPE_CLUSTER_STATE]: 'Cluster state', -}; -const getColumns = (timezone) => [ - { - name: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', { - defaultMessage: 'Status', - }), - field: 'status', - sortable: true, - render: (severity) => { - const severityIconDefaults = { - title: i18n.translate('xpack.monitoring.alerts.severityTitle.unknown', { - defaultMessage: 'Unknown', - }), - color: 'subdued', - value: i18n.translate('xpack.monitoring.alerts.severityValue.unknown', { - defaultMessage: 'N/A', - }), - }; - const severityIcon = { ...severityIconDefaults, ...mapSeverity(severity) }; - - return ( - - - {upperFirst(severityIcon.value)} - - - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.alerts.resolvedColumnTitle', { - defaultMessage: 'Resolved', - }), - field: 'resolved_timestamp', - sortable: true, - render: (resolvedTimestamp) => { - const notResolvedLabel = i18n.translate('xpack.monitoring.alerts.notResolvedDescription', { - defaultMessage: 'Not Resolved', - }); - - const resolution = { - icon: null, - text: notResolvedLabel, - }; - - if (resolvedTimestamp) { - resolution.text = i18n.translate('xpack.monitoring.alerts.resolvedAgoDescription', { - defaultMessage: '{duration} ago', - values: { - duration: formatTimestampToDuration(resolvedTimestamp, CALCULATE_DURATION_SINCE), - }, - }); - } else { - resolution.icon = ; - } - - return ( - - {resolution.icon} {resolution.text} - - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.alerts.messageColumnTitle', { - defaultMessage: 'Message', - }), - field: 'message', - sortable: true, - render: (_message, alert) => { - const message = get(alert, 'message.text', get(alert, 'message', '')); - return ( - - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.alerts.categoryColumnTitle', { - defaultMessage: 'Category', - }), - field: 'category', - sortable: true, - render: (link) => - linkToCategories[link] - ? linkToCategories[link] - : i18n.translate('xpack.monitoring.alerts.categoryColumn.generalLabel', { - defaultMessage: 'General', - }), - }, - { - name: i18n.translate('xpack.monitoring.alerts.lastCheckedColumnTitle', { - defaultMessage: 'Last Checked', - }), - field: 'update_timestamp', - sortable: true, - render: (timestamp) => formatDateTimeLocal(timestamp, timezone), - }, - { - name: i18n.translate('xpack.monitoring.alerts.triggeredColumnTitle', { - defaultMessage: 'Triggered', - }), - field: 'timestamp', - sortable: true, - render: (timestamp) => - i18n.translate('xpack.monitoring.alerts.triggeredColumnValue', { - defaultMessage: '{timestamp} ago', - values: { - timestamp: formatTimestampToDuration(timestamp, CALCULATE_DURATION_SINCE), - }, - }), - }, -]; - -export const Alerts = ({ alerts, sorting, pagination, onTableChange }) => { - const alertsFlattened = alerts.map((alert) => ({ - ...alert, - status: get(alert, 'metadata.severity', get(alert, 'severity', 0)), - category: get(alert, 'metadata.link', get(alert, 'type', null)), - })); - - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - - return ( - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap deleted file mode 100644 index 429d19fbb887ec..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap +++ /dev/null @@ -1,121 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Configuration shallow view should render step 1 1`] = ` - - - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="" - /> - -`; - -exports[`Configuration shallow view should render step 2 1`] = ` - - - - - -`; - -exports[`Configuration shallow view should render step 3 1`] = ` - - - Save - - -`; - -exports[`Configuration should render high level steps 1`] = ` -
- - - - - - - - - -
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap deleted file mode 100644 index cb1081c0c14da6..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap +++ /dev/null @@ -1,301 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Step1 creating should render a create form 1`] = ` - - - - - -`; - -exports[`Step1 editing should allow for editing 1`] = ` - - -

- Edit the action below. -

-
- - -
-`; - -exports[`Step1 should render normally 1`] = ` - - - From: , Service: - , - "inputDisplay": - From: , Service: - , - "value": "1", - }, - Object { - "dropdownDisplay": - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="1" - /> - - - - - Edit - - - - - Test - - - - - Delete - - - - -`; - -exports[`Step1 testing should should a tooltip if there is no email address 1`] = ` - - - Test - - -`; - -exports[`Step1 testing should show a failed test error 1`] = ` - - - From: , Service: - , - "inputDisplay": - From: , Service: - , - "value": "1", - }, - Object { - "dropdownDisplay": - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="1" - /> - - - - - Edit - - - - - Test - - - - - Delete - - - - - -

- Very detailed error message -

-
-
-`; - -exports[`Step1 testing should show a successful test 1`] = ` - - - From: , Service: - , - "inputDisplay": - From: , Service: - , - "value": "1", - }, - Object { - "dropdownDisplay": - Create new email action... - , - "inputDisplay": - Create new email action... - , - "value": "__new__", - }, - ] - } - valueOfSelected="1" - /> - - - - - Edit - - - - - Test - - - - - Delete - - - - - -

- Looks good on our end! -

-
-
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap deleted file mode 100644 index bac183618b4912..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Step2 should render normally 1`] = ` - - - - - -`; - -exports[`Step2 should show form errors 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap deleted file mode 100644 index ed15ae9a9cff7f..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Step3 should render normally 1`] = ` - - - Save - - -`; - -exports[`Step3 should show a disabled state 1`] = ` - - - Save - - -`; - -exports[`Step3 should show a saving state 1`] = ` - - - Save - - -`; - -exports[`Step3 should show an error 1`] = ` - - -

- Test error -

-
- - - Save - -
-`; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx deleted file mode 100644 index 7caef8c230bf48..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mockUseEffects } from '../../../jest.helpers'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { Legacy } from '../../../legacy_shims'; -import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; - -jest.mock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: jest.fn(), - }, - }, -})); - -const defaultProps: AlertsConfigurationProps = { - emailAddress: 'test@elastic.co', - onDone: jest.fn(), -}; - -describe('Configuration', () => { - it('should render high level steps', () => { - const component = shallow(); - expect(component.find('EuiSteps').shallow()).toMatchSnapshot(); - }); - - function getStep(component: ShallowWrapper, index: number) { - return component.find('EuiSteps').shallow().find('EuiStep').at(index).children().shallow(); - } - - describe('shallow view', () => { - it('should render step 1', () => { - const component = shallow(); - const stepOne = getStep(component, 0); - expect(stepOne).toMatchSnapshot(); - }); - - it('should render step 2', () => { - const component = shallow(); - const stepTwo = getStep(component, 1); - expect(stepTwo).toMatchSnapshot(); - }); - - it('should render step 3', () => { - const component = shallow(); - const stepThree = getStep(component, 2); - expect(stepThree).toMatchSnapshot(); - }); - }); - - describe('selected action', () => { - const actionId = 'a123b'; - let component: ShallowWrapper; - beforeEach(async () => { - mockUseEffects(2); - - (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { - return { - data: [ - { - actionTypeId: '.email', - id: actionId, - config: {}, - }, - ], - }; - }); - - component = shallow(); - }); - - it('reflect in Step1', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('EuiStep').at(0).prop('title')).toBe('Select email action'); - expect(steps.find('Step1').prop('selectedEmailActionId')).toBe(actionId); - }); - - it('should enable Step2', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step2').prop('isDisabled')).toBe(false); - }); - - it('should enable Step3', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step3').prop('isDisabled')).toBe(false); - }); - }); - - describe('edit action', () => { - let component: ShallowWrapper; - beforeEach(async () => { - (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { - return { - data: [], - }; - }); - - component = shallow(); - }); - - it('disable Step2', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step2').prop('isDisabled')).toBe(true); - }); - - it('disable Step3', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step3').prop('isDisabled')).toBe(true); - }); - }); - - describe('no email address', () => { - let component: ShallowWrapper; - beforeEach(async () => { - (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { - return { - data: [ - { - actionTypeId: '.email', - id: 'actionId', - config: {}, - }, - ], - }; - }); - - component = shallow(); - }); - - it('should disable Step3', async () => { - const steps = component.find('EuiSteps').dive(); - expect(steps.find('Step3').prop('isDisabled')).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx deleted file mode 100644 index f248e20493a243..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { ReactNode } from 'react'; -import { EuiSteps } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from '../../../legacy_shims'; -import { ActionResult } from '../../../../../../plugins/actions/common'; -import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; -import { getMissingFieldErrors } from '../../../lib/form_validation'; -import { Step1 } from './step1'; -import { Step2 } from './step2'; -import { Step3 } from './step3'; - -export interface AlertsConfigurationProps { - emailAddress: string; - onDone: Function; -} - -export interface StepResult { - title: string; - children: ReactNode; - status: any; -} - -export interface AlertsConfigurationForm { - email: string | null; -} - -export const NEW_ACTION_ID = '__new__'; - -export const AlertsConfiguration: React.FC = ( - props: AlertsConfigurationProps -) => { - const { onDone } = props; - - const [emailActions, setEmailActions] = React.useState([]); - const [selectedEmailActionId, setSelectedEmailActionId] = React.useState(''); - const [editAction, setEditAction] = React.useState(null); - const [emailAddress, setEmailAddress] = React.useState(props.emailAddress); - const [formErrors, setFormErrors] = React.useState({ email: null }); - const [showFormErrors, setShowFormErrors] = React.useState(false); - const [isSaving, setIsSaving] = React.useState(false); - const [saveError, setSaveError] = React.useState(''); - - React.useEffect(() => { - async function fetchData() { - await fetchEmailActions(); - } - - fetchData(); - }, []); - - React.useEffect(() => { - setFormErrors(getMissingFieldErrors({ email: emailAddress }, { email: '' })); - }, [emailAddress]); - - async function fetchEmailActions() { - const kibanaActions = await Legacy.shims.kfetch({ - method: 'GET', - pathname: `/api/actions`, - }); - - const actions = kibanaActions.data.filter( - (action: ActionResult) => action.actionTypeId === ALERT_ACTION_TYPE_EMAIL - ); - if (actions.length > 0) { - setSelectedEmailActionId(actions[0].id); - } else { - setSelectedEmailActionId(NEW_ACTION_ID); - } - setEmailActions(actions); - } - - async function save() { - if (emailAddress.length === 0) { - setShowFormErrors(true); - return; - } - setIsSaving(true); - setShowFormErrors(false); - - try { - await Legacy.shims.kfetch({ - method: 'POST', - pathname: `/api/monitoring/v1/alerts`, - body: JSON.stringify({ selectedEmailActionId, emailAddress }), - }); - } catch (err) { - setIsSaving(false); - setSaveError( - err?.body?.message || - i18n.translate('xpack.monitoring.alerts.configuration.unknownError', { - defaultMessage: 'Something went wrong. Please consult the server logs.', - }) - ); - return; - } - - onDone(); - } - - function isStep2Disabled() { - return isStep2AndStep3Disabled(); - } - - function isStep3Disabled() { - return isStep2AndStep3Disabled() || !emailAddress || emailAddress.length === 0; - } - - function isStep2AndStep3Disabled() { - return !!editAction || !selectedEmailActionId || selectedEmailActionId === NEW_ACTION_ID; - } - - function getStep2Status() { - const isDisabled = isStep2AndStep3Disabled(); - - if (isDisabled) { - return 'disabled' as const; - } - - if (emailAddress && emailAddress.length) { - return 'complete' as const; - } - - return 'incomplete' as const; - } - - function getStep1Status() { - if (editAction) { - return 'incomplete' as const; - } - - return selectedEmailActionId ? ('complete' as const) : ('incomplete' as const); - } - - const steps = [ - { - title: emailActions.length - ? i18n.translate('xpack.monitoring.alerts.configuration.selectEmailAction', { - defaultMessage: 'Select email action', - }) - : i18n.translate('xpack.monitoring.alerts.configuration.createEmailAction', { - defaultMessage: 'Create email action', - }), - children: ( - await fetchEmailActions()} - emailActions={emailActions} - selectedEmailActionId={selectedEmailActionId} - setSelectedEmailActionId={setSelectedEmailActionId} - emailAddress={emailAddress} - editAction={editAction} - setEditAction={setEditAction} - /> - ), - status: getStep1Status(), - }, - { - title: i18n.translate('xpack.monitoring.alerts.configuration.setEmailAddress', { - defaultMessage: 'Set the email to receive alerts', - }), - status: getStep2Status(), - children: ( - - ), - }, - { - title: i18n.translate('xpack.monitoring.alerts.configuration.confirm', { - defaultMessage: 'Confirm and save', - }), - status: getStep2Status(), - children: ( - - ), - }, - ]; - - return ( -
- -
- ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx deleted file mode 100644 index 1be66ce4ccfefb..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { omit, pick } from 'lodash'; -import '../../../jest.helpers'; -import { shallow } from 'enzyme'; -import { GetStep1Props } from './step1'; -import { EmailActionData } from '../manage_email_action'; -import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; - -let Step1: React.FC; -let NEW_ACTION_ID: string; - -function setModules() { - Step1 = require('./step1').Step1; - NEW_ACTION_ID = require('./configuration').NEW_ACTION_ID; -} - -describe('Step1', () => { - const emailActions = [ - { - id: '1', - actionTypeId: '1abc', - name: 'Testing', - config: {}, - isPreconfigured: false, - }, - ]; - const selectedEmailActionId = emailActions[0].id; - const setSelectedEmailActionId = jest.fn(); - const emailAddress = 'test@test.com'; - const editAction = null; - const setEditAction = jest.fn(); - const onActionDone = jest.fn(); - - const defaultProps: GetStep1Props = { - onActionDone, - emailActions, - selectedEmailActionId, - setSelectedEmailActionId, - emailAddress, - editAction, - setEditAction, - }; - - beforeEach(() => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: () => { - return {}; - }, - }, - }, - })); - setModules(); - }); - }); - - it('should render normally', () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - describe('creating', () => { - it('should render a create form', () => { - const customProps = { - emailActions: [], - selectedEmailActionId: NEW_ACTION_ID, - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - it('should render the select box if at least one action exists', () => { - const customProps = { - emailActions: [ - { - id: 'foo', - actionTypeId: '.email', - name: '', - config: {}, - isPreconfigured: false, - }, - ], - selectedEmailActionId: NEW_ACTION_ID, - }; - - const component = shallow(); - expect(component.find('EuiSuperSelect').exists()).toBe(true); - }); - - it('should send up the create to the server', async () => { - const kfetch = jest.fn().mockImplementation(() => {}); - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch, - }, - }, - })); - setModules(); - }); - - const customProps = { - emailActions: [], - selectedEmailActionId: NEW_ACTION_ID, - }; - - const component = shallow(); - - const data: EmailActionData = { - service: 'gmail', - host: 'smtp.gmail.com', - port: 465, - secure: true, - from: 'test@test.com', - user: 'user@user.com', - password: 'password', - }; - - const createEmailAction: (data: EmailActionData) => void = component - .find('ManageEmailAction') - .prop('createEmailAction'); - createEmailAction(data); - - expect(kfetch).toHaveBeenCalledWith({ - method: 'POST', - pathname: `/api/actions/action`, - body: JSON.stringify({ - name: 'Email action for Stack Monitoring alerts', - actionTypeId: ALERT_ACTION_TYPE_EMAIL, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - }); - }); - - describe('editing', () => { - it('should allow for editing', () => { - const customProps = { - editAction: emailActions[0], - }; - - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - it('should send up the edit to the server', async () => { - const kfetch = jest.fn().mockImplementation(() => {}); - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch, - }, - }, - })); - setModules(); - }); - - const customProps = { - editAction: emailActions[0], - }; - - const component = shallow(); - - const data: EmailActionData = { - service: 'gmail', - host: 'smtp.gmail.com', - port: 465, - secure: true, - from: 'test@test.com', - user: 'user@user.com', - password: 'password', - }; - - const createEmailAction: (data: EmailActionData) => void = component - .find('ManageEmailAction') - .prop('createEmailAction'); - createEmailAction(data); - - expect(kfetch).toHaveBeenCalledWith({ - method: 'PUT', - pathname: `/api/actions/action/${emailActions[0].id}`, - body: JSON.stringify({ - name: emailActions[0].name, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - }); - }); - - describe('testing', () => { - it('should allow for testing', async () => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: jest.fn().mockImplementation((arg) => { - if (arg.pathname === '/api/actions/action/1/_execute') { - return { status: 'ok' }; - } - return {}; - }), - }, - }, - })); - setModules(); - }); - - const component = shallow(); - - expect(component.find('EuiButton').at(1).prop('isLoading')).toBe(false); - component.find('EuiButton').at(1).simulate('click'); - expect(component.find('EuiButton').at(1).prop('isLoading')).toBe(true); - await component.update(); - expect(component.find('EuiButton').at(1).prop('isLoading')).toBe(false); - }); - - it('should show a successful test', async () => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: (arg: any) => { - if (arg.pathname === '/api/actions/action/1/_execute') { - return { status: 'ok' }; - } - return {}; - }, - }, - }, - })); - setModules(); - }); - - const component = shallow(); - - component.find('EuiButton').at(1).simulate('click'); - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should show a failed test error', async () => { - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: (arg: any) => { - if (arg.pathname === '/api/actions/action/1/_execute') { - return { message: 'Very detailed error message' }; - } - return {}; - }, - }, - }, - })); - setModules(); - }); - - const component = shallow(); - - component.find('EuiButton').at(1).simulate('click'); - await component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should not allow testing if there is no email address', () => { - const customProps = { - emailAddress: '', - }; - const component = shallow(); - expect(component.find('EuiButton').at(1).prop('isDisabled')).toBe(true); - }); - - it('should should a tooltip if there is no email address', () => { - const customProps = { - emailAddress: '', - }; - const component = shallow(); - expect(component.find('EuiToolTip')).toMatchSnapshot(); - }); - }); - - describe('deleting', () => { - it('should send up the delete to the server', async () => { - const kfetch = jest.fn().mockImplementation(() => {}); - jest.isolateModules(() => { - jest.doMock('../../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch, - }, - }, - })); - setModules(); - }); - - const customProps = { - setSelectedEmailActionId: jest.fn(), - onActionDone: jest.fn(), - }; - const component = shallow(); - - await component.find('EuiButton').at(2).simulate('click'); - await component.update(); - - expect(kfetch).toHaveBeenCalledWith({ - method: 'DELETE', - pathname: `/api/actions/action/${emailActions[0].id}`, - }); - - expect(customProps.setSelectedEmailActionId).toHaveBeenCalledWith(''); - expect(customProps.onActionDone).toHaveBeenCalled(); - expect(component.find('EuiButton').at(2).prop('isLoading')).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx deleted file mode 100644 index b3e6c079378ef2..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiText, - EuiSpacer, - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiSuperSelect, - EuiToolTip, - EuiCallOut, -} from '@elastic/eui'; -import { omit, pick } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { Legacy } from '../../../legacy_shims'; -import { ActionResult, BASE_ACTION_API_PATH } from '../../../../../../plugins/actions/common'; -import { ManageEmailAction, EmailActionData } from '../manage_email_action'; -import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; -import { NEW_ACTION_ID } from './configuration'; - -export interface GetStep1Props { - onActionDone: () => Promise; - emailActions: ActionResult[]; - selectedEmailActionId: string; - setSelectedEmailActionId: (id: string) => void; - emailAddress: string; - editAction: ActionResult | null; - setEditAction: (action: ActionResult | null) => void; -} - -export const Step1: React.FC = (props: GetStep1Props) => { - const [isTesting, setIsTesting] = React.useState(false); - const [isDeleting, setIsDeleting] = React.useState(false); - const [testingStatus, setTestingStatus] = React.useState(null); - const [fullTestingError, setFullTestingError] = React.useState(''); - - async function createEmailAction(data: EmailActionData) { - if (props.editAction) { - await Legacy.shims.kfetch({ - method: 'PUT', - pathname: `${BASE_ACTION_API_PATH}/action/${props.editAction.id}`, - body: JSON.stringify({ - name: props.editAction.name, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - props.setEditAction(null); - } else { - await Legacy.shims.kfetch({ - method: 'POST', - pathname: `${BASE_ACTION_API_PATH}/action`, - body: JSON.stringify({ - name: i18n.translate('xpack.monitoring.alerts.configuration.emailAction.name', { - defaultMessage: 'Email action for Stack Monitoring alerts', - }), - actionTypeId: ALERT_ACTION_TYPE_EMAIL, - config: omit(data, ['user', 'password']), - secrets: pick(data, ['user', 'password']), - }), - }); - } - - await props.onActionDone(); - } - - async function deleteEmailAction(id: string) { - setIsDeleting(true); - - await Legacy.shims.kfetch({ - method: 'DELETE', - pathname: `${BASE_ACTION_API_PATH}/action/${id}`, - }); - - if (props.editAction && props.editAction.id === id) { - props.setEditAction(null); - } - if (props.selectedEmailActionId === id) { - props.setSelectedEmailActionId(''); - } - await props.onActionDone(); - setIsDeleting(false); - setTestingStatus(null); - } - - async function testEmailAction() { - setIsTesting(true); - setTestingStatus(null); - - const params = { - subject: 'Kibana alerting test configuration', - message: `This is a test for the configured email action for Kibana alerting.`, - to: [props.emailAddress], - }; - - const result = await Legacy.shims.kfetch({ - method: 'POST', - pathname: `${BASE_ACTION_API_PATH}/action/${props.selectedEmailActionId}/_execute`, - body: JSON.stringify({ params }), - }); - if (result.status === 'ok') { - setTestingStatus(true); - } else { - setTestingStatus(false); - setFullTestingError(result.message); - } - setIsTesting(false); - } - - function getTestButton() { - const isTestingDisabled = !props.emailAddress || props.emailAddress.length === 0; - const testBtn = ( - - {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.buttonText', { - defaultMessage: 'Test', - })} - - ); - - if (isTestingDisabled) { - return ( - - {testBtn} - - ); - } - - return testBtn; - } - - if (props.editAction) { - return ( - - -

- {i18n.translate('xpack.monitoring.alerts.configuration.step1.editAction', { - defaultMessage: 'Edit the action below.', - })} -

-
- - await createEmailAction(data)} - cancel={() => props.setEditAction(null)} - isNew={false} - action={props.editAction} - /> -
- ); - } - - const newAction = ( - - {i18n.translate('xpack.monitoring.alerts.configuration.newActionDropdownDisplay', { - defaultMessage: 'Create new email action...', - })} - - ); - - const options = [ - ...props.emailActions.map((action) => { - const actionLabel = i18n.translate( - 'xpack.monitoring.alerts.configuration.selectAction.inputDisplay', - { - defaultMessage: 'From: {from}, Service: {service}', - values: { - service: action.config.service, - from: action.config.from, - }, - } - ); - - return { - value: action.id, - inputDisplay: {actionLabel}, - dropdownDisplay: {actionLabel}, - }; - }), - { - value: NEW_ACTION_ID, - inputDisplay: newAction, - dropdownDisplay: newAction, - }, - ]; - - let selectBox: React.ReactNode | null = ( - props.setSelectedEmailActionId(id)} - hasDividers - /> - ); - let createNew = null; - if (props.selectedEmailActionId === NEW_ACTION_ID) { - createNew = ( - - await createEmailAction(data)} - isNew={true} - /> - - ); - - // If there are no actions, do not show the select box as there are no choices - if (props.emailActions.length === 0) { - selectBox = null; - } else { - // Otherwise, add a spacer - selectBox = ( - - {selectBox} - - - ); - } - } - - let manageConfiguration = null; - const selectedEmailAction = props.emailActions.find( - (action) => action.id === props.selectedEmailActionId - ); - - if ( - props.selectedEmailActionId !== NEW_ACTION_ID && - props.selectedEmailActionId && - selectedEmailAction - ) { - let testingStatusUi = null; - if (testingStatus === true) { - testingStatusUi = ( - - - -

- {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.success', { - defaultMessage: 'Looks good on our end!', - })} -

-
-
- ); - } else if (testingStatus === false) { - testingStatusUi = ( - - - -

{fullTestingError}

-
-
- ); - } - - manageConfiguration = ( - - - - - { - const editAction = - props.emailActions.find((action) => action.id === props.selectedEmailActionId) || - null; - props.setEditAction(editAction); - }} - > - {i18n.translate( - 'xpack.monitoring.alerts.configuration.editConfiguration.buttonText', - { - defaultMessage: 'Edit', - } - )} - - - {getTestButton()} - - deleteEmailAction(props.selectedEmailActionId)} - isLoading={isDeleting} - > - {i18n.translate( - 'xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText', - { - defaultMessage: 'Delete', - } - )} - - - - {testingStatusUi} - - ); - } - - return ( - - {selectBox} - {manageConfiguration} - {createNew} - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx deleted file mode 100644 index 14e3cb078f9cca..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import '../../../jest.helpers'; -import { shallow } from 'enzyme'; -import { Step2, GetStep2Props } from './step2'; - -describe('Step2', () => { - const defaultProps: GetStep2Props = { - emailAddress: 'test@test.com', - setEmailAddress: jest.fn(), - showFormErrors: false, - formErrors: { email: null }, - isDisabled: false, - }; - - it('should render normally', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should set the email address properly', () => { - const newEmail = 'email@email.com'; - const component = shallow(); - component.find('EuiFieldText').simulate('change', { target: { value: newEmail } }); - expect(defaultProps.setEmailAddress).toHaveBeenCalledWith(newEmail); - }); - - it('should show form errors', () => { - const customProps = { - showFormErrors: true, - formErrors: { - email: 'This is required', - }, - }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should disable properly', () => { - const customProps = { - isDisabled: true, - }; - const component = shallow(); - expect(component.find('EuiFieldText').prop('disabled')).toBe(true); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx deleted file mode 100644 index 2c215e310af69a..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { AlertsConfigurationForm } from './configuration'; - -export interface GetStep2Props { - emailAddress: string; - setEmailAddress: (email: string) => void; - showFormErrors: boolean; - formErrors: AlertsConfigurationForm; - isDisabled: boolean; -} - -export const Step2: React.FC = (props: GetStep2Props) => { - return ( - - - props.setEmailAddress(e.target.value)} - /> - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx deleted file mode 100644 index 9b1304c42a507d..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import '../../../jest.helpers'; -import { shallow } from 'enzyme'; -import { Step3 } from './step3'; - -describe('Step3', () => { - const defaultProps = { - isSaving: false, - isDisabled: false, - save: jest.fn(), - error: null, - }; - - it('should render normally', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should save properly', () => { - const component = shallow(); - component.find('EuiButton').simulate('click'); - expect(defaultProps.save).toHaveBeenCalledWith(); - }); - - it('should show a saving state', () => { - const customProps = { isSaving: true }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should show a disabled state', () => { - const customProps = { isDisabled: true }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should show an error', () => { - const customProps = { error: 'Test error' }; - const component = shallow(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx deleted file mode 100644 index 80acb8992cbc16..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export interface GetStep3Props { - isSaving: boolean; - isDisabled: boolean; - save: () => void; - error: string | null; -} - -export const Step3: React.FC = (props: GetStep3Props) => { - let errorUi = null; - if (props.error) { - errorUi = ( - - -

{props.error}

-
- -
- ); - } - - return ( - - {errorUi} - - {i18n.translate('xpack.monitoring.alerts.configuration.save', { - defaultMessage: 'Save', - })} - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js b/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js deleted file mode 100644 index d23b5b60318c1d..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import 'moment-duration-format'; -import React from 'react'; -import { formatTimestampToDuration } from '../../../common/format_timestamp_to_duration'; -import { CALCULATE_DURATION_UNTIL } from '../../../common/constants'; -import { EuiLink } from '@elastic/eui'; -import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; - -export function FormattedAlert({ prefix, suffix, message, metadata }) { - const formattedAlert = (() => { - if (metadata && metadata.link) { - if (metadata.link.startsWith('https')) { - return ( - - {message} - - ); - } - - return ( - - {message} - - ); - } - - return message; - })(); - - if (metadata && metadata.time) { - // scan message prefix and replace relative times - // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. - prefix = prefix.replace( - /{{#relativeTime}}metadata\.([\w\.]+){{\/relativeTime}}/, - (_match, field) => { - return formatTimestampToDuration(metadata[field], CALCULATE_DURATION_UNTIL); - } - ); - prefix = prefix.replace( - /{{#absoluteTime}}metadata\.([\w\.]+){{\/absoluteTime}}/, - (_match, field) => { - return moment.tz(metadata[field], moment.tz.guess()).format('LLL z'); - } - ); - } - - // suffix and prefix don't contain spaces - const formattedPrefix = prefix ? `${prefix} ` : null; - const formattedSuffix = suffix ? ` ${suffix}` : null; - return ( - - {formattedPrefix} - {formattedAlert} - {formattedSuffix} - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx deleted file mode 100644 index 87588a435078d4..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiForm, - EuiFormRow, - EuiFieldText, - EuiLink, - EuiSpacer, - EuiFieldNumber, - EuiFieldPassword, - EuiSwitch, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ActionResult } from '../../../../../plugins/actions/common'; -import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; -import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; - -export interface EmailActionData { - service: string; - host: string; - port?: number; - secure: boolean; - from: string; - user: string; - password: string; -} - -interface ManageActionModalProps { - createEmailAction: (handler: EmailActionData) => void; - cancel?: () => void; - isNew: boolean; - action?: ActionResult | null; -} - -const DEFAULT_DATA: EmailActionData = { - service: '', - host: '', - port: 0, - secure: false, - from: '', - user: '', - password: '', -}; - -const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', { - defaultMessage: 'Create email action', -}); -const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', { - defaultMessage: 'Save email action', -}); -const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', { - defaultMessage: 'Cancel', -}); - -const NEW_SERVICE_ID = '__new__'; - -export const ManageEmailAction: React.FC = ( - props: ManageActionModalProps -) => { - const { createEmailAction, cancel, isNew, action } = props; - - const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {}); - const [isSaving, setIsSaving] = React.useState(false); - const [showErrors, setShowErrors] = React.useState(false); - const [errors, setErrors] = React.useState( - getMissingFieldErrors(defaultData, DEFAULT_DATA) - ); - const [data, setData] = React.useState(defaultData); - const [createNewService, setCreateNewService] = React.useState(false); - const [newService, setNewService] = React.useState(''); - - React.useEffect(() => { - const missingFieldErrors = getMissingFieldErrors(data, DEFAULT_DATA); - if (!missingFieldErrors.service) { - if (data.service === NEW_SERVICE_ID && !newService) { - missingFieldErrors.service = getRequiredFieldError('service'); - } - } - setErrors(missingFieldErrors); - }, [data, newService]); - - async function saveEmailAction() { - setShowErrors(true); - if (!hasErrors(errors)) { - setShowErrors(false); - setIsSaving(true); - const mergedData = { - ...data, - service: data.service === NEW_SERVICE_ID ? newService : data.service, - }; - try { - await createEmailAction(mergedData); - } catch (err) { - setErrors({ - general: err.body.message, - }); - } - } - } - - const serviceOptions = ALERT_EMAIL_SERVICES.map((service) => ({ - value: service, - inputDisplay: {service}, - dropdownDisplay: {service}, - })); - - serviceOptions.push({ - value: NEW_SERVICE_ID, - inputDisplay: ( - - {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText', { - defaultMessage: 'Adding new service...', - })} - - ), - dropdownDisplay: ( - - {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addNewServiceText', { - defaultMessage: 'Add new service...', - })} - - ), - }); - - let addNewServiceUi = null; - if (createNewService) { - addNewServiceUi = ( - - - setNewService(e.target.value)} - isInvalid={showErrors} - /> - - ); - } - - return ( - - - {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', { - defaultMessage: 'Find out more', - })} - - } - error={errors.service} - isInvalid={showErrors && !!errors.service} - > - - { - if (id === NEW_SERVICE_ID) { - setCreateNewService(true); - setData({ ...data, service: NEW_SERVICE_ID }); - } else { - setCreateNewService(false); - setData({ ...data, service: id }); - } - }} - hasDividers - isInvalid={showErrors && !!errors.service} - /> - {addNewServiceUi} - - - - - setData({ ...data, host: e.target.value })} - isInvalid={showErrors && !!errors.host} - /> - - - - setData({ ...data, port: parseInt(e.target.value, 10) })} - isInvalid={showErrors && !!errors.port} - /> - - - - setData({ ...data, secure: e.target.checked })} - /> - - - - setData({ ...data, from: e.target.value })} - isInvalid={showErrors && !!errors.from} - /> - - - - setData({ ...data, user: e.target.value })} - isInvalid={showErrors && !!errors.user} - /> - - - - setData({ ...data, password: e.target.value })} - isInvalid={showErrors && !!errors.password} - /> - - - - - - - - {isNew ? CREATE_LABEL : SAVE_LABEL} - - - {!action || isNew ? null : ( - - {CANCEL_LABEL} - - )} - - - ); -}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js deleted file mode 100644 index 8232e0a8908d04..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/map_severity.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { upperFirst } from 'lodash'; - -/** - * Map the {@code severity} value to the associated alert level to be usable within the UI. - * - *
    - *
  1. Low: [0, 999) represents an informational level alert.
  2. - *
  3. Medium: [1000, 1999) represents a warning level alert.
  4. - *
  5. High: Any other value.
  6. - *
- * - * The object returned is in the form of: - * - * - * { - * value: 'medium', - * color: 'warning', - * iconType: 'dot', - * title: 'Warning severity alert' - * } - * - * - * @param {Number} severity The number representing the severity. Higher is "worse". - * @return {Object} An object containing details about the severity. - */ - -import { i18n } from '@kbn/i18n'; - -export function mapSeverity(severity) { - const floor = Math.floor(severity / 1000); - let mapped; - - switch (floor) { - case 0: - mapped = { - value: i18n.translate('xpack.monitoring.alerts.lowSeverityName', { defaultMessage: 'low' }), - color: 'warning', - iconType: 'iInCircle', - }; - break; - case 1: - mapped = { - value: i18n.translate('xpack.monitoring.alerts.mediumSeverityName', { - defaultMessage: 'medium', - }), - color: 'warning', - iconType: 'alert', - }; - break; - default: - // severity >= 2000 - mapped = { - value: i18n.translate('xpack.monitoring.alerts.highSeverityName', { - defaultMessage: 'high', - }), - color: 'danger', - iconType: 'bell', - }; - break; - } - - return { - title: i18n.translate('xpack.monitoring.alerts.severityTitle', { - defaultMessage: '{severity} severity alert', - values: { severity: upperFirst(mapped.value) }, - }), - ...mapped, - }; -} diff --git a/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx deleted file mode 100644 index 1c35328d2f8812..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { Legacy } from '../../legacy_shims'; -import { AlertsStatus, AlertsStatusProps } from './status'; -import { ALERT_TYPES } from '../../../common/constants'; -import { getSetupModeState } from '../../lib/setup_mode'; -import { mockUseEffects } from '../../jest.helpers'; - -jest.mock('../../lib/setup_mode', () => ({ - getSetupModeState: jest.fn(), - addSetupModeCallback: jest.fn(), - toggleSetupMode: jest.fn(), -})); - -jest.mock('../../legacy_shims', () => ({ - Legacy: { - shims: { - kfetch: jest.fn(), - docLinks: { - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', - }, - }, - }, -})); - -const defaultProps: AlertsStatusProps = { - clusterUuid: '1adsb23', - emailAddress: 'test@elastic.co', -}; - -describe('Status', () => { - beforeEach(() => { - mockUseEffects(2); - - (getSetupModeState as jest.Mock).mockReturnValue({ - enabled: false, - }); - - (Legacy.shims.kfetch as jest.Mock).mockImplementation(({ pathname }) => { - if (pathname === '/internal/security/api_key/privileges') { - return { areApiKeysEnabled: true }; - } - return { - data: [], - }; - }); - }); - - it('should render without setup mode', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - it('should render a flyout when clicking the link', async () => { - (getSetupModeState as jest.Mock).mockReturnValue({ - enabled: true, - }); - - const component = shallow(); - component.find('EuiLink').simulate('click'); - await component.update(); - expect(component.find('EuiFlyout')).toMatchSnapshot(); - }); - - it('should render a success message if all alerts have been migrated and in setup mode', async () => { - (Legacy.shims.kfetch as jest.Mock).mockReturnValue({ - data: ALERT_TYPES.map((type) => ({ alertTypeId: type })), - }); - - (getSetupModeState as jest.Mock).mockReturnValue({ - enabled: true, - }); - - const component = shallow(); - await component.update(); - expect(component.find('EuiCallOut')).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.tsx deleted file mode 100644 index 6f72168f5069b3..00000000000000 --- a/x-pack/plugins/monitoring/public/components/alerts/status.tsx +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiSpacer, - EuiCallOut, - EuiTitle, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiLink, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Legacy } from '../../legacy_shims'; -import { Alert, BASE_ALERT_API_PATH } from '../../../../alerts/common'; -import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; -import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; -import { AlertsConfiguration } from './configuration'; - -export interface AlertsStatusProps { - clusterUuid: string; - emailAddress: string; -} - -export const AlertsStatus: React.FC = (props: AlertsStatusProps) => { - const { emailAddress } = props; - - const [setupModeEnabled, setSetupModeEnabled] = React.useState(getSetupModeState().enabled); - const [kibanaAlerts, setKibanaAlerts] = React.useState([]); - const [showMigrationFlyout, setShowMigrationFlyout] = React.useState(false); - const [isSecurityConfigured, setIsSecurityConfigured] = React.useState(false); - - React.useEffect(() => { - async function fetchAlertsStatus() { - const alerts = await Legacy.shims.kfetch({ - method: 'GET', - pathname: `${BASE_ALERT_API_PATH}/_find`, - }); - const monitoringAlerts = alerts.data.filter((alert: Alert) => - alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) - ); - setKibanaAlerts(monitoringAlerts); - } - - fetchAlertsStatus(); - fetchSecurityConfigured(); - }, [setupModeEnabled, showMigrationFlyout]); - - React.useEffect(() => { - if (!setupModeEnabled && showMigrationFlyout) { - setShowMigrationFlyout(false); - } - }, [setupModeEnabled, showMigrationFlyout]); - - async function fetchSecurityConfigured() { - const response = await Legacy.shims.kfetch({ - pathname: '/internal/security/api_key/privileges', - }); - setIsSecurityConfigured(response.areApiKeysEnabled); - } - - addSetupModeCallback(() => setSetupModeEnabled(getSetupModeState().enabled)); - - function enterSetupModeAndOpenFlyout() { - toggleSetupMode(true); - setShowMigrationFlyout(true); - } - - function getSecurityConfigurationErrorUi() { - if (isSecurityConfigured) { - return null; - } - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; - const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; - return ( - - - -

- - {i18n.translate( - 'xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel', - { - defaultMessage: 'docs', - } - )} - - ), - }} - /> -

-
-
- ); - } - - function renderContent() { - let flyout = null; - if (showMigrationFlyout) { - flyout = ( - setShowMigrationFlyout(false)} aria-labelledby="flyoutTitle"> - - -

- {i18n.translate('xpack.monitoring.alerts.status.flyoutTitle', { - defaultMessage: 'Monitoring alerts', - })} -

-
- -

- {i18n.translate('xpack.monitoring.alerts.status.flyoutSubtitle', { - defaultMessage: 'Configure an email server and email address to receive alerts.', - })} -

-
- {getSecurityConfigurationErrorUi()} -
- - setShowMigrationFlyout(false)} - /> - -
- ); - } - - const allMigrated = kibanaAlerts.length >= NUMBER_OF_MIGRATED_ALERTS; - if (allMigrated) { - if (setupModeEnabled) { - return ( - - -

- - {i18n.translate('xpack.monitoring.alerts.status.manage', { - defaultMessage: 'Want to make changes? Click here.', - })} - -

-
- {flyout} -
- ); - } - } else { - return ( - - -

- - {i18n.translate('xpack.monitoring.alerts.status.needToMigrate', { - defaultMessage: 'Migrate cluster alerts to our new alerting platform.', - })} - -

-
- {flyout} -
- ); - } - } - - const content = renderContent(); - if (content) { - return ( - - {content} - - - ); - } - - return null; -}; diff --git a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js index c6bd0773343e0c..b760d35cfa2dc6 100644 --- a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js +++ b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { AlertsBadge } from '../../alerts/badge'; const zoomOutBtn = (zoomInfo) => { if (!zoomInfo || !zoomInfo.showZoomOutBtn()) { @@ -67,42 +68,56 @@ export function MonitoringTimeseriesContainer({ series, onBrush, zoomInfo }) { }), ].concat(series.map((item) => `${item.metric.label}: ${item.metric.description}`)); + let alertStatus = null; + if (series.alerts) { + alertStatus = ( + + + + ); + } + return ( - + - - - -

- {getTitle(series)} - {units ? ` (${units})` : ''} - - - - - -

-
-
+ - - } - /> - - - {seriesScreenReaderTextList.join('. ')} - - - + + + +

+ {getTitle(series)} + {units ? ` (${units})` : ''} + + + + + +

+
+
+ + + } + /> + + + {seriesScreenReaderTextList.join('. ')} + + + + + {zoomOutBtn(zoomInfo)} +
- {zoomOutBtn(zoomInfo)} + {alertStatus}
diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js deleted file mode 100644 index 68d7a5a94e42f8..00000000000000 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mapSeverity } from '../../alerts/map_severity'; -import { EuiHealth, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -const HIGH_SEVERITY = 2000; -const MEDIUM_SEVERITY = 1000; -const LOW_SEVERITY = 0; - -export function AlertsIndicator({ alerts }) { - if (alerts && alerts.count > 0) { - const severity = (() => { - if (alerts.high > 0) { - return HIGH_SEVERITY; - } - if (alerts.medium > 0) { - return MEDIUM_SEVERITY; - } - return LOW_SEVERITY; - })(); - const severityIcon = mapSeverity(severity); - const tooltipText = (() => { - switch (severity) { - case HIGH_SEVERITY: - return i18n.translate( - 'xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip', - { - defaultMessage: - 'There are some critical cluster issues that require your immediate attention!', - } - ); - case MEDIUM_SEVERITY: - return i18n.translate( - 'xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip', - { - defaultMessage: 'There are some issues that might have impact on your cluster.', - } - ); - default: - // might never show - return i18n.translate( - 'xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip', - { - defaultMessage: 'There are some low-severity cluster issues', - } - ); - } - })(); - - return ( - - - - - - ); - } - - return ( - - - - - - ); -} 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 b90e7b52f4962a..4dc4201e358fb9 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -14,16 +14,16 @@ import { EuiPage, EuiPageBody, EuiPageContent, - EuiToolTip, EuiCallOut, EuiSpacer, EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { EuiMonitoringTable } from '../../table'; -import { AlertsIndicator } from '../../cluster/listing/alerts_indicator'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { AlertsStatus } from '../../../alerts/status'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import './listing.scss'; @@ -31,8 +31,6 @@ const IsClusterSupported = ({ isSupported, children }) => { return isSupported ? children : '-'; }; -const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster'; - /* * This checks if alerts feature is supported via monitoring cluster * license. If the alerts feature is not supported because the prod cluster @@ -61,6 +59,8 @@ const IsAlertsSupported = (props) => { ); }; +const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster'; + const getColumns = ( showLicenseExpiration, changeCluster, @@ -119,7 +119,7 @@ const getColumns = ( render: (_status, cluster) => ( - + ), diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js deleted file mode 100644 index 2dc76aa7e4496c..00000000000000 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import moment from 'moment-timezone'; -import { FormattedAlert } from '../../alerts/formatted_alert'; -import { mapSeverity } from '../../alerts/map_severity'; -import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import { - CALCULATE_DURATION_SINCE, - KIBANA_ALERTING_ENABLED, - CALCULATE_DURATION_UNTIL, -} from '../../../../common/constants'; -import { formatDateTimeLocal } from '../../../../common/formatting'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiButton, - EuiText, - EuiSpacer, - EuiCallOut, - EuiLink, -} from '@elastic/eui'; - -function replaceTokens(alert) { - if (!alert.message.tokens) { - return alert.message.text; - } - - let text = alert.message.text; - - for (const token of alert.message.tokens) { - if (token.type === 'time') { - text = text.replace( - token.startToken, - token.isRelative - ? formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) - : moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z') - ); - } else if (token.type === 'link') { - const linkPart = new RegExp(`${token.startToken}(.+?)${token.endToken}`).exec(text); - // TODO: we assume this is at the end, which works for now but will not always work - const nonLinkText = text.replace(linkPart[0], ''); - text = ( - - {nonLinkText} - {linkPart[1]} - - ); - } - } - - return text; -} - -export function AlertsPanel({ alerts }) { - if (!alerts || !alerts.length) { - // no-op - return null; - } - - // enclosed component for accessing - function TopAlertItem({ item, index }) { - const severityIcon = mapSeverity(item.metadata.severity); - - if (item.resolved_timestamp) { - severityIcon.title = i18n.translate( - 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', - { - defaultMessage: '{severityIconTitle} (resolved {time} ago)', - values: { - severityIconTitle: severityIcon.title, - time: formatTimestampToDuration(item.resolved_timestamp, CALCULATE_DURATION_SINCE), - }, - } - ); - severityIcon.color = 'success'; - severityIcon.iconType = 'check'; - } - - return ( - - - - -

- -

-
-
- ); - } - - const alertsList = KIBANA_ALERTING_ENABLED - ? alerts.map((alert, idx) => { - const callOutProps = mapSeverity(alert.severity); - const message = replaceTokens(alert); - - if (!alert.isFiring) { - callOutProps.title = i18n.translate( - 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', - { - defaultMessage: '{severityIconTitle} (resolved {time} ago)', - values: { - severityIconTitle: callOutProps.title, - time: formatTimestampToDuration(alert.resolvedMS, CALCULATE_DURATION_SINCE), - }, - } - ); - callOutProps.color = 'success'; - callOutProps.iconType = 'check'; - } - - return ( - - -

{message}

- - -

- -

-
-
- -
- ); - }) - : alerts.map((item, index) => ( - - )); - - return ( -
- - - -

- -

-
-
- - - - - -
- - {alertsList} - -
- ); -} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 034bacfb3bf62a..edf4c5d73f837f 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -5,11 +5,11 @@ */ import React, { Fragment } from 'react'; +import moment from 'moment-timezone'; import { get, capitalize } from 'lodash'; import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, - HealthStatusIndicator, BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink, } from './helpers'; @@ -26,14 +26,24 @@ import { EuiBadge, EuiToolTip, EuiFlexGroup, + EuiHealth, + EuiText, } from '@elastic/eui'; -import { LicenseText } from './license_text'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from '../../logs/reason'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { + ELASTICSEARCH_SYSTEM_ID, + ALERT_LICENSE_EXPIRATION, + ALERT_CLUSTER_HEALTH, + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, +} from '../../../../common/constants'; +import { AlertsBadge } from '../../../alerts/badge'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; const calculateShards = (shards) => { const total = get(shards, 'total', 0); @@ -53,6 +63,8 @@ const calculateShards = (shards) => { }; }; +const formatDateLocal = (input) => moment.tz(input, moment.tz.guess()).format('LL'); + function getBadgeColorFromLogLevel(level) { switch (level) { case 'warn': @@ -138,11 +150,20 @@ function renderLog(log) { ); } +const OVERVIEW_PANEL_ALERTS = [ALERT_CLUSTER_HEALTH, ALERT_LICENSE_EXPIRATION]; + +const NODES_PANEL_ALERTS = [ + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, +]; + export function ElasticsearchPanel(props) { const clusterStats = props.cluster_stats || {}; const nodes = clusterStats.nodes; const indices = clusterStats.indices; const setupMode = props.setupMode; + const alerts = props.alerts; const goToElasticsearch = () => getSafeForExternalLink('#/elasticsearch'); const goToNodes = () => getSafeForExternalLink('#/elasticsearch/nodes'); @@ -150,12 +171,6 @@ export function ElasticsearchPanel(props) { const { primaries, replicas } = calculateShards(get(props, 'cluster_stats.indices.shards', {})); - const statusIndicator = ; - - const licenseText = ( - - ); - const setupModeData = get(setupMode.data, 'elasticsearch'); const setupModeTooltip = setupMode && setupMode.enabled ? ( @@ -199,40 +214,80 @@ export function ElasticsearchPanel(props) { return null; }; + const statusColorMap = { + green: 'success', + yellow: 'warning', + red: 'danger', + }; + + let nodesAlertStatus = null; + if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { + const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + nodesAlertStatus = ( + + + + ); + } + + let overviewAlertStatus = null; + if (shouldShowAlertBadge(alerts, OVERVIEW_PANEL_ALERTS)) { + const alertsList = OVERVIEW_PANEL_ALERTS.map((alertType) => alerts[alertType]); + overviewAlertStatus = ( + + + + ); + } + return ( - + - -

- - - -

-
+ + + +

+ + + +

+
+
+ {overviewAlertStatus} +
+ + + + + + + + {showMlJobs()} + + + + + + + + {capitalize(props.license.type)} + + + + + {props.license.expiry_date_in_millis === undefined ? ( + '' + ) : ( + + )} + + + +
- +

@@ -280,7 +365,12 @@ export function ElasticsearchPanel(props) {

- {setupModeTooltip} + + + {setupModeTooltip} + {nodesAlertStatus} + +
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js index 0d9290225cd5f0..4f6fa520750bda 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js @@ -29,13 +29,17 @@ export function HealthStatusIndicator(props) { const statusColor = statusColorMap[props.status] || 'n/a'; return ( - - - + + + + + + + ); } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js index 88c626b5ad5aeb..66701c1dfd95a0 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js @@ -8,24 +8,14 @@ import React, { Fragment } from 'react'; import { ElasticsearchPanel } from './elasticsearch_panel'; import { KibanaPanel } from './kibana_panel'; import { LogstashPanel } from './logstash_panel'; -import { AlertsPanel } from './alerts_panel'; import { BeatsPanel } from './beats_panel'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AlertsStatus } from '../../alerts/status'; -import { - STANDALONE_CLUSTER_CLUSTER_UUID, - KIBANA_ALERTING_ENABLED, -} from '../../../../common/constants'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; - - const kibanaAlerts = KIBANA_ALERTING_ENABLED ? ( - - ) : null; - return ( @@ -38,10 +28,6 @@ export function Overview(props) { - {kibanaAlerts} - - - {!isFromStandaloneCluster ? ( + - ) : null} - + diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 8bf2bc472b8fdc..eb1f82eb5550dc 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -28,11 +28,16 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; -import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; +import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { AlertsBadge } from '../../../alerts/badge'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; + +const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; export function KibanaPanel(props) { const setupMode = props.setupMode; + const alerts = props.alerts; const showDetectedKibanas = setupMode.enabled && get(setupMode.data, 'kibana.detected.doesExist', false); if (!props.count && !showDetectedKibanas) { @@ -54,6 +59,16 @@ export function KibanaPanel(props) { /> ) : null; + let instancesAlertStatus = null; + if (shouldShowAlertBadge(alerts, INSTANCES_PANEL_ALERTS)) { + const alertsList = INSTANCES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + instancesAlertStatus = ( + + + + ); + } + return ( - +

@@ -148,7 +163,12 @@ export function KibanaPanel(props) {

- {setupModeTooltip} + + + {setupModeTooltip} + {instancesAlertStatus} + +
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js deleted file mode 100644 index 19905b9d7791ab..00000000000000 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import moment from 'moment-timezone'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; -import { capitalize } from 'lodash'; -import { EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -const formatDateLocal = (input) => moment.tz(input, moment.tz.guess()).format('LL'); - -export function LicenseText({ license, showLicenseExpiration }) { - if (!showLicenseExpiration) { - return null; - } - - return ( - - - ), - }} - /> - - ); -} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index e81f9b64dcb4b8..7c9758bc0ddb60 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -11,7 +11,11 @@ import { BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink, } from './helpers'; -import { LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { + LOGSTASH, + LOGSTASH_SYSTEM_ID, + ALERT_LOGSTASH_VERSION_MISMATCH, +} from '../../../../common/constants'; import { EuiFlexGrid, @@ -31,11 +35,16 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { AlertsBadge } from '../../../alerts/badge'; +import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; + +const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; export function LogstashPanel(props) { const { setupMode } = props; const nodesCount = props.node_count || 0; const queueTypes = props.queue_types || {}; + const alerts = props.alerts; // Do not show if we are not in setup mode if (!nodesCount && !setupMode.enabled) { @@ -56,6 +65,16 @@ export function LogstashPanel(props) { /> ) : null; + let nodesAlertStatus = null; + if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { + const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + nodesAlertStatus = ( + + + + ); + } + return ( - +

@@ -141,7 +160,12 @@ export function LogstashPanel(props) {

- {setupModeTooltip} + + + {setupModeTooltip} + {nodesAlertStatus} + +
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js index aea2456a3f3d45..ba19ed0ae1913e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js @@ -10,7 +10,7 @@ import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function ClusterStatus({ stats }) { +export function ClusterStatus({ stats, alerts }) { const { dataSize, nodesCount, @@ -81,6 +81,7 @@ export function ClusterStatus({ stats }) { diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js index 418661ff322e48..f91e251030d76e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { get } from 'lodash'; import { EuiPage, EuiPageContent, @@ -20,8 +21,33 @@ import { Logs } from '../../logs/'; import { MonitoringTimeseriesContainer } from '../../chart'; import { ShardAllocation } from '../shard_allocation/shard_allocation'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsCallout } from '../../../alerts/callout'; + +export const Node = ({ + nodeSummary, + metrics, + logs, + alerts, + nodeId, + clusterUuid, + scope, + ...props +}) => { + if (alerts) { + for (const alertTypeId of Object.keys(alerts)) { + const alertInstance = alerts[alertTypeId]; + for (const { meta } of alertInstance.states) { + const metricList = get(meta, 'metrics', []); + for (const metric of metricList) { + if (metrics[metric]) { + metrics[metric].alerts = metrics[metric].alerts || {}; + metrics[metric].alerts[alertTypeId] = alertInstance; + } + } + } + } + } -export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, ...props }) => { const metricsToShow = [ metrics.node_jvm_mem, metrics.node_mem, @@ -31,6 +57,7 @@ export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, . metrics.node_latency, metrics.node_segment_count, ]; + return ( @@ -43,9 +70,10 @@ export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, . - + + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js index f912d2755b0c73..18533b3bd4b5ea 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js @@ -10,7 +10,7 @@ import { NodeStatusIcon } from '../node'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function NodeDetailStatus({ stats }) { +export function NodeDetailStatus({ stats, alerts }) { const { transport_address: transportAddress, usedHeap, @@ -28,6 +28,10 @@ export function NodeDetailStatus({ stats }) { const percentSpaceUsed = (freeSpace / totalSpace) * 100; const metrics = [ + { + label: 'Alerts', + value: {Object.values(alerts).length}, + }, { label: i18n.translate('xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress', { defaultMessage: 'Transport Address', diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 8844388f8647a8..c2e5c8e22a1c06 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -5,7 +5,6 @@ */ import React, { Fragment } from 'react'; -import { NodeStatusIcon } from '../node'; import { extractIp } from '../../../lib/extract_ip'; // TODO this is only used for elasticsearch nodes summary / node detail, so it should be moved to components/elasticsearch/nodes/lib import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ClusterStatus } from '../cluster_status'; @@ -25,12 +24,14 @@ import { EuiButton, EuiText, EuiScreenReaderOnly, + EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { AlertsStatus } from '../../../alerts/status'; const getNodeTooltip = (node) => { const { nodeTypeLabel, nodeTypeClass } = node; @@ -56,7 +57,7 @@ const getNodeTooltip = (node) => { }; const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']); -const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { +const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts) => { const cols = []; const cpuUsageColumnTitle = i18n.translate( @@ -123,6 +124,18 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { }, }); + cols.push({ + name: i18n.translate('xpack.monitoring.elasticsearch.nodes.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'alerts', + width: '175px', + sortable: true, + render: () => { + return ; + }, + }); + cols.push({ name: i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumnTitle', { defaultMessage: 'Status', @@ -138,9 +151,20 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { defaultMessage: 'Offline', }); return ( -
- {status} -
+ + + {status} + + ); }, }); @@ -197,14 +221,16 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { name: cpuUsageColumnTitle, field: 'node_cpu_utilization', sortable: getSortHandler('node_cpu_utilization'), - render: (value, node) => ( - - ), + render: (value, node) => { + return ( + + ); + }, }); cols.push({ @@ -263,8 +289,17 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { }; export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsearch, ...props }) { - const { sorting, pagination, onTableChange, clusterUuid, setupMode, fetchMoreData } = props; - const columns = getColumns(showCgroupMetricsElasticsearch, setupMode, clusterUuid); + const { + sorting, + pagination, + onTableChange, + clusterUuid, + setupMode, + fetchMoreData, + alerts, + } = props; + + const columns = getColumns(showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts); // Merge the nodes data with the setup data if enabled const nodes = props.nodes || []; @@ -392,7 +427,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear return ( - + diff --git a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js index c9b95eb4876d86..32d2bdadcea960 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js @@ -10,7 +10,7 @@ import { KibanaStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function ClusterStatus({ stats }) { +export function ClusterStatus({ stats, alerts }) { const { concurrent_connections: connections, count: instances, @@ -65,6 +65,7 @@ export function ClusterStatus({ stats }) { diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js index 9f960c8ddea09d..95a9276569bb15 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js @@ -14,11 +14,12 @@ import { EuiLink, EuiCallOut, EuiScreenReaderOnly, + EuiToolTip, + EuiHealth, } from '@elastic/eui'; import { capitalize, get } from 'lodash'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; -import { KibanaStatusIcon } from '../status_icon'; import { StatusIcon } from '../../status_icon'; import { formatMetric, formatNumber } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; @@ -27,8 +28,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { SetupModeBadge } from '../../setup_mode/badge'; import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { AlertsStatus } from '../../../alerts/status'; -const getColumns = (setupMode) => { +const getColumns = (setupMode, alerts) => { const columns = [ { name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', { @@ -79,33 +81,34 @@ const getColumns = (setupMode) => { ); }, }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'isOnline', + width: '175px', + sortable: true, + render: () => { + return ; + }, + }, { name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', { defaultMessage: 'Status', }), field: 'status', - render: (status, kibana) => ( -
- -   - {!kibana.availability ? ( - - ) : ( - capitalize(status) - )} -
- ), + render: (status, kibana) => { + return ( + + + {capitalize(status)} + + + ); + }, }, { name: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', { @@ -158,7 +161,7 @@ const getColumns = (setupMode) => { export class KibanaInstances extends PureComponent { render() { - const { clusterStatus, setupMode, sorting, pagination, onTableChange } = this.props; + const { clusterStatus, alerts, setupMode, sorting, pagination, onTableChange } = this.props; let setupModeCallOut = null; // Merge the instances data with the setup data if enabled @@ -254,7 +257,7 @@ export class KibanaInstances extends PureComponent { - + {setupModeCallOut} @@ -262,7 +265,7 @@ export class KibanaInstances extends PureComponent { ({ Legacy: { shims: { getBasePath: () => '', - capabilities: { - get: () => ({ logs: { show: true } }), - }, + capabilities: { logs: { show: true } }, }, }, })); diff --git a/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js index 9d5a6a184b4e8c..abd18b61da8ff8 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js @@ -9,7 +9,7 @@ import { SummaryStatus } from '../../summary_status'; import { formatMetric } from '../../../lib/format_number'; import { i18n } from '@kbn/i18n'; -export function ClusterStatus({ stats }) { +export function ClusterStatus({ stats, alerts }) { const { node_count: nodeCount, avg_memory_used: avgMemoryUsed, @@ -49,5 +49,5 @@ export function ClusterStatus({ stats }) { }, ]; - return ; + return ; } diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap index edb7d139bb9352..2e01fce7247dc0 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap @@ -11,6 +11,13 @@ exports[`Listing should render with certain data pieces missing 1`] = ` "render": [Function], "sortable": true, }, + Object { + "field": "isOnline", + "name": "Alerts", + "render": [Function], + "sortable": true, + "width": "175px", + }, Object { "field": "cpu_usage", "name": "CPU Usage", @@ -106,6 +113,13 @@ exports[`Listing should render with expected props 1`] = ` "render": [Function], "sortable": true, }, + Object { + "field": "isOnline", + "name": "Alerts", + "render": [Function], + "sortable": true, + "width": "175px", + }, Object { "field": "cpu_usage", "name": "CPU Usage", diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index 78eb982a95dd7e..caa21e5e692920 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -16,7 +16,7 @@ import { EuiScreenReaderOnly, } from '@elastic/eui'; import { formatPercentageUsage, formatNumber } from '../../../lib/format_number'; -import { ClusterStatus } from '..//cluster_status'; +import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,10 +24,12 @@ import { LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; import { SetupModeBadge } from '../../setup_mode/badge'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { AlertsStatus } from '../../../alerts/status'; export class Listing extends PureComponent { getColumns() { const setupMode = this.props.setupMode; + const alerts = this.props.alerts; return [ { @@ -72,6 +74,17 @@ export class Listing extends PureComponent { ); }, }, + { + name: i18n.translate('xpack.monitoring.logstash.nodes.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'isOnline', + width: '175px', + sortable: true, + render: () => { + return ; + }, + }, { name: i18n.translate('xpack.monitoring.logstash.nodes.cpuUsageTitle', { defaultMessage: 'CPU Usage', @@ -141,7 +154,7 @@ export class Listing extends PureComponent { } render() { - const { stats, sorting, pagination, onTableChange, data, setupMode } = this.props; + const { stats, alerts, sorting, pagination, onTableChange, data, setupMode } = this.props; const columns = this.getColumns(); const flattenedData = data.map((item) => ({ ...item, @@ -176,7 +189,7 @@ export class Listing extends PureComponent { - + {setupModeCallOut} diff --git a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js index 5b52f5d85d44dd..21e5c1708a05c2 100644 --- a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js @@ -116,7 +116,7 @@ export class SetupModeRenderer extends React.Component { } getBottomBar(setupModeState) { - if (!setupModeState.enabled) { + if (!setupModeState.enabled || setupModeState.hideBottomBar) { return null; } diff --git a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js index 943e100dc54093..8175806cb192af 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import { isEmpty, capitalize } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; import { StatusIcon } from '../status_icon/index.js'; +import { AlertsStatus } from '../../alerts/status'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import './summary_status.scss'; @@ -86,6 +87,7 @@ const StatusIndicator = ({ status, isOnline, IconComponent }) => { export function SummaryStatus({ metrics, status, + alerts, isOnline, IconComponent = DefaultIconComponent, ...props @@ -94,6 +96,19 @@ export function SummaryStatus({
+ {alerts ? ( + + } + titleSize="xxxs" + textAlign="left" + className="monSummaryStatusNoWrap__stat" + description={i18n.translate('xpack.monitoring.summaryStatus.alertsDescription', { + defaultMessage: 'Alerts', + })} + /> + + ) : null} {metrics.map(wrapChild)}
diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 450a34b797c38e..0f979e5637d686 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from 'kibana/public'; +import { CoreStart, HttpSetup, IUiSettingsClient } from 'kibana/public'; import angular from 'angular'; import { Observable } from 'rxjs'; import { HttpRequestInit } from '../../../../src/core/public'; -import { MonitoringPluginDependencies } from './types'; +import { MonitoringStartPluginDependencies } from './types'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TypeRegistry } from '../../triggers_actions_ui/public/application/type_registry'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionTypeModel, AlertTypeModel } from '../../triggers_actions_ui/public/types'; interface BreadcrumbItem { ['data-test-subj']?: string; @@ -32,7 +37,7 @@ export interface KFetchKibanaOptions { export interface IShims { toastNotifications: CoreStart['notifications']['toasts']; - capabilities: { get: () => CoreStart['application']['capabilities'] }; + capabilities: CoreStart['application']['capabilities']; getAngularInjector: () => angular.auto.IInjectorService; getBasePath: () => string; getInjected: (name: string, defaultValue?: unknown) => unknown; @@ -43,24 +48,29 @@ export interface IShims { I18nContext: CoreStart['i18n']['Context']; docLinks: CoreStart['docLinks']; docTitle: CoreStart['chrome']['docTitle']; - timefilter: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + timefilter: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; + actionTypeRegistry: TypeRegistry; + alertTypeRegistry: TypeRegistry; + uiSettings: IUiSettingsClient; + http: HttpSetup; kfetch: ( { pathname, ...options }: KFetchOptions, kfetchOptions?: KFetchKibanaOptions | undefined ) => Promise; isCloud: boolean; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } export class Legacy { private static _shims: IShims; public static init( - { core, data, isCloud }: MonitoringPluginDependencies, + { core, data, isCloud, triggersActionsUi }: MonitoringStartPluginDependencies, ngInjector: angular.auto.IInjectorService ) { this._shims = { toastNotifications: core.notifications.toasts, - capabilities: { get: () => core.application.capabilities }, + capabilities: core.application.capabilities, getAngularInjector: (): angular.auto.IInjectorService => ngInjector, getBasePath: (): string => core.http.basePath.get(), getInjected: (name: string, defaultValue?: unknown): string | unknown => @@ -95,6 +105,10 @@ export class Legacy { docLinks: core.docLinks, docTitle: core.chrome.docTitle, timefilter: data.query.timefilter.timefilter, + actionTypeRegistry: triggersActionsUi?.actionTypeRegistry, + alertTypeRegistry: triggersActionsUi?.alertTypeRegistry, + uiSettings: core.uiSettings, + http: core.http, kfetch: async ( { pathname, ...options }: KFetchOptions, kfetchOptions?: KFetchKibanaOptions @@ -104,6 +118,7 @@ export class Legacy { ...options, }), isCloud, + triggersActionsUi, }; } diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 2a4caf17515e11..a36b945e82ef78 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -39,11 +39,13 @@ interface ISetupModeState { enabled: boolean; data: any; callback?: (() => void) | null; + hideBottomBar: boolean; } const setupModeState: ISetupModeState = { enabled: false, data: null, callback: null, + hideBottomBar: false, }; export const getSetupModeState = () => setupModeState; @@ -128,6 +130,15 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid } }; +export const hideBottomBar = () => { + setupModeState.hideBottomBar = true; + notifySetupModeDataChange(); +}; +export const showBottomBar = () => { + setupModeState.hideBottomBar = false; + notifySetupModeDataChange(); +}; + export const disableElasticsearchInternalCollection = async () => { checkAngularState(); diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index de8c8d59b78bff..1b9ae75a0968ee 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -19,19 +19,25 @@ import { } from '../../../../src/plugins/home/public'; import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { MonitoringPluginDependencies, MonitoringConfig } from './types'; -import { - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from '../common/constants'; +import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; +import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; +import { createLegacyAlertTypes } from './alerts/legacy_alert'; + +interface MonitoringSetupPluginDependencies { + home?: HomePublicPluginSetup; + cloud?: { isCloudEnabled: boolean }; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} export class MonitoringPlugin - implements Plugin { + implements + Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup( - core: CoreSetup, - plugins: object & { home?: HomePublicPluginSetup; cloud?: { isCloudEnabled: boolean } } + core: CoreSetup, + plugins: MonitoringSetupPluginDependencies ) { const { home } = plugins; const id = 'monitoring'; @@ -59,6 +65,12 @@ export class MonitoringPlugin }); } + plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType()); + const legacyAlertTypes = createLegacyAlertTypes(); + for (const legacyAlertType of legacyAlertTypes) { + plugins.triggers_actions_ui.alertTypeRegistry.register(legacyAlertType); + } + const app: App = { id, title, @@ -68,7 +80,7 @@ export class MonitoringPlugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const { AngularApp } = await import('./angular'); - const deps: MonitoringPluginDependencies = { + const deps: MonitoringStartPluginDependencies = { navigation: pluginsStart.navigation, kibanaLegacy: pluginsStart.kibanaLegacy, element: params.element, @@ -77,11 +89,11 @@ export class MonitoringPlugin isCloud: Boolean(plugins.cloud?.isCloudEnabled), pluginInitializerContext: this.initializerContext, externalConfig: this.getExternalConfig(), + triggersActionsUi: plugins.triggers_actions_ui, }; pluginsStart.kibanaLegacy.loadFontAwesome(); this.setInitialTimefilter(deps); - this.overrideAlertingEmailDefaults(deps); const monitoringApp = new AngularApp(deps); const removeHistoryListener = params.history.listen((location) => { @@ -105,7 +117,7 @@ export class MonitoringPlugin public stop() {} - private setInitialTimefilter({ core: coreContext, data }: MonitoringPluginDependencies) { + private setInitialTimefilter({ core: coreContext, data }: MonitoringStartPluginDependencies) { const { timefilter } = data.query.timefilter; const { uiSettings } = coreContext; const refreshInterval = { value: 10000, pause: false }; @@ -119,25 +131,6 @@ export class MonitoringPlugin uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); } - private overrideAlertingEmailDefaults({ core: coreContext }: MonitoringPluginDependencies) { - const { uiSettings } = coreContext; - if (KIBANA_ALERTING_ENABLED && !uiSettings.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS)) { - uiSettings.overrideLocalDefault( - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - JSON.stringify({ - name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { - defaultMessage: 'Alerting email address', - }), - value: '', - description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { - defaultMessage: `The default email address to receive alerts from Stack Monitoring`, - }), - category: ['monitoring'], - }) - ); - } - } - private getExternalConfig() { const monitoring = this.initializerContext.config.get(); return [ diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index 2862c6f424927e..f3eadcaf9831b7 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -19,6 +19,8 @@ function formatCluster(cluster) { return cluster; } +let once = false; + export function monitoringClustersProvider($injector) { return (clusterUuid, ccs, codePaths) => { const { min, max } = Legacy.shims.timefilter.getBounds(); @@ -30,23 +32,52 @@ export function monitoringClustersProvider($injector) { } const $http = $injector.get('$http'); - return $http - .post(url, { - ccs, - timeRange: { - min: min.toISOString(), - max: max.toISOString(), - }, - codePaths, - }) - .then((response) => response.data) - .then((data) => { - return formatClusters(data); // return set of clusters - }) - .catch((err) => { + + function getClusters() { + return $http + .post(url, { + ccs, + timeRange: { + min: min.toISOString(), + max: max.toISOString(), + }, + codePaths, + }) + .then((response) => response.data) + .then((data) => { + return formatClusters(data); // return set of clusters + }) + .catch((err) => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); + } + + function ensureAlertsEnabled() { + return $http.post('../api/monitoring/v1/alerts/enable', {}).catch((err) => { const Private = $injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); }); + } + + if (!once) { + return getClusters().then((clusters) => { + if (clusters.length) { + return ensureAlertsEnabled() + .then(() => { + once = true; + return clusters; + }) + .catch(() => { + // Intentionally swallow the error as this will retry the next page load + return clusters; + }); + } + return clusters; + }); + } + return getClusters(); }; } diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts index 6266755a041206..f911af2db8c58c 100644 --- a/x-pack/plugins/monitoring/public/types.ts +++ b/x-pack/plugins/monitoring/public/types.ts @@ -7,12 +7,13 @@ import { PluginInitializerContext, CoreStart } from 'kibana/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { MonitoringConfig } from '../server'; -export interface MonitoringPluginDependencies { +export interface MonitoringStartPluginDependencies { navigation: NavigationStart; data: DataPublicPluginStart; kibanaLegacy: KibanaLegacyStart; @@ -21,4 +22,5 @@ export interface MonitoringPluginDependencies { isCloud: boolean; pluginInitializerContext: PluginInitializerContext; externalConfig: Array | Array>; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index f2ae0a93d5df03..e53497d751f9bc 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -6,7 +6,7 @@ import { Subscription } from 'rxjs'; import { History, createHashHistory } from 'history'; -import { MonitoringPluginDependencies } from './types'; +import { MonitoringStartPluginDependencies } from './types'; import { Legacy } from './legacy_shims'; import { @@ -64,13 +64,13 @@ export class GlobalState { private readonly stateStorage: IKbnUrlStateStorage; private readonly stateContainerChangeSub: Subscription; private readonly syncQueryStateWithUrlManager: { stop: () => void }; - private readonly timefilterRef: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + private readonly timefilterRef: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; private lastAssignedState: MonitoringAppState = {}; private lastKnownGlobalState?: string; constructor( - queryService: MonitoringPluginDependencies['data']['query'], + queryService: MonitoringStartPluginDependencies['data']['query'], rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService, externalState: RawObject diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.html b/x-pack/plugins/monitoring/public/views/alerts/index.html deleted file mode 100644 index 4a764634d86fa3..00000000000000 --- a/x-pack/plugins/monitoring/public/views/alerts/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js deleted file mode 100644 index ea857cb69d22b4..00000000000000 --- a/x-pack/plugins/monitoring/public/views/alerts/index.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { render } from 'react-dom'; -import { find, get } from 'lodash'; -import { uiRoutes } from '../../angular/helpers/routes'; -import template from './index.html'; -import { routeInitProvider } from '../../lib/route_init'; -import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; -import { Legacy } from '../../legacy_shims'; -import { Alerts } from '../../components/alerts'; -import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui'; -import { CODE_PATH_ALERTS, KIBANA_ALERTING_ENABLED } from '../../../common/constants'; - -function getPageData($injector) { - const globalState = $injector.get('globalState'); - const $http = $injector.get('$http'); - const Private = $injector.get('Private'); - const url = KIBANA_ALERTING_ENABLED - ? `../api/monitoring/v1/alert_status` - : `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; - - const timeBounds = Legacy.shims.timefilter.getBounds(); - const data = { - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }; - - if (!KIBANA_ALERTING_ENABLED) { - data.ccs = globalState.ccs; - } - - return $http - .post(url, data) - .then((response) => { - const result = get(response, 'data', []); - if (KIBANA_ALERTING_ENABLED) { - return result.alerts; - } - return result; - }) - .catch((err) => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/alerts', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ALERTS] }); - }, - alerts: getPageData, - }, - controllerAs: 'alerts', - controller: class AlertsView extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - - // breadcrumbs + page title - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.alerts.clusterAlertsTitle', { - defaultMessage: 'Cluster Alerts', - }), - getPageData, - $scope, - $injector, - storageKey: 'alertsTable', - reactNodeId: 'monitoringAlertsApp', - }); - - this.data = $route.current.locals.alerts; - - const renderReact = (data) => { - const app = data.message ? ( -

{data.message}

- ) : ( - - ); - - render( - - - - {app} - - - - - - - , - document.getElementById('monitoringAlertsApp') - ); - }; - $scope.$watch( - () => this.data, - (data) => renderReact(data) - ); - } - }, -}); diff --git a/x-pack/plugins/monitoring/public/views/all.js b/x-pack/plugins/monitoring/public/views/all.js index 51dcce751863c2..d192b366fec331 100644 --- a/x-pack/plugins/monitoring/public/views/all.js +++ b/x-pack/plugins/monitoring/public/views/all.js @@ -6,7 +6,6 @@ import './no_data'; import './access_denied'; -import './alerts'; import './license'; import './cluster/listing'; import './cluster/overview'; diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index e189491a3be03f..2f88245d88c4a5 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -85,6 +85,7 @@ export class MonitoringViewBaseController { $scope, $injector, options = {}, + alerts = { shouldFetch: false, options: {} }, fetchDataImmediately = true, }) { const titleService = $injector.get('title'); @@ -112,6 +113,34 @@ export class MonitoringViewBaseController { const { enableTimeFilter = true, enableAutoRefresh = true } = options; + async function fetchAlerts() { + const globalState = $injector.get('globalState'); + const bounds = Legacy.shims.timefilter.getBounds(); + const min = bounds.min?.valueOf(); + const max = bounds.max?.valueOf(); + const options = alerts.options || {}; + try { + return await Legacy.shims.http.post( + `/api/monitoring/v1/alert/${globalState.cluster_uuid}/status`, + { + body: JSON.stringify({ + alertTypeIds: options.alertTypeIds, + filters: options.filters, + timeRange: { + min, + max, + }, + }), + } + ); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: 'Error fetching alert status', + text: err.message, + }); + } + } + this.updateData = () => { if (this.updateDataPromise) { // Do not sent another request if one is inflight @@ -122,14 +151,18 @@ export class MonitoringViewBaseController { const _api = apiUrlFn ? apiUrlFn() : api; const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; const setupMode = getSetupModeState(); + if (alerts.shouldFetch) { + promises.push(fetchAlerts()); + } if (setupMode.enabled) { promises.push(updateSetupModeData()); } this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); - return this.updateDataPromise.promise().then(([pageData]) => { + return this.updateDataPromise.promise().then(([pageData, alerts]) => { $scope.$apply(() => { this._isDataInitialized = true; // render will replace loading screen with the react component $scope.pageData = this.data = pageData; // update the view's data with the fetch result + $scope.alerts = this.alerts = alerts; }); }); }; diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js index d47b31cfb5b793..f3e6d5def9b6f9 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -5,7 +5,6 @@ */ import React, { Fragment } from 'react'; import { isEmpty } from 'lodash'; -import { Legacy } from '../../../legacy_shims'; import { i18n } from '@kbn/i18n'; import { uiRoutes } from '../../../angular/helpers/routes'; import { routeInitProvider } from '../../../lib/route_init'; @@ -13,11 +12,7 @@ import template from './index.html'; import { MonitoringViewBaseController } from '../../'; import { Overview } from '../../../components/cluster/overview'; import { SetupModeRenderer } from '../../../components/renderers'; -import { - CODE_PATH_ALL, - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from '../../../../common/constants'; +import { CODE_PATH_ALL } from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -35,7 +30,6 @@ uiRoutes.when('/overview', { const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); - const config = $injector.get('config'); super({ title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -53,6 +47,9 @@ uiRoutes.when('/overview', { reactNodeId: 'monitoringClusterOverviewApp', $scope, $injector, + alerts: { + shouldFetch: true, + }, }); $scope.$watch( @@ -62,11 +59,6 @@ uiRoutes.when('/overview', { return; } - let emailAddress = Legacy.shims.getInjected('monitoringLegacyEmailAddress') || ''; - if (KIBANA_ALERTING_ENABLED) { - emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; - } - this.renderReact( diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index a1ce9bda16cdc1..f6f7a016905296 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -18,7 +18,7 @@ import { Node } from '../../../components/elasticsearch/node/node'; import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; +import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../../../common/constants'; uiRoutes.when('/elasticsearch/nodes/:node', { template, @@ -47,6 +47,17 @@ uiRoutes.when('/elasticsearch/nodes/:node', { reactNodeId: 'monitoringElasticsearchNodeApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_CPU_USAGE], + filters: [ + { + nodeUuid: nodeName, + }, + ], + }, + }, }); this.nodeName = nodeName; @@ -79,6 +90,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', { this.renderReact( diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js index 802c0e3d30d5b5..a7cb6c8094f74a 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -26,7 +26,8 @@ import { import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; +import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { const $http = $injector.get('$http'); @@ -70,6 +71,12 @@ uiRoutes.when('/kibana/instances/:uuid', { reactNodeId: 'monitoringKibanaInstanceApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + }, + }, }); $scope.$watch( @@ -88,6 +95,7 @@ uiRoutes.when('/kibana/instances/:uuid', {
+ diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js index 8556103e47c301..7106da0fdabd3f 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -12,7 +12,11 @@ import { getPageData } from './get_page_data'; import template from './index.html'; import { KibanaInstances } from '../../../components/kibana/instances'; import { SetupModeRenderer } from '../../../components/renderers'; -import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA } from '../../../../common/constants'; +import { + KIBANA_SYSTEM_ID, + CODE_PATH_KIBANA, + ALERT_KIBANA_VERSION_MISMATCH, +} from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { template, @@ -33,6 +37,12 @@ uiRoutes.when('/kibana/instances', { reactNodeId: 'monitoringKibanaInstancesApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + }, + }, }); const renderReact = () => { @@ -46,6 +56,7 @@ uiRoutes.when('/kibana/instances', { {flyoutComponent}
+ {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js index f78a426b9b7c39..563d04af55bb23 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -11,7 +11,11 @@ import { getPageData } from './get_page_data'; import template from './index.html'; import { Listing } from '../../../components/logstash/listing'; import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { + CODE_PATH_LOGSTASH, + LOGSTASH_SYSTEM_ID, + ALERT_LOGSTASH_VERSION_MISMATCH, +} from '../../../../common/constants'; uiRoutes.when('/logstash/nodes', { template, @@ -32,6 +36,12 @@ uiRoutes.when('/logstash/nodes', { reactNodeId: 'monitoringLogstashNodesApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + }, + }, }); $scope.$watch( @@ -49,6 +59,7 @@ uiRoutes.when('/logstash/nodes', { data={data.nodes} setupMode={setupMode} stats={data.clusterStatus} + alerts={this.alerts} sorting={this.sorting} pagination={this.pagination} onTableChange={this.onTableChange} diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts new file mode 100644 index 00000000000000..d8fa703c7f7858 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsFactory } from './alerts_factory'; +import { ALERT_CPU_USAGE } from '../../common/constants'; + +describe('AlertsFactory', () => { + const alertsClient = { + find: jest.fn(), + }; + + afterEach(() => { + alertsClient.find.mockReset(); + }); + + it('should get by type', async () => { + const id = '1abc'; + alertsClient.find = jest.fn().mockImplementation(() => { + return { + total: 1, + data: [ + { + id, + }, + ], + }; + }); + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + expect(alert).not.toBeNull(); + expect(alert?.type).toBe(ALERT_CPU_USAGE); + }); + + it('should handle no alert found', async () => { + alertsClient.find = jest.fn().mockImplementation(() => { + return { + total: 0, + }; + }); + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + expect(alert).not.toBeNull(); + expect(alert?.type).toBe(ALERT_CPU_USAGE); + }); + + it('should pass in the correct filters', async () => { + let filter = null; + alertsClient.find = jest.fn().mockImplementation(({ options }) => { + filter = options.filter; + return { + total: 0, + }; + }); + await AlertsFactory.getByType(ALERT_CPU_USAGE, alertsClient as any); + expect(filter).toBe(`alert.attributes.alertTypeId:${ALERT_CPU_USAGE}`); + }); + + it('should handle no alerts client', async () => { + const alert = await AlertsFactory.getByType(ALERT_CPU_USAGE, undefined); + expect(alert).not.toBeNull(); + expect(alert?.type).toBe(ALERT_CPU_USAGE); + }); + + it('should get all', () => { + const alerts = AlertsFactory.getAll(); + expect(alerts.length).toBe(7); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts new file mode 100644 index 00000000000000..b91eab05cf9127 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CpuUsageAlert, + NodesChangedAlert, + ClusterHealthAlert, + LicenseExpirationAlert, + LogstashVersionMismatchAlert, + KibanaVersionMismatchAlert, + ElasticsearchVersionMismatchAlert, + BaseAlert, +} from './'; +import { + ALERT_CLUSTER_HEALTH, + ALERT_LICENSE_EXPIRATION, + ALERT_CPU_USAGE, + ALERT_NODES_CHANGED, + ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_KIBANA_VERSION_MISMATCH, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, +} from '../../common/constants'; +import { AlertsClient } from '../../../alerts/server'; + +export const BY_TYPE = { + [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, + [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, + [ALERT_CPU_USAGE]: CpuUsageAlert, + [ALERT_NODES_CHANGED]: NodesChangedAlert, + [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, + [ALERT_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchAlert, + [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchAlert, +}; + +export class AlertsFactory { + public static async getByType( + type: string, + alertsClient: AlertsClient | undefined + ): Promise { + const alertCls = BY_TYPE[type]; + if (!alertCls) { + return null; + } + if (alertsClient) { + const alertClientAlerts = await alertsClient.find({ + options: { + filter: `alert.attributes.alertTypeId:${type}`, + }, + }); + + if (alertClientAlerts.total === 0) { + return new alertCls(); + } + + const rawAlert = alertClientAlerts.data[0]; + return new alertCls(rawAlert as BaseAlert['rawAlert']); + } + + return new alertCls(); + } + + public static getAll() { + return Object.values(BY_TYPE).map((alert) => new alert()); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts new file mode 100644 index 00000000000000..8fd31db421a309 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { BaseAlert } from './base_alert'; + +describe('BaseAlert', () => { + describe('serialize', () => { + it('should serialize with a raw alert provided', () => { + const alert = new BaseAlert({} as any); + expect(alert.serialize()).not.toBeNull(); + }); + it('should not serialize without a raw alert provided', () => { + const alert = new BaseAlert(); + expect(alert.serialize()).toBeNull(); + }); + }); + + describe('create', () => { + it('should create an alert if it does not exist', async () => { + const alert = new BaseAlert(); + const alertsClient = { + create: jest.fn(), + find: jest.fn().mockImplementation(() => { + return { + total: 0, + }; + }), + }; + const actionsClient = { + get: jest.fn().mockImplementation(() => { + return { + actionTypeId: 'foo', + }; + }), + }; + const actions = [ + { + id: '1abc', + config: {}, + }, + ]; + + await alert.createIfDoesNotExist(alertsClient as any, actionsClient as any, actions); + expect(alertsClient.create).toHaveBeenCalledWith({ + data: { + actions: [ + { + group: 'default', + id: '1abc', + params: { + message: '{{context.internalShortMessage}}', + }, + }, + ], + alertTypeId: undefined, + consumer: 'monitoring', + enabled: true, + name: undefined, + params: {}, + schedule: { + interval: '1m', + }, + tags: [], + throttle: '1m', + }, + }); + }); + + it('should not create an alert if it exists', async () => { + const alert = new BaseAlert(); + const alertsClient = { + create: jest.fn(), + find: jest.fn().mockImplementation(() => { + return { + total: 1, + data: [], + }; + }), + }; + const actionsClient = { + get: jest.fn().mockImplementation(() => { + return { + actionTypeId: 'foo', + }; + }), + }; + const actions = [ + { + id: '1abc', + config: {}, + }, + ]; + + await alert.createIfDoesNotExist(alertsClient as any, actionsClient as any, actions); + expect(alertsClient.create).not.toHaveBeenCalled(); + }); + }); + + describe('getStates', () => { + it('should get alert states', async () => { + const alertsClient = { + getAlertState: jest.fn().mockImplementation(() => { + return { + alertInstances: { + abc123: { + id: 'foobar', + }, + }, + }; + }), + }; + const id = '456def'; + const filters: any[] = []; + const alert = new BaseAlert(); + const states = await alert.getStates(alertsClient as any, id, filters); + expect(states).toStrictEqual({ + abc123: { + id: 'foobar', + }, + }); + }); + + it('should return nothing if no states are available', async () => { + const alertsClient = { + getAlertState: jest.fn().mockImplementation(() => { + return null; + }), + }; + const id = '456def'; + const filters: any[] = []; + const alert = new BaseAlert(); + const states = await alert.getStates(alertsClient as any, id, filters); + expect(states).toStrictEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts new file mode 100644 index 00000000000000..622ee7dc51af14 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -0,0 +1,339 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + UiSettingsServiceStart, + ILegacyCustomClusterClient, + Logger, + IUiSettingsClient, +} from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { + AlertType, + AlertExecutorOptions, + AlertInstance, + AlertsClient, + AlertServices, +} from '../../../alerts/server'; +import { Alert, RawAlertInstance } from '../../../alerts/common'; +import { ActionsClient } from '../../../actions/server'; +import { + AlertState, + AlertCluster, + AlertMessage, + AlertData, + AlertInstanceState, + AlertEnableAction, +} from './types'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { MonitoringConfig } from '../config'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types'; +import { MonitoringLicenseService } from '../types'; + +export class BaseAlert { + public type!: string; + public label!: string; + public defaultThrottle: string = '1m'; + public defaultInterval: string = '1m'; + public rawAlert: Alert | undefined; + public isLegacy: boolean = false; + + protected getUiSettingsService!: () => Promise; + protected monitoringCluster!: ILegacyCustomClusterClient; + protected getLogger!: (...scopes: string[]) => Logger; + protected config!: MonitoringConfig; + protected kibanaUrl!: string; + protected defaultParams: CommonAlertParams | {} = {}; + public get paramDetails() { + return {}; + } + protected actionVariables: Array<{ name: string; description: string }> = []; + protected alertType!: AlertType; + + constructor(rawAlert: Alert | undefined = undefined) { + if (rawAlert) { + this.rawAlert = rawAlert; + } + } + + public serialize(): CommonBaseAlert | null { + if (!this.rawAlert) { + return null; + } + + return { + type: this.type, + label: this.label, + rawAlert: this.rawAlert, + paramDetails: this.paramDetails, + isLegacy: this.isLegacy, + }; + } + + public initializeAlertType( + getUiSettingsService: () => Promise, + monitoringCluster: ILegacyCustomClusterClient, + getLogger: (...scopes: string[]) => Logger, + config: MonitoringConfig, + kibanaUrl: string + ) { + this.getUiSettingsService = getUiSettingsService; + this.monitoringCluster = monitoringCluster; + this.config = config; + this.kibanaUrl = kibanaUrl; + this.getLogger = getLogger; + } + + public getAlertType(): AlertType { + return { + id: this.type, + name: this.label, + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.monitoring.alerts.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], + defaultActionGroupId: 'default', + executor: (options: AlertExecutorOptions): Promise => this.execute(options), + producer: 'monitoring', + actionVariables: { + context: this.actionVariables, + }, + }; + } + + public isEnabled(licenseService: MonitoringLicenseService) { + if (this.isLegacy) { + const watcherFeature = licenseService.getWatcherFeature(); + if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { + return false; + } + } + return true; + } + + public getId() { + return this.rawAlert ? this.rawAlert.id : null; + } + + public async createIfDoesNotExist( + alertsClient: AlertsClient, + actionsClient: ActionsClient, + actions: AlertEnableAction[] + ): Promise { + const existingAlertData = await alertsClient.find({ + options: { + search: this.type, + }, + }); + + if (existingAlertData.total > 0) { + const existingAlert = existingAlertData.data[0] as Alert; + return existingAlert; + } + + const alertActions = []; + for (const actionData of actions) { + const action = await actionsClient.get({ id: actionData.id }); + if (!action) { + continue; + } + alertActions.push({ + group: 'default', + id: actionData.id, + params: { + // This is just a server log right now, but will get more robut over time + message: this.getDefaultActionMessage(true), + ...actionData.config, + }, + }); + } + + return await alertsClient.create({ + data: { + enabled: true, + tags: [], + params: this.defaultParams, + consumer: 'monitoring', + name: this.label, + alertTypeId: this.type, + throttle: this.defaultThrottle, + schedule: { interval: this.defaultInterval }, + actions: alertActions, + }, + }); + } + + public async getStates( + alertsClient: AlertsClient, + id: string, + filters: CommonAlertFilter[] + ): Promise<{ [instanceId: string]: RawAlertInstance }> { + const states = await alertsClient.getAlertState({ id }); + if (!states || !states.alertInstances) { + return {}; + } + + return Object.keys(states.alertInstances).reduce( + (accum: { [instanceId: string]: RawAlertInstance }, instanceId) => { + if (!states.alertInstances) { + return accum; + } + const alertInstance: RawAlertInstance = states.alertInstances[instanceId]; + if (alertInstance && this.filterAlertInstance(alertInstance, filters)) { + accum[instanceId] = alertInstance; + } + return accum; + }, + {} + ); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + return true; + } + + protected async execute({ services, params, state }: AlertExecutorOptions): Promise { + const logger = this.getLogger(this.type); + logger.debug( + `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const callCluster = this.monitoringCluster + ? this.monitoringCluster.callAsInternalUser + : services.callCluster; + const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const clusters = await fetchClusters(callCluster, esIndexPattern); + const uiSettings = (await this.getUiSettingsService()).asScopedToClient( + services.savedObjectsClient + ); + + const data = await this.fetchData(params, callCluster, clusters, uiSettings, availableCcs); + this.processData(data, clusters, services, logger); + } + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + // Child should implement + throw new Error('Child classes must implement `fetchData`'); + } + + protected processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger + ) { + for (const item of data) { + const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); + if (!cluster) { + logger.warn(`Unable to find cluster for clusterUuid='${item.clusterUuid}'`); + continue; + } + + const instance = services.alertInstanceFactory(`${this.type}:${item.instanceKey}`); + const state = (instance.getState() as unknown) as AlertInstanceState; + const alertInstanceState: AlertInstanceState = { alertStates: state?.alertStates || [] }; + let alertState: AlertState; + const indexInState = this.findIndexInInstanceState(alertInstanceState, cluster); + if (indexInState > -1) { + alertState = state.alertStates[indexInState]; + } else { + alertState = this.getDefaultAlertState(cluster, item); + } + + let shouldExecuteActions = false; + if (item.shouldFire) { + logger.debug(`${this.type} is firing`); + alertState.ui.triggeredMS = +new Date(); + alertState.ui.isFiring = true; + alertState.ui.message = this.getUiMessage(alertState, item); + alertState.ui.severity = item.severity; + alertState.ui.resolvedMS = 0; + shouldExecuteActions = true; + } else if (!item.shouldFire && alertState.ui.isFiring) { + logger.debug(`${this.type} is not firing anymore`); + alertState.ui.isFiring = false; + alertState.ui.resolvedMS = +new Date(); + alertState.ui.message = this.getUiMessage(alertState, item); + shouldExecuteActions = true; + } + + if (indexInState === -1) { + alertInstanceState.alertStates.push(alertState); + } else { + alertInstanceState.alertStates = [ + ...alertInstanceState.alertStates.slice(0, indexInState), + alertState, + ...alertInstanceState.alertStates.slice(indexInState + 1), + ]; + } + + instance.replaceState(alertInstanceState); + if (shouldExecuteActions) { + this.executeActions(instance, alertInstanceState, item, cluster); + } + } + } + + public getDefaultActionMessage(forDefaultServerLog: boolean): string { + return forDefaultServerLog + ? '{{context.internalShortMessage}}' + : '{{context.internalFullMessage}}'; + } + + protected findIndexInInstanceState(stateInstance: AlertInstanceState, cluster: AlertCluster) { + return stateInstance.alertStates.findIndex( + (alertState) => alertState.cluster.clusterUuid === cluster.clusterUuid + ); + } + + protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { + return { + cluster, + ccs: item.ccs, + ui: { + isFiring: false, + message: null, + severity: AlertSeverity.Success, + resolvedMS: 0, + triggeredMS: 0, + lastCheckedMS: 0, + }, + }; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + throw new Error('Child classes must implement `getUiMessage`'); + } + + protected executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + throw new Error('Child classes must implement `executeActions`'); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts new file mode 100644 index 00000000000000..10b75c43ac8798 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ClusterHealthAlert } from './cluster_health_alert'; +import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('ClusterHealthAlert', () => { + it('should have defaults', () => { + const alert = new ClusterHealthAlert(); + expect(alert.type).toBe(ALERT_CLUSTER_HEALTH); + expect(alert.label).toBe('Cluster health'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterHealth', description: 'The health of the cluster.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'Elasticsearch cluster status is yellow.', + message: 'Allocate missing replica shards.', + metadata: { + severity: 2000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new ClusterHealthAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: 'Elasticsearch cluster health is yellow.', + nextSteps: [ + { + text: 'Allocate missing replica shards. #start_linkView now#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/indices', + }, + ], + }, + ], + }, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[Allocate missing replica shards.](http://localhost:5601/app/monitoring#elasticsearch/indices?_g=(cluster_uuid:abc123))', + actionPlain: 'Allocate missing replica shards.', + internalFullMessage: + 'Cluster health alert is firing for testCluster. Current health is yellow. [Allocate missing replica shards.](http://localhost:5601/app/monitoring#elasticsearch/indices?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.', + clusterName, + clusterHealth: 'yellow', + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new ClusterHealthAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new ClusterHealthAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'Elasticsearch cluster health is green.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Cluster health alert is resolved for testCluster.', + internalShortMessage: 'Cluster health alert is resolved for testCluster.', + clusterName, + clusterHealth: 'yellow', + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts new file mode 100644 index 00000000000000..bb6c4715914170 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertMessageLinkToken, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; +import { CommonAlertParams } from '../../common/types'; + +const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { + defaultMessage: 'Allocate missing primary and replica shards', +}); + +const YELLOW_STATUS_MESSAGE = i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.yellowMessage', + { + defaultMessage: 'Allocate missing replica shards', + } +); + +const WATCH_NAME = 'elasticsearch_cluster_status'; + +export class ClusterHealthAlert extends BaseAlert { + public type = ALERT_CLUSTER_HEALTH; + public label = i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { + defaultMessage: 'Cluster health', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate('xpack.monitoring.alerts.clusterHealth.actionVariables.state', { + defaultMessage: 'The current state of the alert.', + }), + }, + { + name: 'clusterHealth', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.clusterHealth', + { + defaultMessage: 'The health of the cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate('xpack.monitoring.alerts.clusterHealth.actionVariables.action', { + defaultMessage: 'The recommended action for this alert.', + }), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity: mapLegacySeverity(legacyAlert.metadata.severity), + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getHealth(legacyAlert: LegacyAlert) { + const prefixStr = 'Elasticsearch cluster status is '; + return legacyAlert.prefix.slice( + legacyAlert.prefix.indexOf(prefixStr) + prefixStr.length, + legacyAlert.prefix.length - 1 + ) as AlertClusterHealthType; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const health = this.getHealth(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.resolvedMessage', { + defaultMessage: `Elasticsearch cluster health is green.`, + }), + }; + } + + return { + text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.firingMessage', { + defaultMessage: `Elasticsearch cluster health is {health}.`, + values: { + health, + }, + }), + nextSteps: [ + { + text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1', { + defaultMessage: `{message}. #start_linkView now#end_link`, + values: { + message: + health === AlertClusterHealthType.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE, + }, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: 'elasticsearch/indices', + } as AlertMessageLinkToken, + ], + }, + ], + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const health = this.getHealth(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.resolved.internalShortMessage', + { + defaultMessage: `Cluster health alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.resolved.internalFullMessage', + { + defaultMessage: `Cluster health alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: i18n.translate('xpack.monitoring.alerts.clusterHealth.resolved', { + defaultMessage: `resolved`, + }), + clusterHealth: health, + clusterName: cluster.clusterName, + }); + } else { + const actionText = + health === AlertClusterHealthType.Red + ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { + defaultMessage: `Allocate missing primary and replica shards.`, + }) + : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { + defaultMessage: `Allocate missing replica shards.`, + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/indices?_g=(${globalState.join( + ',' + )})`; + const action = `[${actionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: i18n.translate('xpack.monitoring.alerts.clusterHealth.firing', { + defaultMessage: `firing`, + }), + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts deleted file mode 100644 index 62620360377122..00000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Logger } from 'src/core/server'; -import { getClusterState } from './cluster_state'; -import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; -import { AlertCommonParams, AlertCommonState, AlertClusterStatePerClusterState } from './types'; -import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { executeActions } from '../lib/alerts/cluster_state.lib'; -import { AlertClusterStateState } from './enums'; -import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; - -jest.mock('../lib/alerts/cluster_state.lib', () => ({ - executeActions: jest.fn(), - getUiMessage: jest.fn(), -})); - -jest.mock('../lib/alerts/get_prepared_alert', () => ({ - getPreparedAlert: jest.fn(() => { - return { - emailAddress: 'foo@foo.com', - }; - }), -})); - -describe('getClusterState', () => { - const services: AlertServicesMock = alertsMock.createAlertServices(); - - const params: AlertCommonParams = { - dateFormat: 'YYYY', - timezone: 'UTC', - }; - - const emailAddress = 'foo@foo.com'; - const clusterUuid = 'kdksdfj434'; - const clusterName = 'monitoring_test'; - const cluster = { clusterUuid, clusterName }; - - async function setupAlert( - previousState: AlertClusterStateState, - newState: AlertClusterStateState - ): Promise { - const logger: Logger = { - warn: jest.fn(), - log: jest.fn(), - debug: jest.fn(), - trace: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - info: jest.fn(), - get: jest.fn(), - }; - const getLogger = (): Logger => logger; - const ccrEnabled = false; - (getPreparedAlert as jest.Mock).mockImplementation(() => ({ - emailAddress, - data: [ - { - state: newState, - clusterUuid, - }, - ], - clusters: [cluster], - })); - - const alert = getClusterState(null as any, null as any, getLogger, ccrEnabled); - const state: AlertCommonState = { - [clusterUuid]: { - state: previousState, - ui: { - isFiring: false, - severity: 0, - message: null, - resolvedMS: 0, - lastCheckedMS: 0, - triggeredMS: 0, - }, - } as AlertClusterStatePerClusterState, - }; - - return (await alert.executor({ services, params, state } as any)) as AlertCommonState; - } - - afterEach(() => { - (executeActions as jest.Mock).mockClear(); - }); - - it('should configure the alert properly', () => { - const alert = getClusterState(null as any, null as any, jest.fn(), false); - expect(alert.id).toBe(ALERT_TYPE_CLUSTER_STATE); - expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); - }); - - it('should alert if green -> yellow', async () => { - const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Yellow); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Yellow, - emailAddress - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Yellow); - expect(clusterResult.ui.isFiring).toBe(true); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should alert if yellow -> green', async () => { - const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Green); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Green, - emailAddress, - true - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Green); - expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0); - }); - - it('should alert if green -> red', async () => { - const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Red); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Red, - emailAddress - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Red); - expect(clusterResult.ui.isFiring).toBe(true); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should alert if red -> green', async () => { - const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Green); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE), - cluster, - AlertClusterStateState.Green, - emailAddress, - true - ); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Green); - expect(clusterResult.ui.resolvedMS).toBeGreaterThan(0); - }); - - it('should not alert if red -> yellow', async () => { - const result = await setupAlert(AlertClusterStateState.Red, AlertClusterStateState.Yellow); - expect(executeActions).not.toHaveBeenCalled(); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Red); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should not alert if yellow -> red', async () => { - const result = await setupAlert(AlertClusterStateState.Yellow, AlertClusterStateState.Red); - expect(executeActions).not.toHaveBeenCalled(); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Yellow); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); - - it('should not alert if green -> green', async () => { - const result = await setupAlert(AlertClusterStateState.Green, AlertClusterStateState.Green); - expect(executeActions).not.toHaveBeenCalled(); - const clusterResult = result[clusterUuid] as AlertClusterStatePerClusterState; - expect(clusterResult.state).toBe(AlertClusterStateState.Green); - expect(clusterResult.ui.resolvedMS).toBe(0); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts deleted file mode 100644 index c357a5878b93a5..00000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import { Logger, ILegacyCustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; -import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; -import { AlertType } from '../../../alerts/server'; -import { executeActions, getUiMessage } from '../lib/alerts/cluster_state.lib'; -import { - AlertCommonExecutorOptions, - AlertCommonState, - AlertClusterStatePerClusterState, - AlertCommonCluster, -} from './types'; -import { AlertClusterStateState } from './enums'; -import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { fetchClusterState } from '../lib/alerts/fetch_cluster_state'; - -export const getClusterState = ( - getUiSettingsService: () => Promise, - monitoringCluster: ILegacyCustomClusterClient, - getLogger: (...scopes: string[]) => Logger, - ccsEnabled: boolean -): AlertType => { - const logger = getLogger(ALERT_TYPE_CLUSTER_STATE); - return { - id: ALERT_TYPE_CLUSTER_STATE, - name: 'Monitoring Alert - Cluster Status', - actionGroups: [ - { - id: 'default', - name: i18n.translate('xpack.monitoring.alerts.clusterState.actionGroups.default', { - defaultMessage: 'Default', - }), - }, - ], - producer: 'monitoring', - defaultActionGroupId: 'default', - async executor({ - services, - params, - state, - }: AlertCommonExecutorOptions): Promise { - logger.debug( - `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` - ); - - const preparedAlert = await getPreparedAlert( - ALERT_TYPE_CLUSTER_STATE, - getUiSettingsService, - monitoringCluster, - logger, - ccsEnabled, - services, - fetchClusterState - ); - - if (!preparedAlert) { - return state; - } - - const { emailAddress, data: states, clusters } = preparedAlert; - - const result: AlertCommonState = { ...state }; - const defaultAlertState: AlertClusterStatePerClusterState = { - state: AlertClusterStateState.Green, - ui: { - isFiring: false, - message: null, - severity: 0, - resolvedMS: 0, - triggeredMS: 0, - lastCheckedMS: 0, - }, - }; - - for (const clusterState of states) { - const alertState: AlertClusterStatePerClusterState = - (state[clusterState.clusterUuid] as AlertClusterStatePerClusterState) || - defaultAlertState; - const cluster = clusters.find( - (c: AlertCommonCluster) => c.clusterUuid === clusterState.clusterUuid - ); - if (!cluster) { - logger.warn(`Unable to find cluster for clusterUuid='${clusterState.clusterUuid}'`); - continue; - } - const isNonGreen = clusterState.state !== AlertClusterStateState.Green; - const severity = clusterState.state === AlertClusterStateState.Red ? 2100 : 1100; - - const ui = alertState.ui; - let triggered = ui.triggeredMS; - let resolved = ui.resolvedMS; - let message = ui.message || {}; - let lastState = alertState.state; - const instance = services.alertInstanceFactory(ALERT_TYPE_CLUSTER_STATE); - - if (isNonGreen) { - if (lastState === AlertClusterStateState.Green) { - logger.debug(`Cluster state changed from green to ${clusterState.state}`); - executeActions(instance, cluster, clusterState.state, emailAddress); - lastState = clusterState.state; - triggered = moment().valueOf(); - } - message = getUiMessage(clusterState.state); - resolved = 0; - } else if (!isNonGreen && lastState !== AlertClusterStateState.Green) { - logger.debug(`Cluster state changed from ${lastState} to green`); - executeActions(instance, cluster, clusterState.state, emailAddress, true); - lastState = clusterState.state; - message = getUiMessage(clusterState.state, true); - resolved = moment().valueOf(); - } - - result[clusterState.clusterUuid] = { - state: lastState, - ui: { - message, - isFiring: isNonGreen, - severity, - resolvedMS: resolved, - triggeredMS: triggered, - lastCheckedMS: moment().valueOf(), - }, - } as AlertClusterStatePerClusterState; - } - - return result; - }, - }; -}; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts new file mode 100644 index 00000000000000..f0d11abab14924 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CpuUsageAlert } from './cpu_usage_alert'; +import { ALERT_CPU_USAGE } from '../../common/constants'; +import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({ + fetchCpuUsageNodeStats: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('CpuUsageAlert', () => { + it('should have defaults', () => { + const alert = new CpuUsageAlert(); + expect(alert.type).toBe(ALERT_CPU_USAGE); + expect(alert.label).toBe('CPU Usage'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.defaultParams).toStrictEqual({ threshold: 90, duration: '5m' }); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'nodes', description: 'The list of nodes reporting high cpu usage.' }, + { name: 'count', description: 'The number of nodes reporting high cpu usage.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'myNodeId'; + const nodeName = 'myNodeName'; + const cpuUsage = 91; + const stat = { + clusterUuid, + nodeId, + nodeName, + cpuUsage, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + cpuUsage, + nodeId, + nodeName, + ui: { + isFiring: true, + message: { + text: + 'Node #start_linkmyNodeName#end_link is reporting cpu usage of 91.00% at #absolute', + nextSteps: [ + { + text: '#start_linkCheck hot threads#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html', + }, + ], + }, + { + text: '#start_linkCheck long running tasks#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html', + }, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes/myNodeId', + }, + ], + }, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + cpuUsage: 1, + }, + ]; + }); + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + ccs: undefined, + cluster: { + clusterUuid, + clusterName, + }, + cpuUsage: 1, + nodeId, + nodeName, + ui: { + isFiring: false, + lastCheckedMS: 0, + message: null, + resolvedMS: 0, + severity: 'danger', + triggeredMS: 0, + }, + }, + ], + }); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + cpuUsage: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + cpuUsage: 91, + nodeId, + nodeName, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + cpuUsage: 1, + nodeId, + nodeName, + ui: { + isFiring: false, + message: { + text: + 'The cpu usage on node myNodeName is now under the threshold, currently reporting at 1.00% as of #resolved', + tokens: [ + { + startToken: '#resolved', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is resolved for ${count} node(s) in cluster: ${clusterName}.`, + internalShortMessage: `CPU usage alert is resolved for ${count} node(s) in cluster: ${clusterName}.`, + clusterName, + count, + nodes: `${nodeName}:1.00`, + state: 'resolved', + }); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid},ccs:${ccs}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts new file mode 100644 index 00000000000000..9171745fba7470 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -0,0 +1,451 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient, Logger } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertCpuUsageState, + AlertCpuUsageNodeStats, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertInstanceState, + AlertMessageDocLinkToken, +} from './types'; +import { AlertInstance, AlertServices } from '../../../alerts/server'; +import { INDEX_PATTERN_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../common/constants'; +import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { RawAlertInstance } from '../../../alerts/common'; +import { parseDuration } from '../../../alerts/common/parse_duration'; +import { + CommonAlertFilter, + CommonAlertCpuUsageFilter, + CommonAlertParams, + CommonAlertParamDetail, +} from '../../common/types'; + +const RESOLVED = i18n.translate('xpack.monitoring.alerts.cpuUsage.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.cpuUsage.firing', { + defaultMessage: 'firing', +}); + +const DEFAULT_THRESHOLD = 90; +const DEFAULT_DURATION = '5m'; + +interface CpuUsageParams { + threshold: number; + duration: string; +} + +export class CpuUsageAlert extends BaseAlert { + public static paramDetails = { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label', { + defaultMessage: `Notify when CPU is over`, + }), + type: AlertParamType.Percentage, + } as CommonAlertParamDetail, + duration: { + label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label', { + defaultMessage: `Look at the average over`, + }), + type: AlertParamType.Duration, + } as CommonAlertParamDetail, + }; + + public type = ALERT_CPU_USAGE; + public label = i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { + defaultMessage: 'CPU Usage', + }); + + protected defaultParams: CpuUsageParams = { + threshold: DEFAULT_THRESHOLD, + duration: DEFAULT_DURATION, + }; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.state', { + defaultMessage: 'The current state of the alert.', + }), + }, + { + name: 'nodes', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.nodes', { + defaultMessage: 'The list of nodes reporting high cpu usage.', + }), + }, + { + name: 'count', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.count', { + defaultMessage: 'The number of nodes reporting high cpu usage.', + }), + }, + { + name: 'clusterName', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.clusterName', { + defaultMessage: 'The cluster to which the nodes belong.', + }), + }, + { + name: 'action', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.action', { + defaultMessage: 'The recommended action for this alert.', + }), + }, + { + name: 'actionPlain', + description: i18n.translate('xpack.monitoring.alerts.cpuUsage.actionVariables.actionPlain', { + defaultMessage: 'The recommended action for this alert, without any markdown.', + }), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const duration = parseDuration(((params as unknown) as CpuUsageParams).duration); + const endMs = +new Date(); + const startMs = endMs - duration; + const stats = await fetchCpuUsageNodeStats( + callCluster, + clusters, + esIndexPattern, + startMs, + endMs, + this.config.ui.max_bucket_size + ); + return stats.map((stat) => { + let cpuUsage = 0; + if (this.config.ui.container.elasticsearch.enabled) { + cpuUsage = + (stat.containerUsage / (stat.containerPeriods * stat.containerQuota * 1000)) * 100; + } else { + cpuUsage = stat.cpuUsage; + } + + return { + instanceKey: `${stat.clusterUuid}:${stat.nodeId}`, + clusterUuid: stat.clusterUuid, + shouldFire: cpuUsage > params.threshold, + severity: AlertSeverity.Danger, + meta: stat, + ccs: stat.ccs, + }; + }); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState; + if (filters && filters.length) { + for (const _filter of filters) { + const filter = _filter as CommonAlertCpuUsageFilter; + if (filter && filter.nodeUuid) { + let nodeExistsInStates = false; + for (const state of alertInstanceState.alertStates) { + if ((state as AlertCpuUsageState).nodeId === filter.nodeUuid) { + nodeExistsInStates = true; + break; + } + } + if (!nodeExistsInStates) { + return false; + } + } + } + } + return true; + } + + protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { + const base = super.getDefaultAlertState(cluster, item); + return { + ...base, + ui: { + ...base.ui, + severity: AlertSeverity.Danger, + }, + }; + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const stat = item.meta as AlertCpuUsageNodeStats; + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.resolvedMessage', { + defaultMessage: `The cpu usage on node {nodeName} is now under the threshold, currently reporting at {cpuUsage}% as of #resolved`, + values: { + nodeName: stat.nodeName, + cpuUsage: stat.cpuUsage.toFixed(2), + }, + }), + tokens: [ + { + startToken: '#resolved', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.resolvedMS, + } as AlertMessageTimeToken, + ], + }; + } + return { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.firingMessage', { + defaultMessage: `Node #start_link{nodeName}#end_link is reporting cpu usage of {cpuUsage}% at #absolute`, + values: { + nodeName: stat.nodeName, + cpuUsage: stat.cpuUsage.toFixed(2), + }, + }), + nextSteps: [ + { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads', { + defaultMessage: `#start_linkCheck hot threads#end_link`, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.DocLink, + partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html`, + } as AlertMessageDocLinkToken, + ], + }, + { + text: i18n.translate('xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks', { + defaultMessage: `#start_linkCheck long running tasks#end_link`, + }), + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.DocLink, + partialUrl: `{elasticWebsiteUrl}/guide/en/elasticsearch/reference/{docLinkVersion}/tasks.html`, + } as AlertMessageDocLinkToken, + ], + }, + ], + tokens: [ + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.triggeredMS, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: `elasticsearch/nodes/${stat.nodeId}`, + } as AlertMessageLinkToken, + ], + }; + } + + protected executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData | null, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + + const nodes = instanceState.alertStates + .map((_state) => { + const state = _state as AlertCpuUsageState; + return `${state.nodeName}:${state.cpuUsage.toFixed(2)}`; + }) + .join(','); + + const ccs = instanceState.alertStates.reduce((accum: string, state): string => { + if (state.ccs) { + return state.ccs; + } + return accum; + }, ''); + + const count = instanceState.alertStates.length; + if (!instanceState.alertStates[0].ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.resolved.internalShortMessage', + { + defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`, + values: { + count, + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.resolved.internalFullMessage', + { + defaultMessage: `CPU usage alert is resolved for {count} node(s) in cluster: {clusterName}.`, + values: { + count, + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + nodes, + count, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.shortAction', { + defaultMessage: 'Verify CPU levels across affected nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.cpuUsage.fullAction', { + defaultMessage: 'View nodes', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (ccs) { + globalState.push(`ccs:${ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count, + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count, + clusterName: cluster.clusterName, + action, + }, + } + ), + state: FIRING, + nodes, + count, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); + } + } + + protected processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger + ) { + for (const cluster of clusters) { + const nodes = data.filter((_item) => _item.clusterUuid === cluster.clusterUuid); + if (nodes.length === 0) { + continue; + } + + const instance = services.alertInstanceFactory(`${this.type}:${cluster.clusterUuid}`); + const state = (instance.getState() as unknown) as AlertInstanceState; + const alertInstanceState: AlertInstanceState = { alertStates: state?.alertStates || [] }; + let shouldExecuteActions = false; + for (const node of nodes) { + const stat = node.meta as AlertCpuUsageNodeStats; + let nodeState: AlertCpuUsageState; + const indexInState = alertInstanceState.alertStates.findIndex((alertState) => { + const nodeAlertState = alertState as AlertCpuUsageState; + return ( + nodeAlertState.cluster.clusterUuid === cluster.clusterUuid && + nodeAlertState.nodeId === (node.meta as AlertCpuUsageNodeStats).nodeId + ); + }); + if (indexInState > -1) { + nodeState = alertInstanceState.alertStates[indexInState] as AlertCpuUsageState; + } else { + nodeState = this.getDefaultAlertState(cluster, node) as AlertCpuUsageState; + } + + nodeState.cpuUsage = stat.cpuUsage; + nodeState.nodeId = stat.nodeId; + nodeState.nodeName = stat.nodeName; + + if (node.shouldFire) { + nodeState.ui.triggeredMS = new Date().valueOf(); + nodeState.ui.isFiring = true; + nodeState.ui.message = this.getUiMessage(nodeState, node); + nodeState.ui.severity = node.severity; + nodeState.ui.resolvedMS = 0; + shouldExecuteActions = true; + } else if (!node.shouldFire && nodeState.ui.isFiring) { + nodeState.ui.isFiring = false; + nodeState.ui.resolvedMS = new Date().valueOf(); + nodeState.ui.message = this.getUiMessage(nodeState, node); + shouldExecuteActions = true; + } + + if (indexInState === -1) { + alertInstanceState.alertStates.push(nodeState); + } else { + alertInstanceState.alertStates = [ + ...alertInstanceState.alertStates.slice(0, indexInState), + nodeState, + ...alertInstanceState.alertStates.slice(indexInState + 1), + ]; + } + } + + instance.replaceState(alertInstanceState); + if (shouldExecuteActions) { + this.executeActions(instance, alertInstanceState, null, cluster); + } + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts new file mode 100644 index 00000000000000..44684939ca261a --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; +import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('ElasticsearchVersionMismatchAlert', () => { + it('should have defaults', () => { + const alert = new ElasticsearchVersionMismatchAlert(); + expect(alert.type).toBe(ALERT_ELASTICSEARCH_VERSION_MISMATCH); + expect(alert.label).toBe('Elasticsearch version mismatch'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { + name: 'versionList', + description: 'The versions of Elasticsearch running in this cluster.', + }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'This cluster is running with multiple versions of Elasticsearch.', + message: 'Versions: [8.0.0, 7.2.1].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new ElasticsearchVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: + 'Multiple versions of Elasticsearch ([8.0.0, 7.2.1]) running in this cluster.', + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify you have the same version across all nodes.', + internalFullMessage: + 'Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running [8.0.0, 7.2.1]. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + versionList: '[8.0.0, 7.2.1]', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new ElasticsearchVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new ElasticsearchVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'All versions of Elasticsearch are the same in this cluster.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Elasticsearch version mismatch alert is resolved for testCluster.', + internalShortMessage: 'Elasticsearch version mismatch alert is resolved for testCluster.', + clusterName, + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts new file mode 100644 index 00000000000000..e3b952fbbe5d35 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; + +const WATCH_NAME = 'elasticsearch_version_mismatch'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.firing', { + defaultMessage: 'firing', +}); + +export class ElasticsearchVersionMismatchAlert extends BaseAlert { + public type = ALERT_ELASTICSEARCH_VERSION_MISMATCH; + public label = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { + defaultMessage: 'Elasticsearch version mismatch', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'versionList', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterHealth', + { + defaultMessage: 'The versions of Elasticsearch running in this cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + const severity = AlertSeverity.Warning; + + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity, + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getVersions(legacyAlert: LegacyAlert) { + const prefixStr = 'Versions: '; + return legacyAlert.message.slice( + legacyAlert.message.indexOf(prefixStr) + prefixStr.length, + legacyAlert.message.length - 1 + ); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.resolvedMessage', + { + defaultMessage: `All versions of Elasticsearch are the same in this cluster.`, + } + ), + }; + } + + const text = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage', + { + defaultMessage: `Multiple versions of Elasticsearch ({versions}) running in this cluster.`, + values: { + versions, + }, + } + ); + + return { + text, + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalShortMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalFullMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts new file mode 100644 index 00000000000000..048e703d2222cd --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { BaseAlert } from './base_alert'; +export { CpuUsageAlert } from './cpu_usage_alert'; +export { ClusterHealthAlert } from './cluster_health_alert'; +export { LicenseExpirationAlert } from './license_expiration_alert'; +export { NodesChangedAlert } from './nodes_changed_alert'; +export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; +export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; +export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; +export { AlertsFactory, BY_TYPE } from './alerts_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts new file mode 100644 index 00000000000000..6c56c7aa08d712 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; +import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('KibanaVersionMismatchAlert', () => { + it('should have defaults', () => { + const alert = new KibanaVersionMismatchAlert(); + expect(alert.type).toBe(ALERT_KIBANA_VERSION_MISMATCH); + expect(alert.label).toBe('Kibana version mismatch'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { + name: 'versionList', + description: 'The versions of Kibana running in this cluster.', + }, + { + name: 'clusterName', + description: 'The cluster to which the instances belong.', + }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'This cluster is running with multiple versions of Kibana.', + message: 'Versions: [8.0.0, 7.2.1].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new KibanaVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Kibana ([8.0.0, 7.2.1]) running in this cluster.', + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View instances](http://localhost:5601/app/monitoring#kibana/instances?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify you have the same version across all instances.', + internalFullMessage: + 'Kibana version mismatch alert is firing for testCluster. Kibana is running [8.0.0, 7.2.1]. [View instances](http://localhost:5601/app/monitoring#kibana/instances?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', + versionList: '[8.0.0, 7.2.1]', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new KibanaVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new KibanaVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'All versions of Kibana are the same in this cluster.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Kibana version mismatch alert is resolved for testCluster.', + internalShortMessage: 'Kibana version mismatch alert is resolved for testCluster.', + clusterName, + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts new file mode 100644 index 00000000000000..80e8701933f562 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; + +const WATCH_NAME = 'kibana_version_mismatch'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.firing', { + defaultMessage: 'firing', +}); + +export class KibanaVersionMismatchAlert extends BaseAlert { + public type = ALERT_KIBANA_VERSION_MISMATCH; + public label = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { + defaultMessage: 'Kibana version mismatch', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'versionList', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.clusterHealth', + { + defaultMessage: 'The versions of Kibana running in this cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the instances belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + const severity = AlertSeverity.Warning; + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity, + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getVersions(legacyAlert: LegacyAlert) { + const prefixStr = 'Versions: '; + return legacyAlert.message.slice( + legacyAlert.message.indexOf(prefixStr) + prefixStr.length, + legacyAlert.message.length - 1 + ); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.resolvedMessage', { + defaultMessage: `All versions of Kibana are the same in this cluster.`, + }), + }; + } + + const text = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage', { + defaultMessage: `Multiple versions of Kibana ({versions}) running in this cluster.`, + values: { + versions, + }, + }); + + return { + text, + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalShortMessage', + { + defaultMessage: `Kibana version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all instances.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + { + defaultMessage: 'View instances', + } + ); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#kibana/instances?_g=(${globalState.join(',')})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts deleted file mode 100644 index fb8d10884fdc7b..00000000000000 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import { getLicenseExpiration } from './license_expiration'; -import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; -import { Logger } from 'src/core/server'; -import { - AlertCommonParams, - AlertCommonState, - AlertLicensePerClusterState, - AlertLicense, -} from './types'; -import { executeActions } from '../lib/alerts/license_expiration.lib'; -import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; - -jest.mock('../lib/alerts/license_expiration.lib', () => ({ - executeActions: jest.fn(), - getUiMessage: jest.fn(), -})); - -jest.mock('../lib/alerts/get_prepared_alert', () => ({ - getPreparedAlert: jest.fn(() => { - return { - emailAddress: 'foo@foo.com', - }; - }), -})); - -describe('getLicenseExpiration', () => { - const services: AlertServicesMock = alertsMock.createAlertServices(); - - const params: AlertCommonParams = { - dateFormat: 'YYYY', - timezone: 'UTC', - }; - - const emailAddress = 'foo@foo.com'; - const clusterUuid = 'kdksdfj434'; - const clusterName = 'monitoring_test'; - const dateFormat = 'YYYY-MM-DD'; - const cluster = { clusterUuid, clusterName }; - const defaultUiState = { - isFiring: false, - severity: 0, - message: null, - resolvedMS: 0, - lastCheckedMS: 0, - triggeredMS: 0, - }; - - async function setupAlert( - license: AlertLicense | null, - expiredCheckDateMS: number, - preparedAlertResponse: PreparedAlert | null | undefined = undefined - ): Promise { - const logger: Logger = { - warn: jest.fn(), - log: jest.fn(), - debug: jest.fn(), - trace: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - info: jest.fn(), - get: jest.fn(), - }; - const getLogger = (): Logger => logger; - const ccrEnabled = false; - (getPreparedAlert as jest.Mock).mockImplementation(() => { - if (preparedAlertResponse !== undefined) { - return preparedAlertResponse; - } - - return { - emailAddress, - data: [license], - clusters: [cluster], - dateFormat, - }; - }); - - const alert = getLicenseExpiration(null as any, null as any, getLogger, ccrEnabled); - const state: AlertCommonState = { - [clusterUuid]: { - expiredCheckDateMS, - ui: { ...defaultUiState }, - } as AlertLicensePerClusterState, - }; - - return (await alert.executor({ services, params, state } as any)) as AlertCommonState; - } - - afterEach(() => { - jest.clearAllMocks(); - (executeActions as jest.Mock).mockClear(); - (getPreparedAlert as jest.Mock).mockClear(); - }); - - it('should have the right id and actionGroups', () => { - const alert = getLicenseExpiration(null as any, null as any, jest.fn(), false); - expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); - expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); - }); - - it('should return the state if no license is provided', async () => { - const result = await setupAlert(null, 0, null); - expect(result[clusterUuid].ui).toEqual(defaultUiState); - }); - - it('should fire actions if going to expire', async () => { - const expiryDateMS = moment().add(7, 'days').valueOf(); - const license = { - status: 'active', - type: 'gold', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS > 0).toBe(true); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress - ); - }); - - it('should fire actions if the user fixed their license', async () => { - const expiryDateMS = moment().add(365, 'days').valueOf(); - const license = { - status: 'active', - type: 'gold', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 100); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS).toBe(0); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress, - true - ); - }); - - it('should not fire actions for trial license that expire in more than 14 days', async () => { - const expiryDateMS = moment().add(20, 'days').valueOf(); - const license = { - status: 'active', - type: 'trial', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS).toBe(0); - expect(executeActions).not.toHaveBeenCalled(); - }); - - it('should fire actions for trial license that in 14 days or less', async () => { - const expiryDateMS = moment().add(7, 'days').valueOf(); - const license = { - status: 'active', - type: 'trial', - expiryDateMS, - clusterUuid, - }; - const result = await setupAlert(license, 0); - const newState = result[clusterUuid] as AlertLicensePerClusterState; - expect(newState.expiredCheckDateMS > 0).toBe(true); - expect(executeActions).toHaveBeenCalledWith( - services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION), - cluster, - moment.utc(expiryDateMS), - dateFormat, - emailAddress - ); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts deleted file mode 100644 index 277e108e8f0c0a..00000000000000 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import { Logger, ILegacyCustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; -import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; -import { AlertType } from '../../../alerts/server'; -import { fetchLicenses } from '../lib/alerts/fetch_licenses'; -import { - AlertCommonState, - AlertLicensePerClusterState, - AlertCommonExecutorOptions, - AlertCommonCluster, - AlertLicensePerClusterUiState, -} from './types'; -import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; -import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; - -const EXPIRES_DAYS = [60, 30, 14, 7]; - -export const getLicenseExpiration = ( - getUiSettingsService: () => Promise, - monitoringCluster: ILegacyCustomClusterClient, - getLogger: (...scopes: string[]) => Logger, - ccsEnabled: boolean -): AlertType => { - const logger = getLogger(ALERT_TYPE_LICENSE_EXPIRATION); - return { - id: ALERT_TYPE_LICENSE_EXPIRATION, - name: 'Monitoring Alert - License Expiration', - actionGroups: [ - { - id: 'default', - name: i18n.translate('xpack.monitoring.alerts.licenseExpiration.actionGroups.default', { - defaultMessage: 'Default', - }), - }, - ], - defaultActionGroupId: 'default', - producer: 'monitoring', - async executor({ services, params, state }: AlertCommonExecutorOptions): Promise { - logger.debug( - `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` - ); - - const preparedAlert = await getPreparedAlert( - ALERT_TYPE_LICENSE_EXPIRATION, - getUiSettingsService, - monitoringCluster, - logger, - ccsEnabled, - services, - fetchLicenses - ); - - if (!preparedAlert) { - return state; - } - - const { emailAddress, data: licenses, clusters, dateFormat } = preparedAlert; - - const result: AlertCommonState = { ...state }; - const defaultAlertState: AlertLicensePerClusterState = { - expiredCheckDateMS: 0, - ui: { - isFiring: false, - message: null, - severity: 0, - resolvedMS: 0, - lastCheckedMS: 0, - triggeredMS: 0, - }, - }; - - for (const license of licenses) { - const alertState: AlertLicensePerClusterState = - (state[license.clusterUuid] as AlertLicensePerClusterState) || defaultAlertState; - const cluster = clusters.find( - (c: AlertCommonCluster) => c.clusterUuid === license.clusterUuid - ); - if (!cluster) { - logger.warn(`Unable to find cluster for clusterUuid='${license.clusterUuid}'`); - continue; - } - const $expiry = moment.utc(license.expiryDateMS); - let isExpired = false; - let severity = 0; - - if (license.status !== 'active') { - isExpired = true; - severity = 2001; - } else if (license.expiryDateMS) { - for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { - if (license.type === 'trial' && i < 2) { - break; - } - - const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); - if ($fromNow.isAfter($expiry)) { - isExpired = true; - severity = 1000 * i; - break; - } - } - } - - const ui = alertState.ui; - let triggered = ui.triggeredMS; - let resolved = ui.resolvedMS; - let message = ui.message; - let expiredCheckDate = alertState.expiredCheckDateMS; - const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); - - if (isExpired) { - if (!alertState.expiredCheckDateMS) { - logger.debug(`License will expire soon, sending email`); - executeActions(instance, cluster, $expiry, dateFormat, emailAddress); - expiredCheckDate = triggered = moment().valueOf(); - } - message = getUiMessage(); - resolved = 0; - } else if (!isExpired && alertState.expiredCheckDateMS) { - logger.debug(`License expiration has been resolved, sending email`); - executeActions(instance, cluster, $expiry, dateFormat, emailAddress, true); - expiredCheckDate = 0; - message = getUiMessage(true); - resolved = moment().valueOf(); - } - - result[license.clusterUuid] = { - expiredCheckDateMS: expiredCheckDate, - ui: { - message, - expirationTime: license.expiryDateMS, - isFiring: expiredCheckDate > 0, - severity, - resolvedMS: resolved, - triggeredMS: triggered, - lastCheckedMS: moment().valueOf(), - } as AlertLicensePerClusterUiState, - } as AlertLicensePerClusterState; - } - - return result; - }, - }; -}; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts new file mode 100644 index 00000000000000..09173df1d88b1e --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseExpirationAlert } from './license_expiration_alert'; +import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); +jest.mock('moment', () => { + return function () { + return { + format: () => 'THE_DATE', + }; + }; +}); + +describe('LicenseExpirationAlert', () => { + it('should have defaults', () => { + const alert = new LicenseExpirationAlert(); + expect(alert.type).toBe(ALERT_LICENSE_EXPIRATION); + expect(alert.label).toBe('License expiration'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'expiredDate', description: 'The date when the license expires.' }, + + { name: 'clusterName', description: 'The cluster to which the license belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: + 'The license for this cluster expires in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', + message: 'Update your license.', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + time: 1, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new LicenseExpirationAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: true, + message: { + text: + 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link', + tokens: [ + { + startToken: '#relative', + type: 'time', + isRelative: true, + isAbsolute: false, + timestamp: 1, + }, + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'license', + }, + ], + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[Please update your license.](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Please update your license.', + internalFullMessage: + 'License expiration alert is firing for testCluster. Your license expires in THE_DATE. [Please update your license.](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'License expiration alert is firing for testCluster. Your license expires in THE_DATE. Please update your license.', + clusterName, + expiredDate: 'THE_DATE', + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new LicenseExpirationAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new LicenseExpirationAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'The license for this cluster is active.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'License expiration alert is resolved for testCluster.', + internalShortMessage: 'License expiration alert is resolved for testCluster.', + clusterName, + expiredDate: 'THE_DATE', + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts new file mode 100644 index 00000000000000..7a249db28d2db0 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -0,0 +1,262 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { + INDEX_ALERTS, + ALERT_LICENSE_EXPIRATION, + FORMAT_DURATION_TEMPLATE_SHORT, +} from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; + +const RESOLVED = i18n.translate('xpack.monitoring.alerts.licenseExpiration.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.licenseExpiration.firing', { + defaultMessage: 'firing', +}); + +const WATCH_NAME = 'xpack_license_expiration'; + +export class LicenseExpirationAlert extends BaseAlert { + public type = ALERT_LICENSE_EXPIRATION; + public label = i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { + defaultMessage: 'License expiration', + }); + public isLegacy = true; + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'expiredDate', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate', + { + defaultMessage: 'The date when the license expires.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the license belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity: mapLegacySeverity(legacyAlert.metadata.severity), + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `The license for this cluster is active.`, + }), + }; + } + return { + text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link`, + }), + tokens: [ + { + startToken: '#relative', + type: AlertMessageTokenType.Time, + isRelative: true, + isAbsolute: false, + timestamp: legacyAlert.metadata.time, + } as AlertMessageTimeToken, + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: legacyAlert.metadata.time, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: 'license', + } as AlertMessageLinkToken, + ], + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const $expiry = moment(legacyAlert.metadata.time); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolved.internalShortMessage', + { + defaultMessage: `License expiration alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolved.internalFullMessage', + { + defaultMessage: `License expiration alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + expiredDate: $expiry.format(FORMAT_DURATION_TEMPLATE_SHORT).trim(), + clusterName: cluster.clusterName, + }); + } else { + const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { + defaultMessage: 'Please update your license.', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${actionText}](${url})`; + const expiredDate = $expiry.format(FORMAT_DURATION_TEMPLATE_SHORT).trim(); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: FIRING, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts new file mode 100644 index 00000000000000..3f6d38809a9492 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; +import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +describe('LogstashVersionMismatchAlert', () => { + it('should have defaults', () => { + const alert = new LogstashVersionMismatchAlert(); + expect(alert.type).toBe(ALERT_LOGSTASH_VERSION_MISMATCH); + expect(alert.label).toBe('Logstash version mismatch'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { + name: 'versionList', + description: 'The versions of Logstash running in this cluster.', + }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'This cluster is running with multiple versions of Logstash.', + message: 'Versions: [8.0.0, 7.2.1].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new LogstashVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, + ccs: null, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Logstash ([8.0.0, 7.2.1]) running in this cluster.', + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View nodes](http://localhost:5601/app/monitoring#logstash/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify you have the same version across all nodes.', + internalFullMessage: + 'Logstash version mismatch alert is firing for testCluster. Logstash is running [8.0.0, 7.2.1]. [View nodes](http://localhost:5601/app/monitoring#logstash/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + versionList: '[8.0.0, 7.2.1]', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new LogstashVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should resolve with a resolved message', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [ + { + ...legacyAlert, + resolved_timestamp: 1, + }, + ]; + }); + (getState as jest.Mock).mockImplementation(() => { + return { + alertStates: [ + { + cluster: { + clusterUuid, + clusterName, + }, + ccs: null, + ui: { + isFiring: true, + message: null, + severity: 'danger', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }; + }); + const alert = new LogstashVersionMismatchAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: false, + message: { + text: 'All versions of Logstash are the same in this cluster.', + }, + severity: 'danger', + resolvedMS: 1, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: 'Logstash version mismatch alert is resolved for testCluster.', + internalShortMessage: 'Logstash version mismatch alert is resolved for testCluster.', + clusterName, + state: 'resolved', + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts new file mode 100644 index 00000000000000..f996e54de28ef7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertSeverity } from '../../common/enums'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; + +const WATCH_NAME = 'logstash_version_mismatch'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.firing', { + defaultMessage: 'firing', +}); + +export class LogstashVersionMismatchAlert extends BaseAlert { + public type = ALERT_LOGSTASH_VERSION_MISMATCH; + public label = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { + defaultMessage: 'Logstash version mismatch', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.state', + { + defaultMessage: 'The current state of the alert.', + } + ), + }, + { + name: 'versionList', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth', + { + defaultMessage: 'The versions of Logstash running in this cluster.', + } + ), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'action', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.action', + { + defaultMessage: 'The recommended action for this alert.', + } + ), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + const severity = AlertSeverity.Warning; + + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: !legacyAlert.resolved_timestamp, + severity, + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + private getVersions(legacyAlert: LegacyAlert) { + const prefixStr = 'Versions: '; + return legacyAlert.message.slice( + legacyAlert.message.indexOf(prefixStr) + prefixStr.length, + legacyAlert.message.length - 1 + ); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.ui.resolvedMessage', { + defaultMessage: `All versions of Logstash are the same in this cluster.`, + }), + }; + } + + const text = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage', + { + defaultMessage: `Multiple versions of Logstash ({versions}) running in this cluster.`, + values: { + versions, + }, + } + ); + + return { + text, + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + const versions = this.getVersions(legacyAlert); + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalShortMessage', + { + defaultMessage: `Logstash version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalFullMessage', + { + defaultMessage: `Logstash version mismatch alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#logstash/nodes?_g=(${globalState.join(',')})`; + const action = `[${fullActionText}](${url})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts new file mode 100644 index 00000000000000..13c3dbbbe6e8ae --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { NodesChangedAlert } from './nodes_changed_alert'; +import { ALERT_NODES_CHANGED } from '../../common/constants'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ + fetchLegacyAlerts: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); +jest.mock('moment', () => { + return function () { + return { + format: () => 'THE_DATE', + }; + }; +}); + +describe('NodesChangedAlert', () => { + it('should have defaults', () => { + const alert = new NodesChangedAlert(); + expect(alert.type).toBe(ALERT_NODES_CHANGED); + expect(alert.label).toBe('Nodes changed'); + expect(alert.defaultThrottle).toBe('1m'); + // @ts-ignore + expect(alert.actionVariables).toStrictEqual([ + { + name: 'internalShortMessage', + description: 'The short internal message generated by Elastic.', + }, + { + name: 'internalFullMessage', + description: 'The full internal message generated by Elastic.', + }, + { name: 'state', description: 'The current state of the alert.' }, + { name: 'clusterName', description: 'The cluster to which the nodes belong.' }, + { name: 'added', description: 'The list of nodes added to the cluster.' }, + { name: 'removed', description: 'The list of nodes removed from the cluster.' }, + { name: 'restarted', description: 'The list of nodes restarted in the cluster.' }, + { name: 'action', description: 'The recommended action for this alert.' }, + { + name: 'actionPlain', + description: 'The recommended action for this alert, without any markdown.', + }, + ]); + }); + + describe('execute', () => { + function FakeDate() {} + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const legacyAlert = { + prefix: 'Elasticsearch cluster nodes have changed!', + message: 'Node was restarted [1]: [test].', + metadata: { + severity: 1000, + cluster_uuid: clusterUuid, + }, + nodes: { + added: {}, + removed: {}, + restarted: { + test: 'test', + }, + }, + }; + const getUiSettingsService = () => ({ + asScopedToClient: jest.fn(), + }); + const getLogger = () => ({ + debug: jest.fn(), + }); + const monitoringCluster = null; + const config = { + ui: { ccs: { enabled: true }, container: { elasticsearch: { enabled: false } } }, + }; + const kibanaUrl = 'http://localhost:5601'; + + const replaceState = jest.fn(); + const scheduleActions = jest.fn(); + const getState = jest.fn(); + const executorOptions = { + services: { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return [legacyAlert]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + replaceState.mockReset(); + scheduleActions.mockReset(); + getState.mockReset(); + }); + + it('should fire actions', async () => { + const alert = new NodesChangedAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).toHaveBeenCalledWith({ + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: null, + ui: { + isFiring: true, + message: { + text: "Elasticsearch nodes 'test' restarted in this cluster.", + }, + severity: 'warning', + resolvedMS: 0, + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }); + expect(scheduleActions).toHaveBeenCalledWith('default', { + action: + '[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + actionPlain: 'Verify that you added, removed, or restarted nodes.', + internalFullMessage: + 'Nodes changed alert is firing for testCluster. The following Elasticsearch nodes have been added: removed: restarted:test. [View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:abc123))', + internalShortMessage: + 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', + added: '', + removed: '', + restarted: 'test', + clusterName, + state: 'firing', + }); + }); + + it('should not fire actions if there is no legacy alert', async () => { + (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + return []; + }); + const alert = new NodesChangedAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + expect(replaceState).not.toHaveBeenCalledWith({}); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + // This doesn't work because this watch is weird where it sets the resolved timestamp right away + // It is not really worth fixing as this watch will go away in 8.0 + // it('should resolve with a resolved message', async () => { + // (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { + // return []; + // }); + // (getState as jest.Mock).mockImplementation(() => { + // return { + // alertStates: [ + // { + // cluster: { + // clusterUuid, + // clusterName, + // }, + // ccs: null, + // ui: { + // isFiring: true, + // message: null, + // severity: 'danger', + // resolvedMS: 0, + // triggeredMS: 1, + // lastCheckedMS: 0, + // }, + // }, + // ], + // }; + // }); + // const alert = new NodesChangedAlert(); + // alert.initializeAlertType( + // getUiSettingsService as any, + // monitoringCluster as any, + // getLogger as any, + // config as any, + // kibanaUrl + // ); + // const type = alert.getAlertType(); + // await type.executor({ + // ...executorOptions, + // // @ts-ignore + // params: alert.defaultParams, + // } as any); + // expect(replaceState).toHaveBeenCalledWith({ + // alertStates: [ + // { + // cluster: { clusterUuid, clusterName }, + // ccs: null, + // ui: { + // isFiring: false, + // message: { + // text: "The license for this cluster is active.", + // }, + // severity: 'danger', + // resolvedMS: 1, + // triggeredMS: 1, + // lastCheckedMS: 0, + // }, + // }, + // ], + // }); + // expect(scheduleActions).toHaveBeenCalledWith('default', { + // clusterName, + // expiredDate: 'THE_DATE', + // state: 'resolved', + // }); + // }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts new file mode 100644 index 00000000000000..5b38503c7ece45 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + AlertInstanceState, + LegacyAlert, + LegacyAlertNodesChangedList, +} from './types'; +import { AlertInstance } from '../../../alerts/server'; +import { INDEX_ALERTS, ALERT_NODES_CHANGED } from '../../common/constants'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { CommonAlertParams } from '../../common/types'; +import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; + +const WATCH_NAME = 'elasticsearch_nodes'; +const RESOLVED = i18n.translate('xpack.monitoring.alerts.nodesChanged.resolved', { + defaultMessage: 'resolved', +}); +const FIRING = i18n.translate('xpack.monitoring.alerts.nodesChanged.firing', { + defaultMessage: 'firing', +}); + +export class NodesChangedAlert extends BaseAlert { + public type = ALERT_NODES_CHANGED; + public label = i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { + defaultMessage: 'Nodes changed', + }); + public isLegacy = true; + + protected actionVariables = [ + { + name: 'internalShortMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.internalShortMessage', + { + defaultMessage: 'The short internal message generated by Elastic.', + } + ), + }, + { + name: 'internalFullMessage', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.internalFullMessage', + { + defaultMessage: 'The full internal message generated by Elastic.', + } + ), + }, + { + name: 'state', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.state', { + defaultMessage: 'The current state of the alert.', + }), + }, + { + name: 'clusterName', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.clusterName', + { + defaultMessage: 'The cluster to which the nodes belong.', + } + ), + }, + { + name: 'added', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.added', { + defaultMessage: 'The list of nodes added to the cluster.', + }), + }, + { + name: 'removed', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.removed', { + defaultMessage: 'The list of nodes removed from the cluster.', + }), + }, + { + name: 'restarted', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.restarted', + { + defaultMessage: 'The list of nodes restarted in the cluster.', + } + ), + }, + { + name: 'action', + description: i18n.translate('xpack.monitoring.alerts.nodesChanged.actionVariables.action', { + defaultMessage: 'The recommended action for this alert.', + }), + }, + { + name: 'actionPlain', + description: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.actionVariables.actionPlain', + { + defaultMessage: 'The recommended action for this alert, without any markdown.', + } + ), + }, + ]; + + private getNodeStates(legacyAlert: LegacyAlert): LegacyAlertNodesChangedList | undefined { + return legacyAlert.nodes; + } + + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let alertIndexPattern = INDEX_ALERTS; + if (availableCcs) { + alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); + } + const legacyAlerts = await fetchLegacyAlerts( + callCluster, + clusters, + alertIndexPattern, + WATCH_NAME, + this.config.ui.max_bucket_size + ); + return legacyAlerts.reduce((accum: AlertData[], legacyAlert) => { + accum.push({ + instanceKey: `${legacyAlert.metadata.cluster_uuid}`, + clusterUuid: legacyAlert.metadata.cluster_uuid, + shouldFire: true, // This alert always has a resolved timestamp + severity: mapLegacySeverity(legacyAlert.metadata.severity), + meta: legacyAlert, + ccs: null, + }); + return accum; + }, []); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const legacyAlert = item.meta as LegacyAlert; + const states = this.getNodeStates(legacyAlert) || { added: {}, removed: {}, restarted: {} }; + if (!alertState.ui.isFiring) { + return { + text: i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage', { + defaultMessage: `No changes in Elasticsearch nodes for this cluster.`, + }), + }; + } + + const addedText = + Object.values(states.added).length > 0 + ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { + defaultMessage: `Elasticsearch nodes '{added}' added to this cluster.`, + values: { + added: Object.values(states.added).join(','), + }, + }) + : null; + const removedText = + Object.values(states.removed).length > 0 + ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage', { + defaultMessage: `Elasticsearch nodes '{removed}' removed from this cluster.`, + values: { + removed: Object.values(states.removed).join(','), + }, + }) + : null; + const restartedText = + Object.values(states.restarted).length > 0 + ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage', { + defaultMessage: `Elasticsearch nodes '{restarted}' restarted in this cluster.`, + values: { + restarted: Object.values(states.restarted).join(','), + }, + }) + : null; + + return { + text: [addedText, removedText, restartedText].filter(Boolean).join(' '), + }; + } + + protected async executeActions( + instance: AlertInstance, + instanceState: AlertInstanceState, + item: AlertData, + cluster: AlertCluster + ) { + if (instanceState.alertStates.length === 0) { + return; + } + const alertState = instanceState.alertStates[0]; + const legacyAlert = item.meta as LegacyAlert; + if (!alertState.ui.isFiring) { + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.resolved.internalShortMessage', + { + defaultMessage: `Elasticsearch nodes changed alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.resolved.internalFullMessage', + { + defaultMessage: `Elasticsearch nodes changed alert is resolved for {clusterName}.`, + values: { + clusterName: cluster.clusterName, + }, + } + ), + state: RESOLVED, + clusterName: cluster.clusterName, + }); + } else { + const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { + defaultMessage: 'Verify that you added, removed, or restarted nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { + defaultMessage: 'View nodes', + }); + const globalState = [`cluster_uuid:${cluster.clusterUuid}`]; + if (alertState.ccs) { + globalState.push(`ccs:${alertState.ccs}`); + } + const url = `${this.kibanaUrl}/app/monitoring#elasticsearch/nodes?_g=(${globalState.join( + ',' + )})`; + const action = `[${fullActionText}](${url})`; + const states = this.getNodeStates(legacyAlert) || { added: {}, removed: {}, restarted: {} }; + const added = Object.values(states.added).join(','); + const removed = Object.values(states.removed).join(','); + const restarted = Object.values(states.restarted).join(','); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: FIRING, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index 67c74635b4e36d..06988002a20346 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -3,81 +3,106 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Moment } from 'moment'; -import { AlertExecutorOptions } from '../../../alerts/server'; -import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from './enums'; - -export interface AlertLicense { - status: string; - type: string; - expiryDateMS: number; - clusterUuid: string; -} - -export interface AlertClusterState { - state: AlertClusterStateState; - clusterUuid: string; -} +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; -export interface AlertCommonState { - [clusterUuid: string]: AlertCommonPerClusterState; +export interface AlertEnableAction { + id: string; + config: { [key: string]: any }; } -export interface AlertCommonPerClusterState { - ui: AlertCommonPerClusterUiState; +export interface AlertInstanceState { + alertStates: AlertState[]; } -export interface AlertClusterStatePerClusterState extends AlertCommonPerClusterState { - state: AlertClusterStateState; +export interface AlertState { + cluster: AlertCluster; + ccs: string | null; + ui: AlertUiState; } -export interface AlertLicensePerClusterState extends AlertCommonPerClusterState { - expiredCheckDateMS: number; +export interface AlertCpuUsageState extends AlertState { + cpuUsage: number; + nodeId: string; + nodeName: string; } -export interface AlertCommonPerClusterUiState { +export interface AlertUiState { isFiring: boolean; - severity: number; - message: AlertCommonPerClusterMessage | null; + severity: AlertSeverity; + message: AlertMessage | null; resolvedMS: number; lastCheckedMS: number; triggeredMS: number; } -export interface AlertCommonPerClusterMessage { +export interface AlertMessage { text: string; // Do this. #link this is a link #link - tokens?: AlertCommonPerClusterMessageToken[]; + nextSteps?: AlertMessage[]; + tokens?: AlertMessageToken[]; } -export interface AlertCommonPerClusterMessageToken { +export interface AlertMessageToken { startToken: string; endToken?: string; - type: AlertCommonPerClusterMessageTokenType; + type: AlertMessageTokenType; } -export interface AlertCommonPerClusterMessageLinkToken extends AlertCommonPerClusterMessageToken { +export interface AlertMessageLinkToken extends AlertMessageToken { url?: string; } -export interface AlertCommonPerClusterMessageTimeToken extends AlertCommonPerClusterMessageToken { +export interface AlertMessageTimeToken extends AlertMessageToken { isRelative: boolean; isAbsolute: boolean; + timestamp: string | number; } -export interface AlertLicensePerClusterUiState extends AlertCommonPerClusterUiState { - expirationTime: number; +export interface AlertMessageDocLinkToken extends AlertMessageToken { + partialUrl: string; } -export interface AlertCommonCluster { +export interface AlertCluster { clusterUuid: string; clusterName: string; } -export interface AlertCommonExecutorOptions extends AlertExecutorOptions { - state: AlertCommonState; +export interface AlertCpuUsageNodeStats { + clusterUuid: string; + nodeId: string; + nodeName: string; + cpuUsage: number; + containerUsage: number; + containerPeriods: number; + containerQuota: number; + ccs: string | null; +} + +export interface AlertData { + instanceKey: string; + clusterUuid: string; + ccs: string | null; + shouldFire: boolean; + severity: AlertSeverity; + meta: any; +} + +export interface LegacyAlert { + prefix: string; + message: string; + resolved_timestamp: string; + metadata: LegacyAlertMetadata; + nodes?: LegacyAlertNodesChangedList; +} + +export interface LegacyAlertMetadata { + severity: number; + cluster_uuid: string; + time: string; + link: string; } -export interface AlertCommonParams { - dateFormat: string; - timezone: string; +export interface LegacyAlertNodesChangedList { + removed: { [nodeName: string]: string }; + added: { [nodeName: string]: string }; + restarted: { [nodeName: string]: string }; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts deleted file mode 100644 index 81e375734cc507..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { executeActions, getUiMessage } from './cluster_state.lib'; -import { AlertClusterStateState } from '../../alerts/enums'; -import { AlertCommonPerClusterMessageLinkToken } from '../../alerts/types'; - -describe('clusterState lib', () => { - describe('executeActions', () => { - const clusterName = 'clusterA'; - const instance: any = { scheduleActions: jest.fn() }; - const license: any = { clusterName }; - const status = AlertClusterStateState.Green; - const emailAddress = 'test@test.com'; - - beforeEach(() => { - instance.scheduleActions.mockClear(); - }); - - it('should schedule actions when firing', () => { - executeActions(instance, license, status, emailAddress, false); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'NEW X-Pack Monitoring: Cluster Status', - message: `Allocate missing replica shards for cluster '${clusterName}'`, - to: emailAddress, - }); - }); - - it('should have a different message for red state', () => { - executeActions(instance, license, AlertClusterStateState.Red, emailAddress, false); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'NEW X-Pack Monitoring: Cluster Status', - message: `Allocate missing primary and replica shards for cluster '${clusterName}'`, - to: emailAddress, - }); - }); - - it('should schedule actions when resolved', () => { - executeActions(instance, license, status, emailAddress, true); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'RESOLVED X-Pack Monitoring: Cluster Status', - message: `This cluster alert has been resolved: Allocate missing replica shards for cluster '${clusterName}'`, - to: emailAddress, - }); - }); - }); - - describe('getUiMessage', () => { - it('should return a message when firing', () => { - const message = getUiMessage(AlertClusterStateState.Red, false); - expect(message.text).toBe( - `Elasticsearch cluster status is red. #start_linkAllocate missing primary and replica shards#end_link` - ); - expect(message.tokens && message.tokens.length).toBe(1); - expect(message.tokens && message.tokens[0].startToken).toBe('#start_link'); - expect(message.tokens && message.tokens[0].endToken).toBe('#end_link'); - expect( - message.tokens && (message.tokens[0] as AlertCommonPerClusterMessageLinkToken).url - ).toBe('elasticsearch/indices'); - }); - - it('should return a message when resolved', () => { - const message = getUiMessage(AlertClusterStateState.Green, true); - expect(message.text).toBe(`Elasticsearch cluster status is green.`); - expect(message.tokens).not.toBeDefined(); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts deleted file mode 100644 index c4553d87980da5..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ -import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerts/server'; -import { - AlertCommonCluster, - AlertCommonPerClusterMessage, - AlertCommonPerClusterMessageLinkToken, -} from '../../alerts/types'; -import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from '../../alerts/enums'; - -const RESOLVED_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.resolvedSubject', { - defaultMessage: 'RESOLVED X-Pack Monitoring: Cluster Status', -}); - -const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.clusterStatus.newSubject', { - defaultMessage: 'NEW X-Pack Monitoring: Cluster Status', -}); - -const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterStatus.redMessage', { - defaultMessage: 'Allocate missing primary and replica shards', -}); - -const YELLOW_STATUS_MESSAGE = i18n.translate( - 'xpack.monitoring.alerts.clusterStatus.yellowMessage', - { - defaultMessage: 'Allocate missing replica shards', - } -); - -export function executeActions( - instance: AlertInstance, - cluster: AlertCommonCluster, - status: AlertClusterStateState, - emailAddress: string, - resolved: boolean = false -) { - const message = - status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE; - if (resolved) { - instance.scheduleActions('default', { - subject: RESOLVED_SUBJECT, - message: `This cluster alert has been resolved: ${message} for cluster '${cluster.clusterName}'`, - to: emailAddress, - }); - } else { - instance.scheduleActions('default', { - subject: NEW_SUBJECT, - message: `${message} for cluster '${cluster.clusterName}'`, - to: emailAddress, - }); - } -} - -export function getUiMessage( - status: AlertClusterStateState, - resolved: boolean = false -): AlertCommonPerClusterMessage { - if (resolved) { - return { - text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage', { - defaultMessage: `Elasticsearch cluster status is green.`, - }), - }; - } - const message = - status === AlertClusterStateState.Red ? RED_STATUS_MESSAGE : YELLOW_STATUS_MESSAGE; - return { - text: i18n.translate('xpack.monitoring.alerts.clusterStatus.ui.firingMessage', { - defaultMessage: `Elasticsearch cluster status is {status}. #start_link{message}#end_link`, - values: { - status, - message, - }, - }), - tokens: [ - { - startToken: '#start_link', - endToken: '#end_link', - type: AlertCommonPerClusterMessageTokenType.Link, - url: 'elasticsearch/indices', - } as AlertCommonPerClusterMessageLinkToken, - ], - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts deleted file mode 100644 index 642ae3c39a0275..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { fetchClusterState } from './fetch_cluster_state'; - -describe('fetchClusterState', () => { - it('should return the cluster state', async () => { - const status = 'green'; - const clusterUuid = 'sdfdsaj34434'; - const callCluster = jest.fn(() => ({ - hits: { - hits: [ - { - _source: { - cluster_state: { - status, - }, - cluster_uuid: clusterUuid, - }, - }, - ], - }, - })); - - const clusters = [{ clusterUuid, clusterName: 'foo' }]; - const index = '.monitoring-es-*'; - - const state = await fetchClusterState(callCluster, clusters, index); - expect(state).toEqual([ - { - state: status, - clusterUuid, - }, - ]); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts deleted file mode 100644 index 3fcc3a2c989937..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_state.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { get } from 'lodash'; -import { AlertCommonCluster, AlertClusterState } from '../../alerts/types'; - -export async function fetchClusterState( - callCluster: any, - clusters: AlertCommonCluster[], - index: string -): Promise { - const params = { - index, - filterPath: ['hits.hits._source.cluster_state.status', 'hits.hits._source.cluster_uuid'], - body: { - size: 1, - sort: [{ timestamp: { order: 'desc' } }], - query: { - bool: { - filter: [ - { - terms: { - cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - type: 'cluster_stats', - }, - }, - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - ], - }, - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - return { - state: get(hit, '_source.cluster_state.status'), - clusterUuid: get(hit, '_source.cluster_uuid'), - }; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index d1513ac16fb158..48ad31d20a3951 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCommonCluster } from '../../alerts/types'; +import { AlertCluster } from '../../alerts/types'; -export async function fetchClusters( - callCluster: any, - index: string -): Promise { +export async function fetchClusters(callCluster: any, index: string): Promise { const params = { index, filterPath: [ 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 new file mode 100644 index 00000000000000..12926a30efa1bc --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchCpuUsageNodeStats } from './fetch_cpu_usage_node_stats'; + +describe('fetchCpuUsageNodeStats', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'abc123', + clusterName: 'test', + }, + ]; + const index = '.monitoring-es-*'; + const startMs = 0; + const endMs = 0; + const size = 10; + + it('fetch normal stats', async () => { + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: [ + { + key: clusters[0].clusterUuid, + nodes: { + buckets: [ + { + key: 'theNodeId', + index: { + buckets: [ + { + key: '.monitoring-es-TODAY', + }, + ], + }, + name: { + buckets: [ + { + key: 'theNodeName', + }, + ], + }, + average_cpu: { + value: 10, + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + nodeName: 'theNodeName', + nodeId: 'theNodeId', + cpuUsage: 10, + containerUsage: undefined, + containerPeriods: undefined, + containerQuota: undefined, + ccs: null, + }, + ]); + }); + + it('fetch container stats', async () => { + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: [ + { + key: clusters[0].clusterUuid, + nodes: { + buckets: [ + { + key: 'theNodeId', + index: { + buckets: [ + { + key: '.monitoring-es-TODAY', + }, + ], + }, + name: { + buckets: [ + { + key: 'theNodeName', + }, + ], + }, + average_usage: { + value: 10, + }, + average_periods: { + value: 5, + }, + average_quota: { + value: 50, + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + nodeName: 'theNodeName', + nodeId: 'theNodeId', + cpuUsage: undefined, + containerUsage: 10, + containerPeriods: 5, + containerQuota: 50, + ccs: null, + }, + ]); + }); + + it('fetch properly return ccs', async () => { + callCluster = jest.fn().mockImplementation((...args) => { + return { + aggregations: { + clusters: { + buckets: [ + { + key: clusters[0].clusterUuid, + nodes: { + buckets: [ + { + key: 'theNodeId', + index: { + buckets: [ + { + key: 'foo:.monitoring-es-TODAY', + }, + ], + }, + name: { + buckets: [ + { + key: 'theNodeName', + }, + ], + }, + average_usage: { + value: 10, + }, + average_periods: { + value: 5, + }, + average_quota: { + value: 50, + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(result[0].ccs).toBe('foo'); + }); + + it('should use consistent params', async () => { + let params = null; + callCluster = jest.fn().mockImplementation((...args) => { + params = args[1]; + }); + await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + expect(params).toStrictEqual({ + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { cluster_uuid: clusters.map((cluster) => cluster.clusterUuid) } }, + { term: { type: 'node_stats' } }, + { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + include: clusters.map((cluster) => cluster.clusterUuid), + }, + aggs: { + nodes: { + terms: { field: 'node_stats.node_id', size }, + aggs: { + index: { terms: { field: '_index', size: 1 } }, + average_cpu: { avg: { field: 'node_stats.process.cpu.percent' } }, + average_usage: { avg: { field: 'node_stats.os.cgroup.cpuacct.usage_nanos' } }, + average_periods: { + avg: { field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods' }, + }, + average_quota: { avg: { field: 'node_stats.os.cgroup.cpu.cfs_quota_micros' } }, + name: { terms: { field: 'source_node.name', size: 1 } }, + }, + }, + }, + }, + }, + }, + }); + }); +}); 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 new file mode 100644 index 00000000000000..4fdb03b61950e9 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertCpuUsageNodeStats } from '../../alerts/types'; + +interface NodeBucketESResponse { + key: string; + average_cpu: { value: number }; +} + +interface ClusterBucketESResponse { + key: string; + nodes: { + buckets: NodeBucketESResponse[]; + }; +} + +export async function fetchCpuUsageNodeStats( + callCluster: any, + clusters: AlertCluster[], + index: string, + startMs: number, + endMs: number, + size: number +): Promise { + const filterPath = ['aggregations']; + const params = { + index, + filterPath, + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'node_stats', + }, + }, + { + range: { + timestamp: { + format: 'epoch_millis', + gte: startMs, + lte: endMs, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + include: clusters.map((cluster) => cluster.clusterUuid), + }, + aggs: { + nodes: { + terms: { + field: 'node_stats.node_id', + size, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + average_cpu: { + avg: { + field: 'node_stats.process.cpu.percent', + }, + }, + average_usage: { + avg: { + field: 'node_stats.os.cgroup.cpuacct.usage_nanos', + }, + }, + average_periods: { + avg: { + field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods', + }, + }, + average_quota: { + avg: { + field: 'node_stats.os.cgroup.cpu.cfs_quota_micros', + }, + }, + name: { + terms: { + field: 'source_node.name', + size: 1, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const stats: AlertCpuUsageNodeStats[] = []; + const clusterBuckets = get( + response, + 'aggregations.clusters.buckets', + [] + ) as ClusterBucketESResponse[]; + for (const clusterBucket of clusterBuckets) { + for (const node of clusterBucket.nodes.buckets) { + const indexName = get(node, 'index.buckets[0].key', ''); + stats.push({ + clusterUuid: clusterBucket.key, + nodeId: node.key, + nodeName: get(node, 'name.buckets[0].key'), + cpuUsage: get(node, 'average_cpu.value'), + containerUsage: get(node, 'average_usage.value'), + containerPeriods: get(node, 'average_periods.value'), + containerQuota: get(node, 'average_quota.value'), + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }); + } + } + return stats; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts deleted file mode 100644 index ae914c7a2ace1b..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { fetchDefaultEmailAddress } from './fetch_default_email_address'; -import { uiSettingsServiceMock } from '../../../../../../src/core/server/mocks'; - -describe('fetchDefaultEmailAddress', () => { - it('get the email address', async () => { - const email = 'test@test.com'; - const uiSettingsClient = uiSettingsServiceMock.createClient(); - uiSettingsClient.get.mockResolvedValue(email); - const result = await fetchDefaultEmailAddress(uiSettingsClient); - expect(result).toBe(email); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts deleted file mode 100644 index 88e4199a88256a..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { IUiSettingsClient } from 'src/core/server'; -import { MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS } from '../../../common/constants'; - -export async function fetchDefaultEmailAddress( - uiSettingsClient: IUiSettingsClient -): Promise { - return await uiSettingsClient.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts new file mode 100644 index 00000000000000..a3743a8ff206f7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchLegacyAlerts } from './fetch_legacy_alerts'; + +describe('fetchLegacyAlerts', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'abc123', + clusterName: 'test', + }, + ]; + const index = '.monitoring-es-*'; + const size = 10; + + it('fetch legacy alerts', async () => { + const prefix = 'thePrefix'; + const message = 'theMessage'; + const nodes = {}; + const metadata = { + severity: 2000, + cluster_uuid: clusters[0].clusterUuid, + metadata: {}, + }; + callCluster = jest.fn().mockImplementation(() => { + return { + hits: { + hits: [ + { + _source: { + prefix, + message, + nodes, + metadata, + }, + }, + ], + }, + }; + }); + const result = await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); + expect(result).toEqual([ + { + message, + metadata, + nodes, + prefix, + }, + ]); + }); + + it('should use consistent params', async () => { + let params = null; + callCluster = jest.fn().mockImplementation((...args) => { + params = args[1]; + }); + await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); + expect(params).toStrictEqual({ + index, + filterPath: [ + 'hits.hits._source.prefix', + 'hits.hits._source.message', + 'hits.hits._source.resolved_timestamp', + 'hits.hits._source.nodes', + 'hits.hits._source.metadata.*', + ], + body: { + size, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + minimum_should_match: 1, + filter: [ + { + terms: { 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid) }, + }, + { term: { 'metadata.watch': 'myWatch' } }, + ], + should: [ + { range: { timestamp: { gte: 'now-2m' } } }, + { range: { resolved_timestamp: { gte: 'now-2m' } } }, + { bool: { must_not: { exists: { field: 'resolved_timestamp' } } } }, + ], + }, + }, + collapse: { field: 'metadata.cluster_uuid' }, + }, + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts new file mode 100644 index 00000000000000..fe01a1b921c2eb --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../alerts/types'; + +export async function fetchLegacyAlerts( + callCluster: any, + clusters: AlertCluster[], + index: string, + watchName: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.prefix', + 'hits.hits._source.message', + 'hits.hits._source.resolved_timestamp', + 'hits.hits._source.nodes', + 'hits.hits._source.metadata.*', + ], + body: { + size, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + query: { + bool: { + minimum_should_match: 1, + filter: [ + { + terms: { + 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + 'metadata.watch': watchName, + }, + }, + ], + should: [ + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + { + range: { + resolved_timestamp: { + gte: 'now-2m', + }, + }, + }, + { + bool: { + must_not: { + exists: { + field: 'resolved_timestamp', + }, + }, + }, + }, + ], + }, + }, + collapse: { + field: 'metadata.cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'hits.hits', []).map((hit: any) => { + const legacyAlert: LegacyAlert = { + prefix: get(hit, '_source.prefix'), + message: get(hit, '_source.message'), + resolved_timestamp: get(hit, '_source.resolved_timestamp'), + nodes: get(hit, '_source.nodes'), + metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, + }; + return legacyAlert; + }); +} 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 deleted file mode 100644 index 9dcb4ffb82a5fc..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { fetchLicenses } from './fetch_licenses'; - -describe('fetchLicenses', () => { - const clusterName = 'MyCluster'; - const clusterUuid = 'clusterA'; - const license = { - status: 'active', - expiry_date_in_millis: 1579532493876, - type: 'basic', - }; - - it('return a list of licenses', async () => { - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - license, - cluster_uuid: clusterUuid, - }, - }, - ], - }, - })); - const clusters = [{ clusterUuid, clusterName }]; - const index = '.monitoring-es-*'; - const result = await fetchLicenses(callCluster, clusters, index); - expect(result).toEqual([ - { - status: license.status, - type: license.type, - expiryDateMS: license.expiry_date_in_millis, - clusterUuid, - }, - ]); - }); - - it('should only search for the clusters provided', async () => { - const callCluster = jest.fn(); - const clusters = [{ clusterUuid, clusterName }]; - const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); - }); - - it('should limit the time period in the query', async () => { - const callCluster = jest.fn(); - const clusters = [{ clusterUuid, clusterName }]; - const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts deleted file mode 100644 index a65cba493dab9b..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { get } from 'lodash'; -import { AlertLicense, AlertCommonCluster } from '../../alerts/types'; - -export async function fetchLicenses( - callCluster: any, - clusters: AlertCommonCluster[], - index: string -): Promise { - const params = { - index, - filterPath: ['hits.hits._source.license.*', 'hits.hits._source.cluster_uuid'], - body: { - size: 1, - sort: [{ timestamp: { order: 'desc' } }], - query: { - bool: { - filter: [ - { - terms: { - cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - type: 'cluster_stats', - }, - }, - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - ], - }, - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const rawLicense: any = get(hit, '_source.license', {}); - const license: AlertLicense = { - status: rawLicense.status, - type: rawLicense.type, - expiryDateMS: rawLicense.expiry_date_in_millis, - clusterUuid: get(hit, '_source.cluster_uuid'), - }; - return license; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index a3bcb61afacd61..ff674195f0730a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -5,22 +5,31 @@ */ import { fetchStatus } from './fetch_status'; -import { AlertCommonPerClusterState } from '../../alerts/types'; +import { AlertUiState, AlertState } from '../../alerts/types'; +import { AlertSeverity } from '../../../common/enums'; +import { ALERT_CPU_USAGE, ALERT_CLUSTER_HEALTH } from '../../../common/constants'; describe('fetchStatus', () => { - const alertType = 'monitoringTest'; + const alertType = ALERT_CPU_USAGE; + const alertTypes = [alertType]; const log = { warn: jest.fn() }; const start = 0; const end = 0; const id = 1; - const defaultUiState = { + const defaultClusterState = { + clusterUuid: 'abc', + clusterName: 'test', + }; + const defaultUiState: AlertUiState = { isFiring: false, - severity: 0, + severity: AlertSeverity.Success, message: null, resolvedMS: 0, lastCheckedMS: 0, triggeredMS: 0, }; + let alertStates: AlertState[] = []; + const licenseService = null; const alertsClient = { find: jest.fn(() => ({ total: 1, @@ -31,10 +40,12 @@ describe('fetchStatus', () => { ], })), getAlertState: jest.fn(() => ({ - alertTypeState: { - state: { - ui: defaultUiState, - } as AlertCommonPerClusterState, + alertInstances: { + abc: { + state: { + alertStates, + }, + }, }, })), }; @@ -45,57 +56,96 @@ describe('fetchStatus', () => { }); it('should fetch from the alerts client', async () => { - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status).toEqual([]); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(status).toEqual({ + monitoring_alert_cpu_usage: { + alert: { + isLegacy: false, + label: 'CPU Usage', + paramDetails: {}, + rawAlert: { id: 1 }, + type: 'monitoring_alert_cpu_usage', + }, + enabled: true, + exists: true, + states: [], + }, + }); }); it('should return alerts that are firing', async () => { - alertsClient.getAlertState = jest.fn(() => ({ - alertTypeState: { - state: { - ui: { - ...defaultUiState, - isFiring: true, - }, - } as AlertCommonPerClusterState, + alertStates = [ + { + cluster: defaultClusterState, + ccs: null, + ui: { + ...defaultUiState, + isFiring: true, + }, }, - })); + ]; - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status.length).toBe(1); - expect(status[0].type).toBe(alertType); - expect(status[0].isFiring).toBe(true); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(Object.values(status).length).toBe(1); + expect(Object.keys(status)).toEqual(alertTypes); + expect(status[alertType].states[0].state.ui.isFiring).toBe(true); }); it('should return alerts that have been resolved in the time period', async () => { - alertsClient.getAlertState = jest.fn(() => ({ - alertTypeState: { - state: { - ui: { - ...defaultUiState, - resolvedMS: 1500, - }, - } as AlertCommonPerClusterState, + alertStates = [ + { + cluster: defaultClusterState, + ccs: null, + ui: { + ...defaultUiState, + resolvedMS: 1500, + }, }, - })); + ]; const customStart = 1000; const customEnd = 2000; const status = await fetchStatus( alertsClient as any, - [alertType], + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, customStart, customEnd, log as any ); - expect(status.length).toBe(1); - expect(status[0].type).toBe(alertType); - expect(status[0].isFiring).toBe(false); + expect(Object.values(status).length).toBe(1); + expect(Object.keys(status)).toEqual(alertTypes); + expect(status[alertType].states[0].state.ui.isFiring).toBe(false); }); it('should pass in the right filter to the alerts client', async () => { - await fetchStatus(alertsClient as any, [alertType], start, end, log as any); + await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` ); @@ -106,8 +156,16 @@ describe('fetchStatus', () => { alertTypeState: null, })) as any; - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status).toEqual([]); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(status[alertType].states.length).toEqual(0); }); it('should return nothing if no alerts are found', async () => { @@ -116,7 +174,34 @@ describe('fetchStatus', () => { data: [], })) as any; - const status = await fetchStatus(alertsClient as any, [alertType], start, end, log as any); - expect(status).toEqual([]); + const status = await fetchStatus( + alertsClient as any, + licenseService as any, + alertTypes, + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(status).toEqual({}); + }); + + it('should pass along the license service', async () => { + const customLicenseService = { + getWatcherFeature: jest.fn().mockImplementation(() => ({ + isAvailable: true, + isEnabled: true, + })), + }; + await fetchStatus( + alertsClient as any, + customLicenseService as any, + [ALERT_CLUSTER_HEALTH], + defaultClusterState.clusterUuid, + start, + end, + log as any + ); + expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 614658baf5c799..49e688fafbee59 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -4,56 +4,76 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment'; -import { Logger } from '../../../../../../src/core/server'; -import { AlertCommonPerClusterState } from '../../alerts/types'; +import { AlertInstanceState } from '../../alerts/types'; import { AlertsClient } from '../../../../alerts/server'; +import { AlertsFactory } from '../../alerts'; +import { CommonAlertStatus, CommonAlertState, CommonAlertFilter } from '../../../common/types'; +import { ALERTS } from '../../../common/constants'; +import { MonitoringLicenseService } from '../../types'; export async function fetchStatus( alertsClient: AlertsClient, - alertTypes: string[], + licenseService: MonitoringLicenseService, + alertTypes: string[] | undefined, + clusterUuid: string, start: number, end: number, - log: Logger -): Promise { - const statuses = await Promise.all( - alertTypes.map( - (type) => - new Promise(async (resolve, reject) => { - // We need to get the id from the alertTypeId - const alerts = await alertsClient.find({ - options: { - filter: `alert.attributes.alertTypeId:${type}`, - }, - }); - if (alerts.total === 0) { - return resolve(false); - } + filters: CommonAlertFilter[] +): Promise<{ [type: string]: CommonAlertStatus }> { + const byType: { [type: string]: CommonAlertStatus } = {}; + await Promise.all( + (alertTypes || ALERTS).map(async (type) => { + const alert = await AlertsFactory.getByType(type, alertsClient); + if (!alert || !alert.isEnabled(licenseService)) { + return; + } + const serialized = alert.serialize(); + if (!serialized) { + return; + } - if (alerts.total !== 1) { - log.warn(`Found more than one alert for type ${type} which is unexpected.`); - } + const result: CommonAlertStatus = { + exists: false, + enabled: false, + states: [], + alert: serialized, + }; + + byType[type] = result; + + const id = alert.getId(); + if (!id) { + return result; + } + + result.exists = true; + result.enabled = true; - const id = alerts.data[0].id; + // Now that we have the id, we can get the state + const states = await alert.getStates(alertsClient, id, filters); + if (!states) { + return result; + } - // Now that we have the id, we can get the state - const states = await alertsClient.getAlertState({ id }); - if (!states || !states.alertTypeState) { - log.warn(`No alert states found for type ${type} which is unexpected.`); - return resolve(false); + result.states = Object.values(states).reduce((accum: CommonAlertState[], instance: any) => { + const alertInstanceState = instance.state as AlertInstanceState; + for (const state of alertInstanceState.alertStates) { + const meta = instance.meta; + if (clusterUuid && state.cluster.clusterUuid !== clusterUuid) { + return accum; } - const state = Object.values(states.alertTypeState)[0] as AlertCommonPerClusterState; + let firing = false; const isInBetween = moment(state.ui.resolvedMS).isBetween(start, end); if (state.ui.isFiring || isInBetween) { - return resolve({ - type, - ...state.ui, - }); + firing = true; } - return resolve(false); - }) - ) + accum.push({ firing, state, meta }); + } + return accum; + }, []); + }) ); - return statuses.filter(Boolean); + return byType; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts deleted file mode 100644 index 1840a2026a7534..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getPreparedAlert } from './get_prepared_alert'; -import { fetchClusters } from './fetch_clusters'; -import { fetchDefaultEmailAddress } from './fetch_default_email_address'; - -jest.mock('./fetch_clusters', () => ({ - fetchClusters: jest.fn(), -})); - -jest.mock('./fetch_default_email_address', () => ({ - fetchDefaultEmailAddress: jest.fn(), -})); - -describe('getPreparedAlert', () => { - const uiSettings = { get: jest.fn() }; - const alertType = 'test'; - const getUiSettingsService = async () => ({ - asScopedToClient: () => uiSettings, - }); - const monitoringCluster = null; - const logger = { warn: jest.fn() }; - const ccsEnabled = false; - const services = { - callCluster: jest.fn(), - savedObjectsClient: null, - }; - const emailAddress = 'foo@foo.com'; - const data = [{ foo: 1 }]; - const dataFetcher = () => data; - const clusterName = 'MonitoringCluster'; - const clusterUuid = 'sdf34sdf'; - const clusters = [{ clusterName, clusterUuid }]; - - afterEach(() => { - (uiSettings.get as jest.Mock).mockClear(); - (services.callCluster as jest.Mock).mockClear(); - (fetchClusters as jest.Mock).mockClear(); - (fetchDefaultEmailAddress as jest.Mock).mockClear(); - }); - - beforeEach(() => { - (fetchClusters as jest.Mock).mockImplementation(() => clusters); - (fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => emailAddress); - }); - - it('should return fields as expected', async () => { - (uiSettings.get as jest.Mock).mockImplementation(() => { - return emailAddress; - }); - - const alert = await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - ccsEnabled, - services as any, - dataFetcher as any - ); - - expect(alert && alert.emailAddress).toBe(emailAddress); - expect(alert && alert.data).toBe(data); - }); - - it('should add ccs if specified', async () => { - const ccsClusterName = 'remoteCluster'; - (services.callCluster as jest.Mock).mockImplementation(() => { - return { - [ccsClusterName]: { - connected: true, - }, - }; - }); - - await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - dataFetcher as any - ); - - expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(true); - }); - - it('should ignore ccs if no remote clusters are available', async () => { - const ccsClusterName = 'remoteCluster'; - (services.callCluster as jest.Mock).mockImplementation(() => { - return { - [ccsClusterName]: { - connected: false, - }, - }; - }); - - await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - dataFetcher as any - ); - - expect((fetchClusters as jest.Mock).mock.calls[0][1].includes(ccsClusterName)).toBe(false); - }); - - it('should pass in the clusters into the data fetcher', async () => { - const customDataFetcher = jest.fn(() => data); - - await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - customDataFetcher as any - ); - - expect((customDataFetcher as jest.Mock).mock.calls[0][1]).toBe(clusters); - }); - - it('should return nothing if the data fetcher returns nothing', async () => { - const customDataFetcher = jest.fn(() => []); - - const result = await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - customDataFetcher as any - ); - - expect(result).toBe(null); - }); - - it('should return nothing if there is no email address', async () => { - (fetchDefaultEmailAddress as jest.Mock).mockImplementation(() => null); - - const result = await getPreparedAlert( - alertType, - getUiSettingsService as any, - monitoringCluster as any, - logger as any, - true, - services as any, - dataFetcher as any - ); - - expect(result).toBe(null); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts deleted file mode 100644 index 1d307bc018a7bf..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Logger, ILegacyCustomClusterClient, UiSettingsServiceStart } from 'kibana/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { AlertServices } from '../../../../alerts/server'; -import { AlertCommonCluster } from '../../alerts/types'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; -import { fetchAvailableCcs } from './fetch_available_ccs'; -import { getCcsIndexPattern } from './get_ccs_index_pattern'; -import { fetchClusters } from './fetch_clusters'; -import { fetchDefaultEmailAddress } from './fetch_default_email_address'; - -export interface PreparedAlert { - emailAddress: string; - clusters: AlertCommonCluster[]; - data: any[]; - timezone: string; - dateFormat: string; -} - -async function getCallCluster( - monitoringCluster: ILegacyCustomClusterClient, - services: Pick -): Promise { - if (!monitoringCluster) { - return services.callCluster; - } - - return monitoringCluster.callAsInternalUser; -} - -export async function getPreparedAlert( - alertType: string, - getUiSettingsService: () => Promise, - monitoringCluster: ILegacyCustomClusterClient, - logger: Logger, - ccsEnabled: boolean, - services: Pick, - dataFetcher: ( - callCluster: CallCluster, - clusters: AlertCommonCluster[], - esIndexPattern: string - ) => Promise -): Promise { - const callCluster = await getCallCluster(monitoringCluster, services); - - // Support CCS use cases by querying to find available remote clusters - // and then adding those to the index pattern we are searching against - let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; - if (ccsEnabled) { - const availableCcs = await fetchAvailableCcs(callCluster); - if (availableCcs.length > 0) { - esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); - } - } - - const clusters = await fetchClusters(callCluster, esIndexPattern); - - // Fetch the specific data - const data = await dataFetcher(callCluster, clusters, esIndexPattern); - if (data.length === 0) { - logger.warn(`No data found for ${alertType}.`); - return null; - } - - const uiSettings = (await getUiSettingsService()).asScopedToClient(services.savedObjectsClient); - const dateFormat: string = await uiSettings.get('dateFormat'); - const timezone: string = await uiSettings.get('dateFormat:tz'); - const emailAddress = await fetchDefaultEmailAddress(uiSettings); - if (!emailAddress) { - // TODO: we can do more here - logger.warn(`Unable to send email for ${alertType} because there is no email configured.`); - return null; - } - - return { - emailAddress, - data, - clusters, - dateFormat, - timezone, - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts deleted file mode 100644 index b99208bdde2c86..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import moment from 'moment-timezone'; -import { executeActions, getUiMessage } from './license_expiration.lib'; - -describe('licenseExpiration lib', () => { - describe('executeActions', () => { - const clusterName = 'clusterA'; - const instance: any = { scheduleActions: jest.fn() }; - const license: any = { clusterName }; - const $expiry = moment('2020-01-20'); - const dateFormat = 'dddd, MMMM Do YYYY, h:mm:ss a'; - const emailAddress = 'test@test.com'; - - beforeEach(() => { - instance.scheduleActions.mockClear(); - }); - - it('should schedule actions when firing', () => { - executeActions(instance, license, $expiry, dateFormat, emailAddress, false); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'NEW X-Pack Monitoring: License Expiration', - message: `Cluster '${clusterName}' license is going to expire on Monday, January 20th 2020, 12:00:00 am. Please update your license.`, - to: emailAddress, - }); - }); - - it('should schedule actions when resolved', () => { - executeActions(instance, license, $expiry, dateFormat, emailAddress, true); - expect(instance.scheduleActions).toHaveBeenCalledWith('default', { - subject: 'RESOLVED X-Pack Monitoring: License Expiration', - message: `This cluster alert has been resolved: Cluster '${clusterName}' license was going to expire on Monday, January 20th 2020, 12:00:00 am.`, - to: emailAddress, - }); - }); - }); - - describe('getUiMessage', () => { - it('should return a message when firing', () => { - const message = getUiMessage(false); - expect(message.text).toBe( - `This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license.#end_link` - ); - // LOL How do I avoid this in TS???? - if (!message.tokens) { - return expect(false).toBe(true); - } - expect(message.tokens.length).toBe(3); - expect(message.tokens[0].startToken).toBe('#relative'); - expect(message.tokens[1].startToken).toBe('#absolute'); - expect(message.tokens[2].startToken).toBe('#start_link'); - expect(message.tokens[2].endToken).toBe('#end_link'); - }); - - it('should return a message when resolved', () => { - const message = getUiMessage(true); - expect(message.text).toBe(`This cluster's license is active.`); - expect(message.tokens).not.toBeDefined(); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts deleted file mode 100644 index 97ef2790b516df..00000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ -import { Moment } from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerts/server'; -import { - AlertCommonPerClusterMessageLinkToken, - AlertCommonPerClusterMessageTimeToken, - AlertCommonCluster, - AlertCommonPerClusterMessage, -} from '../../alerts/types'; -import { AlertCommonPerClusterMessageTokenType } from '../../alerts/enums'; - -const RESOLVED_SUBJECT = i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', - { - defaultMessage: 'RESOLVED X-Pack Monitoring: License Expiration', - } -); - -const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.newSubject', { - defaultMessage: 'NEW X-Pack Monitoring: License Expiration', -}); - -export function executeActions( - instance: AlertInstance, - cluster: AlertCommonCluster, - $expiry: Moment, - dateFormat: string, - emailAddress: string, - resolved: boolean = false -) { - if (resolved) { - instance.scheduleActions('default', { - subject: RESOLVED_SUBJECT, - message: `This cluster alert has been resolved: Cluster '${ - cluster.clusterName - }' license was going to expire on ${$expiry.format(dateFormat)}.`, - to: emailAddress, - }); - } else { - instance.scheduleActions('default', { - subject: NEW_SUBJECT, - message: `Cluster '${cluster.clusterName}' license is going to expire on ${$expiry.format( - dateFormat - )}. Please update your license.`, - to: emailAddress, - }); - } -} - -export function getUiMessage(resolved: boolean = false): AlertCommonPerClusterMessage { - if (resolved) { - return { - text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { - defaultMessage: `This cluster's license is active.`, - }), - }; - } - return { - text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { - defaultMessage: `This cluster's license is going to expire in #relative at #absolute. #start_linkPlease update your license.#end_link`, - }), - tokens: [ - { - startToken: '#relative', - type: AlertCommonPerClusterMessageTokenType.Time, - isRelative: true, - isAbsolute: false, - } as AlertCommonPerClusterMessageTimeToken, - { - startToken: '#absolute', - type: AlertCommonPerClusterMessageTokenType.Time, - isAbsolute: true, - isRelative: false, - } as AlertCommonPerClusterMessageTimeToken, - { - startToken: '#start_link', - endToken: '#end_link', - type: AlertCommonPerClusterMessageTokenType.Link, - url: 'license', - } as AlertCommonPerClusterMessageLinkToken, - ], - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts new file mode 100644 index 00000000000000..11a1c6eb1a6d6b --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertSeverity } from '../../../common/enums'; +import { mapLegacySeverity } from './map_legacy_severity'; + +describe('mapLegacySeverity', () => { + it('should map it', () => { + expect(mapLegacySeverity(500)).toBe(AlertSeverity.Warning); + expect(mapLegacySeverity(1000)).toBe(AlertSeverity.Warning); + expect(mapLegacySeverity(2000)).toBe(AlertSeverity.Danger); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.ts new file mode 100644 index 00000000000000..5687c0c15b03b8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/map_legacy_severity.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertSeverity } from '../../../common/enums'; + +export function mapLegacySeverity(severity: number) { + const floor = Math.floor(severity / 1000); + if (floor <= 1) { + return AlertSeverity.Warning; + } + return AlertSeverity.Danger; +} 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 5ed8d6b01aba5e..50a4df8a3ff57e 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 @@ -13,13 +13,10 @@ import { getKibanasForClusters } from '../kibana'; import { getLogstashForClusters } from '../logstash'; import { getLogstashPipelineIds } from '../logstash/get_pipeline_ids'; import { getBeatsForClusters } from '../beats'; -import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_aggregation'; -import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search'; +import { verifyMonitoringLicense } from '../../cluster_alerts/verify_monitoring_license'; import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; -import { fetchStatus } from '../alerts/fetch_status'; import { getClustersSummary } from './get_clusters_summary'; import { - CLUSTER_ALERTS_SEARCH_SIZE, STANDALONE_CLUSTER_CLUSTER_UUID, CODE_PATH_ML, CODE_PATH_ALERTS, @@ -28,12 +25,11 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, - KIBANA_ALERTING_ENABLED, - ALERT_TYPES, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; import { checkCcrEnabled } from '../elasticsearch/ccr'; +import { fetchStatus } from '../alerts/fetch_status'; import { getStandaloneClusterDefinition, hasStandaloneClusters } from '../standalone_clusters'; import { getLogTypes } from '../logs'; import { isInCodePath } from './is_in_code_path'; @@ -52,7 +48,6 @@ export async function getClustersFromRequest( lsIndexPattern, beatsIndexPattern, apmIndexPattern, - alertsIndex, filebeatIndexPattern, } = indexPatterns; @@ -101,25 +96,6 @@ export async function getClustersFromRequest( cluster.ml = { jobs: mlJobs }; } - if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { - if (KIBANA_ALERTING_ENABLED) { - const alertsClient = req.getAlertsClient ? req.getAlertsClient() : null; - cluster.alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger); - } else { - cluster.alerts = await alertsClusterSearch( - req, - alertsIndex, - cluster, - checkLicenseForAlerts, - { - start, - end, - size: CLUSTER_ALERTS_SEARCH_SIZE, - } - ); - } - } - cluster.logs = isInCodePath(codePaths, [CODE_PATH_LOGS]) ? await getLogTypes(req, filebeatIndexPattern, { clusterUuid: cluster.cluster_uuid, @@ -141,21 +117,67 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { - const clustersAlerts = await alertsClustersAggregation( - req, - alertsIndex, - clusters, - checkLicenseForAlerts - ); - clusters.forEach((cluster) => { + const alertsClient = req.getAlertsClient(); + for (const cluster of clusters) { + const verification = verifyMonitoringLicense(req.server); + 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; + } + + // 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) { + cluster.alerts = { + list: await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + cluster.cluster_uuid, + start, + end, + [] + ), + alertsMeta: { + enabled: true, + }, + }; + continue; + } + cluster.alerts = { + list: {}, alertsMeta: { - enabled: clustersAlerts.alertsMeta.enabled, - message: clustersAlerts.alertsMeta.message, // NOTE: this is only defined when the alert feature is disabled + enabled: true, + }, + 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}`, + }, + } + ), }, - ...clustersAlerts[cluster.cluster_uuid], }; - }); + } } } diff --git a/x-pack/plugins/monitoring/server/lib/errors/handle_error.js b/x-pack/plugins/monitoring/server/lib/errors/handle_error.js index d6549a8fa98e97..4726020210ce78 100644 --- a/x-pack/plugins/monitoring/server/lib/errors/handle_error.js +++ b/x-pack/plugins/monitoring/server/lib/errors/handle_error.js @@ -9,7 +9,7 @@ import { isKnownError, handleKnownError } from './known_errors'; import { isAuthError, handleAuthError } from './auth_errors'; export function handleError(err, req) { - req.logger.error(err); + req && req.logger && req.logger.error(err); // specially handle auth errors if (isAuthError(err)) { diff --git a/x-pack/plugins/monitoring/server/license_service.ts b/x-pack/plugins/monitoring/server/license_service.ts index 7dcdf8897f6a18..fb45abc22afa4f 100644 --- a/x-pack/plugins/monitoring/server/license_service.ts +++ b/x-pack/plugins/monitoring/server/license_service.ts @@ -46,7 +46,7 @@ export class LicenseService { license$, getMessage: () => rawLicense?.getUnavailableReason() || 'N/A', getMonitoringFeature: () => rawLicense?.getFeature('monitoring') || defaultLicenseFeature, - getWatcherFeature: () => rawLicense?.getFeature('monitoring') || defaultLicenseFeature, + getWatcherFeature: () => rawLicense?.getFeature('watcher') || defaultLicenseFeature, getSecurityFeature: () => rawLicense?.getFeature('security') || defaultLicenseFeature, stop: () => { if (licenseSubscription) { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 7c346e007da23c..5f358badde4012 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -9,8 +9,6 @@ import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { Logger, PluginInitializerContext, @@ -20,15 +18,12 @@ import { CoreSetup, ILegacyCustomClusterClient, CoreStart, - IRouter, - ILegacyClusterClient, CustomHttpResponseOptions, ResponseError, } from 'kibana/server'; import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG, - KIBANA_ALERTING_ENABLED, KIBANA_STATS_TYPE_MONITORING, } from '../common/constants'; import { MonitoringConfig, createConfig, configSchema } from './config'; @@ -41,56 +36,18 @@ import { initInfraSource } from './lib/logs/init_infra_source'; import { instantiateClient } from './es_client/instantiate_client'; import { registerCollectors } from './kibana_monitoring/collectors'; import { registerMonitoringCollection } from './telemetry_collection'; -import { LicensingPluginSetup } from '../../licensing/server'; -import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { LicenseService } from './license_service'; -import { MonitoringLicenseService } from './types'; +import { AlertsFactory } from './alerts'; import { - PluginStartContract as AlertingPluginStartContract, - PluginSetupContract as AlertingPluginSetupContract, -} from '../../alerts/server'; -import { getLicenseExpiration } from './alerts/license_expiration'; -import { getClusterState } from './alerts/cluster_state'; -import { InfraPluginSetup } from '../../infra/server'; - -export interface LegacyAPI { - getServerStatus: () => string; -} - -interface PluginsSetup { - telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; - usageCollection?: UsageCollectionSetup; - licensing: LicensingPluginSetup; - features: FeaturesPluginSetupContract; - alerts: AlertingPluginSetupContract; - infra: InfraPluginSetup; -} - -interface PluginsStart { - alerts: AlertingPluginStartContract; -} - -interface MonitoringCoreConfig { - get: (key: string) => string | undefined; -} - -interface MonitoringCore { - config: () => MonitoringCoreConfig; - log: Logger; - route: (options: any) => void; -} - -interface LegacyShimDependencies { - router: IRouter; - instanceUuid: string; - esDataClient: ILegacyClusterClient; - kibanaStatsCollector: any; -} - -interface IBulkUploader { - setKibanaStatusGetter: (getter: () => string | undefined) => void; - getKibanaStats: () => any; -} + MonitoringCore, + MonitoringLicenseService, + LegacyShimDependencies, + IBulkUploader, + PluginsSetup, + PluginsStart, + LegacyAPI, + LegacyRequest, +} from './types'; // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; @@ -131,8 +88,9 @@ export class Plugin { .pipe(first()) .toPromise(); + const router = core.http.createRouter(); this.legacyShimDependencies = { - router: core.http.createRouter(), + router, instanceUuid: core.uuid.getInstanceUuid(), esDataClient: core.elasticsearch.legacy.client, kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( @@ -158,29 +116,20 @@ export class Plugin { }); await this.licenseService.refresh(); - if (KIBANA_ALERTING_ENABLED) { - plugins.alerts.registerType( - getLicenseExpiration( - async () => { - const coreStart = (await core.getStartServices())[0]; - return coreStart.uiSettings; - }, - cluster, - this.getLogger, - config.ui.ccs.enabled - ) - ); - plugins.alerts.registerType( - getClusterState( - async () => { - const coreStart = (await core.getStartServices())[0]; - return coreStart.uiSettings; - }, - cluster, - this.getLogger, - config.ui.ccs.enabled - ) - ); + const serverInfo = core.http.getServerInfo(); + let kibanaUrl = `${serverInfo.protocol}://${serverInfo.hostname}:${serverInfo.port}`; + if (core.http.basePath.serverBasePath) { + kibanaUrl += `/${core.http.basePath.serverBasePath}`; + } + const getUiSettingsService = async () => { + const coreStart = (await core.getStartServices())[0]; + return coreStart.uiSettings; + }; + + const alerts = AlertsFactory.getAll(); + for (const alert of alerts) { + alert.initializeAlertType(getUiSettingsService, cluster, this.getLogger, config, kibanaUrl); + plugins.alerts.registerType(alert.getAlertType()); } // Initialize telemetry @@ -200,7 +149,6 @@ export class Plugin { const kibanaCollectionEnabled = config.kibana.collection.enabled; if (kibanaCollectionEnabled) { // Start kibana internal collection - const serverInfo = core.http.getServerInfo(); const bulkUploader = (this.bulkUploader = initBulkUploader({ elasticsearch: core.elasticsearch, config, @@ -252,7 +200,10 @@ export class Plugin { ); this.registerPluginInUI(plugins); - requireUIRoutes(this.monitoringCore); + requireUIRoutes(this.monitoringCore, { + router, + licenseService: this.licenseService, + }); initInfraSource(config, plugins.infra); } @@ -353,14 +304,16 @@ export class Plugin { res: KibanaResponseFactory ) => { const plugins = (await getCoreServices())[1]; - const legacyRequest = { + const legacyRequest: LegacyRequest = { ...req, logger: this.log, getLogger: this.getLogger, payload: req.body, getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector, getUiSettingsService: () => context.core.uiSettings.client, + getActionTypeRegistry: () => context.actions?.listTypes(), getAlertsClient: () => plugins.alerts.getAlertsClientWithRequest(req), + getActionsClient: () => plugins.actions.getActionsClientWithRequest(req), server: { config: legacyConfigWrapper, newPlatform: { @@ -388,7 +341,8 @@ export class Plugin { const result = await options.handler(legacyRequest); return res.ok({ body: result }); } catch (err) { - const statusCode: number = err.output?.statusCode || err.statusCode || err.status; + const statusCode: number = + err.output?.statusCode || err.statusCode || err.status || 500; if (Boom.isBoom(err) || statusCode !== 500) { return res.customError({ statusCode, body: err }); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js deleted file mode 100644 index d5a43d32f600a9..00000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/alerts.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { isFunction } from 'lodash'; -import { - ALERT_TYPE_LICENSE_EXPIRATION, - ALERT_TYPE_CLUSTER_STATE, - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - ALERT_TYPES, -} from '../../../../../common/constants'; -import { handleError } from '../../../../lib/errors'; -import { fetchStatus } from '../../../../lib/alerts/fetch_status'; - -async function createAlerts(req, alertsClient, { selectedEmailActionId }) { - const createdAlerts = []; - - // Create alerts - const ALERT_TYPES = { - [ALERT_TYPE_LICENSE_EXPIRATION]: { - schedule: { interval: '1m' }, - actions: [ - { - group: 'default', - id: selectedEmailActionId, - params: { - subject: '{{context.subject}}', - message: `{{context.message}}`, - to: ['{{context.to}}'], - }, - }, - ], - }, - [ALERT_TYPE_CLUSTER_STATE]: { - schedule: { interval: '1m' }, - actions: [ - { - group: 'default', - id: selectedEmailActionId, - params: { - subject: '{{context.subject}}', - message: `{{context.message}}`, - to: ['{{context.to}}'], - }, - }, - ], - }, - }; - - for (const alertTypeId of Object.keys(ALERT_TYPES)) { - const existingAlert = await alertsClient.find({ - options: { - search: alertTypeId, - }, - }); - if (existingAlert.total === 1) { - await alertsClient.delete({ id: existingAlert.data[0].id }); - } - - const result = await alertsClient.create({ - data: { - enabled: true, - alertTypeId, - ...ALERT_TYPES[alertTypeId], - }, - }); - createdAlerts.push(result); - } - - return createdAlerts; -} - -async function saveEmailAddress(emailAddress, uiSettingsService) { - await uiSettingsService.set(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, emailAddress); -} - -export function createKibanaAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/alerts', - config: { - validate: { - payload: schema.object({ - selectedEmailActionId: schema.string(), - emailAddress: schema.string(), - }), - }, - }, - async handler(req, headers) { - const { emailAddress, selectedEmailActionId } = req.payload; - const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; - if (!alertsClient) { - return headers.response().code(404); - } - - const [alerts, emailResponse] = await Promise.all([ - createAlerts(req, alertsClient, { ...req.params, selectedEmailActionId }), - saveEmailAddress(emailAddress, req.getUiSettingsService()), - ]); - - return { alerts, emailResponse }; - }, - }); - - server.route({ - method: 'POST', - path: '/api/monitoring/v1/alert_status', - config: { - validate: { - payload: schema.object({ - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, - }, - async handler(req, headers) { - const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; - if (!alertsClient) { - return headers.response().code(404); - } - - const start = req.payload.timeRange.min; - const end = req.payload.timeRange.max; - let alerts; - - try { - alerts = await fetchStatus(alertsClient, ALERT_TYPES, start, end, req.logger); - } catch (err) { - throw handleError(err, req); - } - - return { alerts }; - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts new file mode 100644 index 00000000000000..1d83644fce756e --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { handleError } from '../../../../lib/errors'; +import { AlertsFactory } from '../../../../alerts'; +import { RouteDependencies } from '../../../../types'; +import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; +import { ActionResult } from '../../../../../../actions/common'; +// import { fetchDefaultEmailAddress } from '../../../../lib/alerts/fetch_default_email_address'; + +const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log'; + +export function enableAlertsRoute(server: any, npRoute: RouteDependencies) { + npRoute.router.post( + { + path: '/api/monitoring/v1/alerts/enable', + options: { tags: ['access:monitoring'] }, + validate: false, + }, + async (context, request, response) => { + try { + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); + const types = context.actions?.listTypes(); + if (!alertsClient || !actionsClient || !types) { + return response.notFound(); + } + + // Get or create the default log action + let serverLogAction; + const allActions = await actionsClient.getAll(); + for (const action of allActions) { + if (action.name === DEFAULT_SERVER_LOG_NAME) { + serverLogAction = action as ActionResult; + break; + } + } + + if (!serverLogAction) { + serverLogAction = await actionsClient.create({ + action: { + name: DEFAULT_SERVER_LOG_NAME, + actionTypeId: ALERT_ACTION_TYPE_LOG, + config: {}, + secrets: {}, + }, + }); + } + + const actions = [ + { + id: serverLogAction.id, + config: {}, + }, + ]; + + const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); + const createdAlerts = await Promise.all( + alerts.map( + async (alert) => await alert.createIfDoesNotExist(alertsClient, actionsClient, actions) + ) + ); + return response.ok({ body: createdAlerts }); + } catch (err) { + throw handleError(err); + } + } + ); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js index 246cdfde97cff8..a41562dd29a886 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.js @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './legacy_alerts'; -export * from './alerts'; +export { enableAlertsRoute } from './enable'; +export { alertStatusRoute } from './status'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js deleted file mode 100644 index 688caac9b60b1b..00000000000000 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; -import { checkLicense } from '../../../../cluster_alerts/check_license'; -import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; - -/* - * Cluster Alerts route. - */ -export function legacyClusterAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/clusters/{clusterUuid}/legacy_alerts', - config: { - validate: { - params: schema.object({ - clusterUuid: schema.string(), - }), - payload: schema.object({ - ccs: schema.maybe(schema.string()), - timeRange: schema.object({ - min: schema.string(), - max: schema.string(), - }), - }), - }, - }, - handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; - const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); - const options = { - start: req.payload.timeRange.min, - end: req.payload.timeRange.max, - }; - - return getClusterLicense(req, esIndexPattern, clusterUuid).then((license) => - alertsClusterSearch( - req, - alertsIndex, - { cluster_uuid: clusterUuid, license }, - checkLicense, - options - ) - ); - }, - }); -} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts new file mode 100644 index 00000000000000..eef99bbc4ac686 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +// @ts-ignore +import { handleError } from '../../../../lib/errors'; +import { RouteDependencies } from '../../../../types'; +import { fetchStatus } from '../../../../lib/alerts/fetch_status'; +import { CommonAlertFilter } from '../../../../../common/types'; + +export function alertStatusRoute(server: any, npRoute: RouteDependencies) { + npRoute.router.post( + { + path: '/api/monitoring/v1/alert/{clusterUuid}/status', + options: { tags: ['access:monitoring'] }, + validate: { + params: schema.object({ + clusterUuid: schema.string(), + }), + body: schema.object({ + alertTypeIds: schema.maybe(schema.arrayOf(schema.string())), + filters: schema.maybe(schema.arrayOf(schema.any())), + timeRange: schema.object({ + min: schema.number(), + max: schema.number(), + }), + }), + }, + }, + async (context, request, response) => { + try { + const { clusterUuid } = request.params; + const { + alertTypeIds, + timeRange: { min, max }, + filters, + } = request.body; + const alertsClient = context.alerting?.getAlertsClient(); + if (!alertsClient) { + return response.notFound(); + } + + const status = await fetchStatus( + alertsClient, + npRoute.licenseService, + alertTypeIds, + clusterUuid, + min, + max, + filters as CommonAlertFilter[] + ); + return response.ok({ body: status }); + } catch (err) { + throw handleError(err); + } + } + ); +} diff --git a/x-pack/plugins/monitoring/server/routes/index.js b/x-pack/plugins/monitoring/server/routes/index.ts similarity index 67% rename from x-pack/plugins/monitoring/server/routes/index.js rename to x-pack/plugins/monitoring/server/routes/index.ts index 0aefed4d9a507c..69ded6ad5a5f08 100644 --- a/x-pack/plugins/monitoring/server/routes/index.js +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ +/* eslint import/namespace: ['error', { allowComputed: true }]*/ +// @ts-ignore import * as uiRoutes from './api/v1/ui'; // namespace import +import { RouteDependencies } from '../types'; -export function requireUIRoutes(server) { +export function requireUIRoutes(server: any, npRoute: RouteDependencies) { const routes = Object.keys(uiRoutes); routes.forEach((route) => { const registerRoute = uiRoutes[route]; // computed reference to module objects imported via namespace - registerRoute(server); + registerRoute(server, npRoute); }); } diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 9b3725d007fd9a..0c346c8082475b 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -4,7 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { Observable } from 'rxjs'; +import { IRouter, ILegacyClusterClient, Logger } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { LicenseFeature, ILicense } from '../../licensing/server'; +import { PluginStartContract as ActionsPluginsStartContact } from '../../actions/server'; +import { + PluginStartContract as AlertingPluginStartContract, + PluginSetupContract as AlertingPluginSetupContract, +} from '../../alerts/server'; +import { InfraPluginSetup } from '../../infra/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -15,3 +26,85 @@ export interface MonitoringLicenseService { getSecurityFeature: () => LicenseFeature; stop: () => void; } + +export interface MonitoringElasticsearchConfig { + hosts: string[]; +} + +export interface LegacyAPI { + getServerStatus: () => string; +} + +export interface PluginsSetup { + telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; + usageCollection?: UsageCollectionSetup; + licensing: LicensingPluginSetup; + features: FeaturesPluginSetupContract; + alerts: AlertingPluginSetupContract; + infra: InfraPluginSetup; +} + +export interface PluginsStart { + alerts: AlertingPluginStartContract; + actions: ActionsPluginsStartContact; +} + +export interface MonitoringCoreConfig { + get: (key: string) => string | undefined; +} + +export interface RouteDependencies { + router: IRouter; + licenseService: MonitoringLicenseService; +} + +export interface MonitoringCore { + config: () => MonitoringCoreConfig; + log: Logger; + route: (options: any) => void; +} + +export interface LegacyShimDependencies { + router: IRouter; + instanceUuid: string; + esDataClient: ILegacyClusterClient; + kibanaStatsCollector: any; +} + +export interface IBulkUploader { + setKibanaStatusGetter: (getter: () => string | undefined) => void; + getKibanaStats: () => any; +} + +export interface LegacyRequest { + logger: Logger; + getLogger: (...scopes: string[]) => Logger; + payload: unknown; + getKibanaStatsCollector: () => any; + getUiSettingsService: () => any; + getActionTypeRegistry: () => any; + getAlertsClient: () => any; + getActionsClient: () => any; + server: { + config: () => { + get: (key: string) => string | undefined; + }; + newPlatform: { + setup: { + plugins: PluginsStart; + }; + }; + plugins: { + monitoring: { + info: MonitoringLicenseService; + }; + elasticsearch: { + getCluster: ( + name: string + ) => { + callWithRequest: (req: any, endpoint: string, params: any) => Promise; + }; + }; + }; + }; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2a8365a8bc5c90..6ef8a61f932952 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10902,86 +10902,9 @@ "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "監視リクエストエラー", "xpack.monitoring.ajaxErrorHandler.requestFailedNotification.retryButtonLabel": "再試行", "xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle": "監視リクエスト失敗", - "xpack.monitoring.alertingEmailAddress.description": "スタック監視からアラートを受信するデフォルトメールアドレス", - "xpack.monitoring.alertingEmailAddress.name": "アラートメールアドレス", - "xpack.monitoring.alerts.categoryColumn.generalLabel": "一般", - "xpack.monitoring.alerts.categoryColumnTitle": "カテゴリー", - "xpack.monitoring.alerts.clusterAlertsTitle": "クラスターアラート", - "xpack.monitoring.alerts.clusterOverviewLinkLabel": "« クラスターの概要", - "xpack.monitoring.alerts.clusterState.actionGroups.default": "デフォルト", - "xpack.monitoring.alerts.clusterStatus.newSubject": "NEW X-Pack監視:クラスターステータス", - "xpack.monitoring.alerts.clusterStatus.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", - "xpack.monitoring.alerts.clusterStatus.resolvedSubject": "RESOLVED X-Pack監視:クラスターステータス", - "xpack.monitoring.alerts.clusterStatus.ui.firingMessage": "Elasticsearchクラスターステータスは{status}です。 #start_link{message}#end_link", - "xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage": "Elasticsearchクラスターステータスは緑です。", - "xpack.monitoring.alerts.clusterStatus.yellowMessage": "見つからないレプリカシャードを割り当て", - "xpack.monitoring.alerts.configuration.confirm": "確認して保存", - "xpack.monitoring.alerts.configuration.createEmailAction": "メールアクションを作成", - "xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText": "削除", - "xpack.monitoring.alerts.configuration.editConfiguration.buttonText": "編集", - "xpack.monitoring.alerts.configuration.emailAction.name": "スタック監視アラートのメールアクション", - "xpack.monitoring.alerts.configuration.emailAddressLabel": "メールアドレス", - "xpack.monitoring.alerts.configuration.newActionDropdownDisplay": "新しいメールアクションを作成...", - "xpack.monitoring.alerts.configuration.save": "保存", - "xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel": "ドキュメント", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorMessage": "{link} を参照して API キーを有効にします。", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorTitle": "Elasticsearch で API キーが有効になっていません", - "xpack.monitoring.alerts.configuration.selectAction.inputDisplay": "送信元: {from}、サービス: {service}", - "xpack.monitoring.alerts.configuration.selectEmailAction": "メールアクションを選択", - "xpack.monitoring.alerts.configuration.setEmailAddress": "アラートを受信するようにメールを設定します", - "xpack.monitoring.alerts.configuration.step1.editAction": "以下のアクションを編集してください。", - "xpack.monitoring.alerts.configuration.step1.testingError": "テストメールを送信できません。電子メール構成を再確認してください。", - "xpack.monitoring.alerts.configuration.step3.saveError": "を保存できませんでした", - "xpack.monitoring.alerts.configuration.testConfiguration.buttonText": "テスト", - "xpack.monitoring.alerts.configuration.testConfiguration.disabledTooltipText": "以下のメールアドレスを構成してこのアクションをテストします。", - "xpack.monitoring.alerts.configuration.testConfiguration.success": "こちら側からは良好に見えます。", - "xpack.monitoring.alerts.configuration.unknownError": "何か問題が発生しましたサーバーログを参照してください。", - "xpack.monitoring.alerts.filterAlertsPlaceholder": "フィルターアラート…", - "xpack.monitoring.alerts.highSeverityName": "高", - "xpack.monitoring.alerts.lastCheckedColumnTitle": "最終確認", - "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "デフォルト", - "xpack.monitoring.alerts.licenseExpiration.newSubject": "NEW X-Pack 監視:ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "RESOLVED X-Pack 監視:ライセンス期限", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "このクラスターのライセンスはアクティブです。", - "xpack.monitoring.alerts.lowSeverityName": "低", - "xpack.monitoring.alerts.mediumSeverityName": "中", - "xpack.monitoring.alerts.messageColumnTitle": "メッセージ", - "xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText": "新しいサービスを追加中...", - "xpack.monitoring.alerts.migrate.manageAction.addNewServiceText": "新しいサービスを追加...", - "xpack.monitoring.alerts.migrate.manageAction.cancelLabel": "キャンセル", - "xpack.monitoring.alerts.migrate.manageAction.createLabel": "メールアクションを作成", - "xpack.monitoring.alerts.migrate.manageAction.fromHelpText": "アラートの送信元メールアドレス", - "xpack.monitoring.alerts.migrate.manageAction.fromText": "開始:", - "xpack.monitoring.alerts.migrate.manageAction.hostHelpText": "サービスプロバイダーのホスト名", - "xpack.monitoring.alerts.migrate.manageAction.hostText": "ホスト", - "xpack.monitoring.alerts.migrate.manageAction.passwordHelpText": "サービスプロバイダーとともに使用するパスワード", - "xpack.monitoring.alerts.migrate.manageAction.passwordText": "パスワード", - "xpack.monitoring.alerts.migrate.manageAction.portHelpText": "サービスプロバイダーのポート番号", - "xpack.monitoring.alerts.migrate.manageAction.portText": "ポート", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} は必須フィールドです。", - "xpack.monitoring.alerts.migrate.manageAction.saveLabel": "メールアクションを保存", - "xpack.monitoring.alerts.migrate.manageAction.secureHelpText": "サービスプロバイダーと TLS を使用するかどうか", - "xpack.monitoring.alerts.migrate.manageAction.secureText": "セキュア", - "xpack.monitoring.alerts.migrate.manageAction.serviceHelpText": "詳細情報", - "xpack.monitoring.alerts.migrate.manageAction.serviceText": "サービス", - "xpack.monitoring.alerts.migrate.manageAction.userHelpText": "サービスプロバイダーとともに使用するユーザー", - "xpack.monitoring.alerts.migrate.manageAction.userText": "ユーザー", - "xpack.monitoring.alerts.notResolvedDescription": "未解決", - "xpack.monitoring.alerts.resolvedAgoDescription": "{duration} 前", - "xpack.monitoring.alerts.resolvedColumnTitle": "解決済み", - "xpack.monitoring.alerts.severityTitle": "{severity}深刻度アラート", - "xpack.monitoring.alerts.severityTitle.unknown": "不明", - "xpack.monitoring.alerts.severityValue.unknown": "N/A", - "xpack.monitoring.alerts.status.flyoutSubtitle": "アラートを受信するようにメールサーバーとメールアドレスを構成します。", - "xpack.monitoring.alerts.status.flyoutTitle": "監視アラート", - "xpack.monitoring.alerts.status.manage": "変更を加えますか?ここをクリック。", - "xpack.monitoring.alerts.status.needToMigrate": "クラスターアラートを新しいアラートプラットフォームに移行します。", - "xpack.monitoring.alerts.status.needToMigrateTitle": "こんにちは、アラートの改善を図りました。", - "xpack.monitoring.alerts.status.upToDate": "Kibana アラートは最新です。", - "xpack.monitoring.alerts.statusColumnTitle": "ステータス", - "xpack.monitoring.alerts.triggeredColumnTitle": "実行済み", - "xpack.monitoring.alerts.triggeredColumnValue": "{timestamp} 前", "xpack.monitoring.apm.healthStatusLabel": "ヘルス: {status}", "xpack.monitoring.apm.instance.routeTitle": "{apm} - インスタンス", "xpack.monitoring.apm.instance.status.lastEventDescription": "{timeOfLastEvent} 前", @@ -11074,12 +10997,6 @@ "xpack.monitoring.chart.screenReaderUnaccessibleTitle": "このチャートはスクリーンリーダーではアクセスできません", "xpack.monitoring.chart.seriesScreenReaderListDescription": "間隔: {bucketSize}", "xpack.monitoring.chart.timeSeries.zoomOut": "ズームアウト", - "xpack.monitoring.cluster.listing.alertsInticator.alertsTooltip": "アラート", - "xpack.monitoring.cluster.listing.alertsInticator.clearStatusTooltip": "クラスターステータスはクリアです!", - "xpack.monitoring.cluster.listing.alertsInticator.clearTooltip": "クリア", - "xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip": "クラスターにすぐに対処が必要な致命的な問題があります!", - "xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip": "クラスターに低深刻度の問題があります", - "xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip": "クラスターに影響を及ぼす可能性がある問題があります。", "xpack.monitoring.cluster.listing.dataColumnTitle": "データ", "xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel": "全機能を利用できるライセンスを取得", "xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage": "複数クラスターの監視が必要ですか?{getLicenseInfoLink} して、複数クラスターの監視をご利用ください。", @@ -11102,10 +11019,6 @@ "xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle": "Elasticsearch クラスターに接続されていないインスタンスがあるようです。", "xpack.monitoring.cluster.listing.statusColumnTitle": "ステータス", "xpack.monitoring.cluster.listing.unknownHealthMessage": "不明", - "xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText": "最終確認 {updateDateTime} ({duration} 前に実行)", - "xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle": "{severityIconTitle} ({time} 前に解決)", - "xpack.monitoring.cluster.overview.alertsPanel.topClusterTitle": "トップクラスターアラート", - "xpack.monitoring.cluster.overview.alertsPanel.viewAllButtonLabel": "すべてのアラートを表示", "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM", "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM インスタンス: {apmsTotal}", "xpack.monitoring.cluster.overview.apmPanel.lastEventDescription": "{timeOfLastEvent} 前", @@ -11156,8 +11069,6 @@ "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkAriaLabel": "Kibana の概要", "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkLabel": "概要", "xpack.monitoring.cluster.overview.kibanaPanel.requestsLabel": "リクエスト", - "xpack.monitoring.cluster.overview.licenseText.expireDateText": "の有効期限は {expiryDate} です", - "xpack.monitoring.cluster.overview.licenseText.toLicensePageLinkLabel": "{licenseType} ライセンス {willExpireOn}", "xpack.monitoring.cluster.overview.logsPanel.logTypeTitle": "{type}", "xpack.monitoring.cluster.overview.logsPanel.noLogsFound": "ログが見つかりませんでした。", "xpack.monitoring.cluster.overview.logstashPanel.betaFeatureTooltip": "ベータ機能", @@ -11371,8 +11282,6 @@ "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription": "次のインスタンスは監視されていません。\n 下の「Metricbeat で監視」をクリックして、監視を開始してください。", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle": "Kibana インスタンスが検出されました", "xpack.monitoring.kibana.listing.filterInstancesPlaceholder": "フィルターインスタンス…", - "xpack.monitoring.kibana.listing.instanceStatus.offlineLabel": "オフライン", - "xpack.monitoring.kibana.listing.instanceStatusTitle": "インスタンスステータス: {kibanaStatus}", "xpack.monitoring.kibana.listing.loadAverageColumnTitle": "平均負荷", "xpack.monitoring.kibana.listing.memorySizeColumnTitle": "メモリーサイズ", "xpack.monitoring.kibana.listing.nameColumnTitle": "名前", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 42240203a2eaf1..3c8016d64248be 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10908,86 +10908,9 @@ "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "Monitoring 请求错误", "xpack.monitoring.ajaxErrorHandler.requestFailedNotification.retryButtonLabel": "重试", "xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle": "Monitoring 请求失败", - "xpack.monitoring.alertingEmailAddress.description": "用于从 Stack Monitoring 接收告警的默认电子邮件地址", - "xpack.monitoring.alertingEmailAddress.name": "Alerting 电子邮件地址", - "xpack.monitoring.alerts.categoryColumn.generalLabel": "常规", - "xpack.monitoring.alerts.categoryColumnTitle": "类别", - "xpack.monitoring.alerts.clusterAlertsTitle": "集群告警", - "xpack.monitoring.alerts.clusterOverviewLinkLabel": "« 集群概览", - "xpack.monitoring.alerts.clusterState.actionGroups.default": "默认值", - "xpack.monitoring.alerts.clusterStatus.newSubject": "新的 X-Pack Monitoring:集群状态", - "xpack.monitoring.alerts.clusterStatus.redMessage": "分配缺失的主分片和副本分片", - "xpack.monitoring.alerts.clusterStatus.resolvedSubject": "已解决 X-Pack Monitoring:集群状态", - "xpack.monitoring.alerts.clusterStatus.ui.firingMessage": "Elasticsearch 集群状态为 {status}。#start_link{message}#end_link", - "xpack.monitoring.alerts.clusterStatus.ui.resolvedMessage": "Elasticsearch 集群状态为绿色。", - "xpack.monitoring.alerts.clusterStatus.yellowMessage": "分配缺失的副本分片", - "xpack.monitoring.alerts.configuration.confirm": "确认并保存", - "xpack.monitoring.alerts.configuration.createEmailAction": "创建电子邮件操作", - "xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText": "删除", - "xpack.monitoring.alerts.configuration.editConfiguration.buttonText": "编辑", - "xpack.monitoring.alerts.configuration.emailAction.name": "Stack Monitoring 告警的电子邮件操作", - "xpack.monitoring.alerts.configuration.emailAddressLabel": "电子邮件地址", - "xpack.monitoring.alerts.configuration.newActionDropdownDisplay": "创建新电子邮件操作......", - "xpack.monitoring.alerts.configuration.save": "保存", - "xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel": "文档", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorMessage": "请参阅 {link} 以启用 API 密钥。", - "xpack.monitoring.alerts.configuration.securityConfigurationErrorTitle": "Elasticsearch 中未启用 API 密钥", - "xpack.monitoring.alerts.configuration.selectAction.inputDisplay": "来自:{from},服务:{service}", - "xpack.monitoring.alerts.configuration.selectEmailAction": "选择电子邮件操作", - "xpack.monitoring.alerts.configuration.setEmailAddress": "设置电子邮件以接收告警", - "xpack.monitoring.alerts.configuration.step1.editAction": "在下面编辑操作。", - "xpack.monitoring.alerts.configuration.step1.testingError": "无法发送测试电子邮件。请再次检查您的电子邮件配置。", - "xpack.monitoring.alerts.configuration.step3.saveError": "无法保存", - "xpack.monitoring.alerts.configuration.testConfiguration.buttonText": "测试", - "xpack.monitoring.alerts.configuration.testConfiguration.disabledTooltipText": "请在下面配置电子邮件地址以测试此操作。", - "xpack.monitoring.alerts.configuration.testConfiguration.success": "在我们这边看起来不错!", - "xpack.monitoring.alerts.configuration.unknownError": "出问题了。请查看服务器日志。", - "xpack.monitoring.alerts.filterAlertsPlaceholder": "筛选告警……", - "xpack.monitoring.alerts.highSeverityName": "高", - "xpack.monitoring.alerts.lastCheckedColumnTitle": "上次检查时间", - "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "默认值", - "xpack.monitoring.alerts.licenseExpiration.newSubject": "新 X-Pack Monitoring:许可证到期", - "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "已解决 X-Pack Monitoring:许可证到期", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute过期。 #start_link请更新您的许可证。#end_link", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "此集群的许可证处于活动状态。", - "xpack.monitoring.alerts.lowSeverityName": "低", - "xpack.monitoring.alerts.mediumSeverityName": "中", - "xpack.monitoring.alerts.messageColumnTitle": "消息", - "xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText": "正在添加新服务......", - "xpack.monitoring.alerts.migrate.manageAction.addNewServiceText": "添加新服务......", - "xpack.monitoring.alerts.migrate.manageAction.cancelLabel": "取消", - "xpack.monitoring.alerts.migrate.manageAction.createLabel": "创建电子邮件操作", - "xpack.monitoring.alerts.migrate.manageAction.fromHelpText": "告警的发件人电子邮件地址", - "xpack.monitoring.alerts.migrate.manageAction.fromText": "发件人", - "xpack.monitoring.alerts.migrate.manageAction.hostHelpText": "服务提供商的主机名", - "xpack.monitoring.alerts.migrate.manageAction.hostText": "主机", - "xpack.monitoring.alerts.migrate.manageAction.passwordHelpText": "用于服务提供商的密码", - "xpack.monitoring.alerts.migrate.manageAction.passwordText": "密码", - "xpack.monitoring.alerts.migrate.manageAction.portHelpText": "服务提供商的端口号", - "xpack.monitoring.alerts.migrate.manageAction.portText": "端口", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} 是必填字段。", - "xpack.monitoring.alerts.migrate.manageAction.saveLabel": "保存电子邮件操作", - "xpack.monitoring.alerts.migrate.manageAction.secureHelpText": "是否将 TLS 用于服务提供商", - "xpack.monitoring.alerts.migrate.manageAction.secureText": "安全", - "xpack.monitoring.alerts.migrate.manageAction.serviceHelpText": "了解详情", - "xpack.monitoring.alerts.migrate.manageAction.serviceText": "服务", - "xpack.monitoring.alerts.migrate.manageAction.userHelpText": "用于服务提供商的用户", - "xpack.monitoring.alerts.migrate.manageAction.userText": "用户", - "xpack.monitoring.alerts.notResolvedDescription": "未解决", - "xpack.monitoring.alerts.resolvedAgoDescription": "{duration}前", - "xpack.monitoring.alerts.resolvedColumnTitle": "已解决", - "xpack.monitoring.alerts.severityTitle": "{severity}紧急告警", - "xpack.monitoring.alerts.severityTitle.unknown": "未知", - "xpack.monitoring.alerts.severityValue.unknown": "不可用", - "xpack.monitoring.alerts.status.flyoutSubtitle": "配置电子邮件服务器和电子邮件地址以接收告警。", - "xpack.monitoring.alerts.status.flyoutTitle": "Monitoring 告警", - "xpack.monitoring.alerts.status.manage": "想要进行更改?单击此处。", - "xpack.monitoring.alerts.status.needToMigrate": "将集群告警迁移到我们新的告警平台。", - "xpack.monitoring.alerts.status.needToMigrateTitle": "嘿!我们已优化 Alerting!", - "xpack.monitoring.alerts.status.upToDate": "Kibana Alerting 与时俱进!", - "xpack.monitoring.alerts.statusColumnTitle": "状态", - "xpack.monitoring.alerts.triggeredColumnTitle": "已触发", - "xpack.monitoring.alerts.triggeredColumnValue": "{timestamp}前", "xpack.monitoring.apm.healthStatusLabel": "运行状况:{status}", "xpack.monitoring.apm.instance.routeTitle": "{apm} - 实例", "xpack.monitoring.apm.instance.status.lastEventDescription": "{timeOfLastEvent}前", @@ -11080,12 +11003,6 @@ "xpack.monitoring.chart.screenReaderUnaccessibleTitle": "此图表不支持屏幕阅读器读取", "xpack.monitoring.chart.seriesScreenReaderListDescription": "时间间隔:{bucketSize}", "xpack.monitoring.chart.timeSeries.zoomOut": "缩小", - "xpack.monitoring.cluster.listing.alertsInticator.alertsTooltip": "告警", - "xpack.monitoring.cluster.listing.alertsInticator.clearStatusTooltip": "集群状态正常!", - "xpack.monitoring.cluster.listing.alertsInticator.clearTooltip": "清除", - "xpack.monitoring.cluster.listing.alertsInticator.highSeverityTooltip": "有一些紧急集群问题需要您立即关注!", - "xpack.monitoring.cluster.listing.alertsInticator.lowSeverityTooltip": "存在一些低紧急集群问题", - "xpack.monitoring.cluster.listing.alertsInticator.mediumSeverityTooltip": "有一些问题可能影响您的集群。", "xpack.monitoring.cluster.listing.dataColumnTitle": "数据", "xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel": "获取具有完整功能的许可证", "xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage": "需要监测多个集群?{getLicenseInfoLink}以实现多集群监测。", @@ -11108,10 +11025,6 @@ "xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle": "似乎您具有未连接到 Elasticsearch 集群的实例。", "xpack.monitoring.cluster.listing.statusColumnTitle": "状态", "xpack.monitoring.cluster.listing.unknownHealthMessage": "未知", - "xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText": "上次检查时间是 {updateDateTime}(触发于 {duration}前)", - "xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle": "{severityIconTitle}(已在 {time}前解决)", - "xpack.monitoring.cluster.overview.alertsPanel.topClusterTitle": "最亟需处理的集群告警", - "xpack.monitoring.cluster.overview.alertsPanel.viewAllButtonLabel": "查看所有告警", "xpack.monitoring.cluster.overview.apmPanel.apmTitle": "APM", "xpack.monitoring.cluster.overview.apmPanel.instancesTotalLinkAriaLabel": "APM 实例:{apmsTotal}", "xpack.monitoring.cluster.overview.apmPanel.lastEventDescription": "{timeOfLastEvent}前", @@ -11162,8 +11075,6 @@ "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkAriaLabel": "Kibana 概览", "xpack.monitoring.cluster.overview.kibanaPanel.overviewLinkLabel": "概览", "xpack.monitoring.cluster.overview.kibanaPanel.requestsLabel": "请求", - "xpack.monitoring.cluster.overview.licenseText.expireDateText": "将于 {expiryDate}过期", - "xpack.monitoring.cluster.overview.licenseText.toLicensePageLinkLabel": "{licenseType}许可{willExpireOn}", "xpack.monitoring.cluster.overview.logsPanel.logTypeTitle": "{type}", "xpack.monitoring.cluster.overview.logsPanel.noLogsFound": "未找到任何日志。", "xpack.monitoring.cluster.overview.logstashPanel.betaFeatureTooltip": "公测版功能", @@ -11377,8 +11288,6 @@ "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription": "以下实例未受监测。\n 单击下面的“使用 Metricbeat 监测”以开始监测。", "xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle": "检测到 Kibana 实例", "xpack.monitoring.kibana.listing.filterInstancesPlaceholder": "筛选实例……", - "xpack.monitoring.kibana.listing.instanceStatus.offlineLabel": "脱机", - "xpack.monitoring.kibana.listing.instanceStatusTitle": "实例状态:{kibanaStatus}", "xpack.monitoring.kibana.listing.loadAverageColumnTitle": "负载平均值", "xpack.monitoring.kibana.listing.memorySizeColumnTitle": "内存大小", "xpack.monitoring.kibana.listing.nameColumnTitle": "名称", diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index a0e8f3583ac433..55653f49001b9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -10,6 +10,7 @@ import { Plugin } from './plugin'; export { AlertsContextProvider } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; +export { AlertEdit } from './application/sections'; export { ActionForm } from './application/sections/action_connector_form'; export { AlertAction, 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 50614ca64bbd5d..b7c3aee5471d73 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 @@ -107,7 +107,8 @@ "clusterMeta": { "enabled": false, "message": "Cluster [clustertwo] license type [basic] does not support Cluster Alerts" - } + }, + "list": {} }, "isPrimary": false, "status": "green", @@ -219,10 +220,7 @@ "alertsMeta": { "enabled": true }, - "count": 1, - "low": 0, - "medium": 1, - "high": 0 + "list": {} }, "isPrimary": false, "status": "yellow", @@ -333,7 +331,8 @@ "alerts": { "alertsMeta": { "enabled": true - } + }, + "list": {} }, "isPrimary": false, "status": "green", diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json index 49e80b244f7606..15ff9054789337 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/overview.json @@ -114,22 +114,6 @@ "total": null } }, - "alerts": [{ - "metadata": { - "severity": 1100, - "cluster_uuid": "y1qOsQPiRrGtmdEuM3APJw", - "version_created": 6000026, - "watch": "elasticsearch_cluster_status", - "link": "elasticsearch/indices", - "alert_index": ".monitoring-alerts-6", - "type": "monitoring" - }, - "update_timestamp": "2017-08-23T21:45:31.882Z", - "prefix": "Elasticsearch cluster status is yellow.", - "message": "Allocate missing replica shards.", - "resolved_timestamp": "2017-08-23T21:45:31.882Z", - "timestamp": "2017-08-23T21:28:25.639Z" - }], "isCcrEnabled": true, "isPrimary": true, "status": "green" diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json index 802bd0c7fcd74e..f0fe8c152b49fe 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json @@ -45,8 +45,5 @@ "total": 0 } }, - "alerts": { - "message": "Cluster Alerts are not displayed because the [production] cluster's license could not be determined." - }, "isPrimary": false }] 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 68cfe51fbcb95d..f938479578801a 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 @@ -107,7 +107,8 @@ "clusterMeta": { "enabled": false, "message": "Cluster [monitoring] license type [basic] does not support Cluster Alerts" - } + }, + "list": {} }, "isPrimary": true, "status": "yellow", @@ -174,7 +175,8 @@ "clusterMeta": { "enabled": false, "message": "Cluster [] license type [undefined] does not support Cluster Alerts" - } + }, + "list": {} }, "isPrimary": false, "isCcrEnabled": false diff --git a/x-pack/test/functional/apps/monitoring/cluster/alerts.js b/x-pack/test/functional/apps/monitoring/cluster/alerts.js deleted file mode 100644 index 2636fc50280682..00000000000000 --- a/x-pack/test/functional/apps/monitoring/cluster/alerts.js +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { getLifecycleMethods } from '../_get_lifecycle_methods'; - -const HIGH_ALERT_MESSAGE = 'High severity alert'; -const MEDIUM_ALERT_MESSAGE = 'Medium severity alert'; -const LOW_ALERT_MESSAGE = 'Low severity alert'; - -export default function ({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['monitoring', 'header']); - const overview = getService('monitoringClusterOverview'); - const alerts = getService('monitoringClusterAlerts'); - const indices = getService('monitoringElasticsearchIndices'); - - describe('Cluster alerts', () => { - describe('cluster has single alert', () => { - const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); - - before(async () => { - await setup('monitoring/singlecluster-yellow-platinum', { - from: 'Aug 29, 2017 @ 17:23:47.528', - to: 'Aug 29, 2017 @ 17:25:50.701', - }); - - // ensure cluster alerts are shown on overview - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - - after(async () => { - await tearDown(); - }); - - it('in alerts panel, a single medium alert is shown', async () => { - const clusterAlerts = await alerts.getOverviewAlerts(); - await new Promise((r) => setTimeout(r, 10000)); - expect(clusterAlerts.length).to.be(1); - - const { alertIcon, alertText } = await alerts.getOverviewAlert(0); - expect(alertIcon).to.be(MEDIUM_ALERT_MESSAGE); - expect(alertText).to.be( - 'Elasticsearch cluster status is yellow. Allocate missing replica shards.' - ); - }); - }); - - describe('cluster has 10 alerts', () => { - const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); - - before(async () => { - await setup('monitoring/singlecluster-yellow-platinum--with-10-alerts', { - from: 'Aug 29, 2017 @ 17:23:47.528', - to: 'Aug 29, 2017 @ 17:25:50.701', - }); - - // ensure cluster alerts are shown on overview - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - - after(async () => { - await tearDown(); - }); - - it('in alerts panel, top 3 alerts are shown', async () => { - const clusterAlerts = await alerts.getOverviewAlerts(); - expect(clusterAlerts.length).to.be(3); - - // check the all data in the panel - const panelData = [ - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'One cannot step twice in the same river. Heraclitus (ca. 540 – ca. 480 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: 'Quality is not an act, it is a habit. Aristotle (384-322 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'Life contains but two tragedies. One is not to get your heart’s desire; the other is to get it. Socrates (470-399 BCE)', - }, - ]; - - const alertsAll = await alerts.getOverviewAlertsAll(); - - alertsAll.forEach((obj, index) => { - expect(alertsAll[index].alertIcon).to.be(panelData[index].alertIcon); - expect(alertsAll[index].alertText).to.be(panelData[index].alertText); - }); - }); - - it('in alerts table view, all alerts are shown', async () => { - await alerts.clickViewAll(); - expect(await alerts.isOnListingPage()).to.be(true); - - // Check the all data in the table - const tableData = [ - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'One cannot step twice in the same river. Heraclitus (ca. 540 – ca. 480 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: 'Quality is not an act, it is a habit. Aristotle (384-322 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'Life contains but two tragedies. One is not to get your heart’s desire; the other is to get it. Socrates (470-399 BCE)', - }, - { - alertIcon: HIGH_ALERT_MESSAGE, - alertText: - 'The owl of Minerva spreads its wings only with the falling of the dusk. G.W.F. Hegel (1770 – 1831)', - }, - { - alertIcon: MEDIUM_ALERT_MESSAGE, - alertText: - 'We live in the best of all possible worlds. Gottfried Wilhelm Leibniz (1646 – 1716)', - }, - { - alertIcon: MEDIUM_ALERT_MESSAGE, - alertText: - 'To be is to be perceived (Esse est percipi). Bishop George Berkeley (1685 – 1753)', - }, - { - alertIcon: MEDIUM_ALERT_MESSAGE, - alertText: 'I think therefore I am. René Descartes (1596 – 1650)', - }, - { - alertIcon: LOW_ALERT_MESSAGE, - alertText: - 'The life of man [is] solitary, poor, nasty, brutish, and short. Thomas Hobbes (1588 – 1679)', - }, - { - alertIcon: LOW_ALERT_MESSAGE, - alertText: - 'Entities should not be multiplied unnecessarily. William of Ockham (1285 - 1349?)', - }, - { - alertIcon: LOW_ALERT_MESSAGE, - alertText: 'The unexamined life is not worth living. Socrates (470-399 BCE)', - }, - ]; - - // In some environments, with Elasticsearch 7, the cluster's status goes yellow, which makes - // this test flakey, as there is occasionally an unexpected alert about this. So, we'll ignore - // that one. - const alertsAll = Array.from(await alerts.getTableAlertsAll()).filter( - ({ alertText }) => !alertText.includes('status is yellow') - ); - expect(alertsAll.length).to.be(tableData.length); - - alertsAll.forEach((obj, index) => { - expect(`${alertsAll[index].alertIcon} ${alertsAll[index].alertText}`).to.be( - `${tableData[index].alertIcon} ${tableData[index].alertText}` - ); - }); - - await PageObjects.monitoring.clickBreadcrumb('~breadcrumbClusters'); - }); - }); - - describe('alert actions take you to the elasticsearch indices listing', () => { - const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); - - before(async () => { - await setup('monitoring/singlecluster-yellow-platinum', { - from: 'Aug 29, 2017 @ 17:23:47.528', - to: 'Aug 29, 2017 @ 17:25:50.701', - }); - - // ensure cluster alerts are shown on overview - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - - after(async () => { - await tearDown(); - }); - - it('with alert on overview', async () => { - const { alertAction } = await alerts.getOverviewAlert(0); - await alertAction.click(); - expect(await indices.isOnListing()).to.be(true); - - await PageObjects.monitoring.clickBreadcrumb('~breadcrumbClusters'); - }); - - it('with alert on listing table page', async () => { - await alerts.clickViewAll(); - expect(await alerts.isOnListingPage()).to.be(true); - - const { alertAction } = await alerts.getTableAlert(0); - await alertAction.click(); - expect(await indices.isOnListing()).to.be(true); - - await PageObjects.monitoring.clickBreadcrumb('~breadcrumbClusters'); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/monitoring/cluster/overview.js b/x-pack/test/functional/apps/monitoring/cluster/overview.js index 3396426e953808..0e608e9a055fa4 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/overview.js +++ b/x-pack/test/functional/apps/monitoring/cluster/overview.js @@ -25,10 +25,6 @@ export default function ({ getService, getPageObjects }) { await tearDown(); }); - it('shows alerts panel, because there are resolved alerts in the time range', async () => { - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - it('elasticsearch panel has no ML line, because license is Gold', async () => { expect(await overview.doesEsMlJobsExist()).to.be(false); }); @@ -80,10 +76,6 @@ export default function ({ getService, getPageObjects }) { await tearDown(); }); - it('shows alerts panel, because cluster status is Yellow', async () => { - expect(await overview.doesClusterAlertsExist()).to.be(true); - }); - it('elasticsearch panel has ML, because license is Platinum', async () => { expect(await overview.getEsMlJobs()).to.be('0'); }); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 77ca4087da13a9..c383d8593a4fac 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -12,7 +12,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); - loadTestFile(require.resolve('./cluster/alerts')); // loadTestFile(require.resolve('./cluster/license')); loadTestFile(require.resolve('./elasticsearch/overview')); diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js index 8b0ddda8859b86..0cae469e016974 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js @@ -19,12 +19,12 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject const SUBJ_SEARCH_BAR = `${SUBJ_TABLE_CONTAINER} > monitoringTableToolBar`; const SUBJ_TABLE_SORT_NAME_COL = `tableHeaderCell_name_0`; - const SUBJ_TABLE_SORT_STATUS_COL = `tableHeaderCell_isOnline_1`; - const SUBJ_TABLE_SORT_SHARDS_COL = `tableHeaderCell_shardCount_2`; - const SUBJ_TABLE_SORT_CPU_COL = `tableHeaderCell_node_cpu_utilization_3`; - const SUBJ_TABLE_SORT_LOAD_COL = `tableHeaderCell_node_load_average_4`; - const SUBJ_TABLE_SORT_MEM_COL = `tableHeaderCell_node_jvm_mem_percent_5`; - const SUBJ_TABLE_SORT_DISK_COL = `tableHeaderCell_node_free_space_6`; + const SUBJ_TABLE_SORT_STATUS_COL = `tableHeaderCell_isOnline_2`; + const SUBJ_TABLE_SORT_SHARDS_COL = `tableHeaderCell_shardCount_3`; + const SUBJ_TABLE_SORT_CPU_COL = `tableHeaderCell_node_cpu_utilization_4`; + const SUBJ_TABLE_SORT_LOAD_COL = `tableHeaderCell_node_load_average_5`; + const SUBJ_TABLE_SORT_MEM_COL = `tableHeaderCell_node_jvm_mem_percent_6`; + const SUBJ_TABLE_SORT_DISK_COL = `tableHeaderCell_node_free_space_7`; const SUBJ_TABLE_BODY = 'elasticsearchNodesTableContainer'; const SUBJ_NODES_NAMES = `${SUBJ_TABLE_BODY} > name`; From 8ecbb25ab5ea15f9573536bb17db41b7988a8186 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Tue, 14 Jul 2020 15:57:22 -0600 Subject: [PATCH 20/26] [expressions] AST Builder (#64395) --- ...blic.esaggsexpressionfunctiondefinition.md | 11 + .../kibana-plugin-plugins-data-public.md | 1 + ...rver.esaggsexpressionfunctiondefinition.md | 11 + .../kibana-plugin-plugins-data-server.md | 1 + src/plugins/data/common/index.ts | 1 + .../data/common/search/expressions/esaggs.ts | 43 ++ .../data/common/search/expressions/index.ts | 20 + src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 13 +- .../data/public/search/expressions/esaggs.ts | 23 +- src/plugins/data/server/index.ts | 2 +- src/plugins/data/server/server.api.md | 11 + .../common/ast/build_expression.test.ts | 386 +++++++++++++++++ .../common/ast/build_expression.ts | 169 ++++++++ .../common/ast/build_function.test.ts | 399 ++++++++++++++++++ .../expressions/common/ast/build_function.ts | 243 +++++++++++ .../expressions/common/ast/format.test.ts | 18 +- src/plugins/expressions/common/ast/format.ts | 10 +- .../common/ast/format_expression.test.ts | 39 ++ .../common/ast/format_expression.ts | 30 ++ src/plugins/expressions/common/ast/index.ts | 9 +- .../expressions/common/ast/parse.test.ts | 6 + src/plugins/expressions/common/ast/parse.ts | 8 +- .../common/ast/parse_expression.ts | 2 +- .../common/expression_functions/specs/clog.ts | 4 +- .../common/expression_functions/specs/font.ts | 4 +- .../common/expression_functions/specs/var.ts | 7 +- .../expression_functions/specs/var_set.ts | 9 +- .../common/expression_functions/types.ts | 33 +- src/plugins/expressions/public/index.ts | 6 + src/plugins/expressions/server/index.ts | 6 + 31 files changed, 1478 insertions(+), 49 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md create mode 100644 src/plugins/data/common/search/expressions/esaggs.ts create mode 100644 src/plugins/data/common/search/expressions/index.ts create mode 100644 src/plugins/expressions/common/ast/build_expression.test.ts create mode 100644 src/plugins/expressions/common/ast/build_expression.ts create mode 100644 src/plugins/expressions/common/ast/build_function.test.ts create mode 100644 src/plugins/expressions/common/ast/build_function.ts create mode 100644 src/plugins/expressions/common/ast/format_expression.test.ts create mode 100644 src/plugins/expressions/common/ast/format_expression.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md new file mode 100644 index 00000000000000..6cf05dde276270 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) + +## EsaggsExpressionFunctionDefinition type + +Signature: + +```typescript +export declare type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input, Arguments, Output>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 7cb6ef64431bf6..4852ad15781c74 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -125,6 +125,7 @@ | [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | +| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | | [ExistsFilter](./kibana-plugin-plugins-data-public.existsfilter.md) | | | [FieldFormatId](./kibana-plugin-plugins-data-public.fieldformatid.md) | id type is needed for creating custom converters. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md new file mode 100644 index 00000000000000..572c4e0c1eb2fe --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) + +## EsaggsExpressionFunctionDefinition type + +Signature: + +```typescript +export declare type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input, Arguments, Output>; +``` 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 9adefda7183388..6bf481841f3347 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 @@ -69,6 +69,7 @@ | Type Alias | Description | | --- | --- | +| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) | | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 0fb45fcc739d45..ca6bc965d48c53 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -26,5 +26,6 @@ export * from './kbn_field_types'; export * from './query'; export * from './search'; export * from './search/aggs'; +export * from './search/expressions'; export * from './types'; export * from './utils'; diff --git a/src/plugins/data/common/search/expressions/esaggs.ts b/src/plugins/data/common/search/expressions/esaggs.ts new file mode 100644 index 00000000000000..2957512886b4d8 --- /dev/null +++ b/src/plugins/data/common/search/expressions/esaggs.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + KibanaContext, + KibanaDatatable, + ExpressionFunctionDefinition, +} from '../../../../../plugins/expressions/common'; + +type Input = KibanaContext | null; +type Output = Promise; + +interface Arguments { + index: string; + metricsAtAllLevels: boolean; + partialRows: boolean; + includeFormatHints: boolean; + aggConfigs: string; + timeFields?: string[]; +} + +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'esaggs', + Input, + Arguments, + Output +>; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts new file mode 100644 index 00000000000000..f1a39a83836299 --- /dev/null +++ b/src/plugins/data/common/search/expressions/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './esaggs'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 2efd1c82aae793..6328e694193c94 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -313,7 +313,7 @@ import { toAbsoluteDates, } from '../common'; -export { ParsedInterval } from '../common'; +export { EsaggsExpressionFunctionDefinition, ParsedInterval } from '../common'; export { // aggs diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0c23ba340304f6..cd3fff010c0537 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -52,6 +52,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; +import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; @@ -145,7 +146,7 @@ import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { RequestAdapter } from 'src/plugins/inspector/common'; -import { RequestStatistics } from 'src/plugins/inspector/common'; +import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; @@ -180,6 +181,7 @@ import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; +import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { UserProvidedValues } from 'src/core/server/types'; @@ -425,6 +427,15 @@ export enum ES_FIELD_TYPES { // @public (undocumented) export const ES_SEARCH_STRATEGY = "es"; +// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDefinition" 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 +// Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Output" needs to be exported by the entry point index.d.ts +// 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, Arguments, Output>; + // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 4ac6c823d2e3b7..b01f17762b2bee 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -19,12 +19,8 @@ import { get, hasIn } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - KibanaContext, - KibanaDatatable, - ExpressionFunctionDefinition, - KibanaDatatableColumn, -} from 'src/plugins/expressions/public'; + +import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; import { calculateObjectHash } from '../../../../../plugins/kibana_utils/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -34,6 +30,7 @@ import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; import { calculateBounds, + EsaggsExpressionFunctionDefinition, Filter, getTime, IIndexPattern, @@ -71,18 +68,6 @@ export interface RequestHandlerParams { const name = 'esaggs'; -type Input = KibanaContext | null; -type Output = Promise; - -interface Arguments { - index: string; - metricsAtAllLevels: boolean; - partialRows: boolean; - includeFormatHints: boolean; - aggConfigs: string; - timeFields?: string[]; -} - const handleCourierRequest = async ({ searchSource, aggs, @@ -244,7 +229,7 @@ const handleCourierRequest = async ({ return (searchSource as any).tabifiedResponse; }; -export const esaggs = (): ExpressionFunctionDefinition => ({ +export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ name, type: 'kibana_datatable', inputTypes: ['kibana_context', 'null'], diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 321bd913ce760a..461b21e1cc980c 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -161,7 +161,7 @@ import { toAbsoluteDates, } from '../common'; -export { ParsedInterval } from '../common'; +export { EsaggsExpressionFunctionDefinition, ParsedInterval } from '../common'; export { ISearchStrategy, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 88f2cc3264c6e6..4dc60056ed9184 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -39,6 +39,7 @@ import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { ErrorToastOptions } from 'src/core/public/notifications'; +import { EventEmitter } from 'events'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; @@ -146,6 +147,7 @@ import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; +import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { Url } from 'url'; @@ -220,6 +222,15 @@ export enum ES_FIELD_TYPES { _TYPE = "_type" } +// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDefinition" 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 +// Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Output" needs to be exported by the entry point index.d.ts +// 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, Arguments, Output>; + // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/expressions/common/ast/build_expression.test.ts b/src/plugins/expressions/common/ast/build_expression.test.ts new file mode 100644 index 00000000000000..657b9d3bdda289 --- /dev/null +++ b/src/plugins/expressions/common/ast/build_expression.test.ts @@ -0,0 +1,386 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstExpression } from './types'; +import { buildExpression, isExpressionAstBuilder, isExpressionAst } from './build_expression'; +import { buildExpressionFunction, ExpressionAstFunctionBuilder } from './build_function'; +import { format } from './format'; + +describe('isExpressionAst()', () => { + test('returns true when a valid AST is provided', () => { + const ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: {}, + }, + ], + }; + expect(isExpressionAst(ast)).toBe(true); + }); + + test('returns false when a invalid value is provided', () => { + const invalidValues = [ + buildExpression('hello | world'), + false, + null, + undefined, + 'hi', + { type: 'unknown' }, + {}, + ]; + + invalidValues.forEach((value) => { + expect(isExpressionAst(value)).toBe(false); + }); + }); +}); + +describe('isExpressionAstBuilder()', () => { + test('returns true when a valid builder is provided', () => { + const builder = buildExpression('hello | world'); + expect(isExpressionAstBuilder(builder)).toBe(true); + }); + + test('returns false when a invalid value is provided', () => { + const invalidValues = [ + buildExpressionFunction('myFn', {}), + false, + null, + undefined, + 'hi', + { type: 'unknown' }, + {}, + ]; + + invalidValues.forEach((value) => { + expect(isExpressionAstBuilder(value)).toBe(false); + }); + }); +}); + +describe('buildExpression()', () => { + let ast: ExpressionAstExpression; + let str: string; + + beforeEach(() => { + ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: { + bar: ['baz'], + subexp: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'hello', + arguments: { + world: [false, true], + }, + }, + ], + }, + ], + }, + }, + ], + }; + str = format(ast, 'expression'); + }); + + test('accepts an expression AST as input', () => { + ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: { + bar: ['baz'], + }, + }, + ], + }; + const exp = buildExpression(ast); + expect(exp.toAst()).toEqual(ast); + }); + + test('converts subexpressions in provided AST to expression builder instances', () => { + const exp = buildExpression(ast); + expect(isExpressionAstBuilder(exp.functions[0].getArgument('subexp')![0])).toBe(true); + }); + + test('accepts an expresssion string as input', () => { + const exp = buildExpression(str); + expect(exp.toAst()).toEqual(ast); + }); + + test('accepts an array of function builders as input', () => { + const firstFn = ast.chain[0]; + const exp = buildExpression([ + buildExpressionFunction(firstFn.function, firstFn.arguments), + buildExpressionFunction('hiya', {}), + ]); + expect(exp.toAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "bar": Array [ + "baz", + ], + "subexp": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "foo", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "hiya", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + describe('functions', () => { + test('returns an array of buildExpressionFunctions', () => { + const exp = buildExpression(ast); + expect(exp.functions).toHaveLength(1); + expect(exp.functions.map((f) => f.name)).toEqual(['foo']); + }); + + test('functions.push() adds new function to the AST', () => { + const exp = buildExpression(ast); + const fn = buildExpressionFunction('test', { abc: [123] }); + exp.functions.push(fn); + expect(exp.toAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "bar": Array [ + "baz", + ], + "subexp": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "foo", + "type": "function", + }, + Object { + "arguments": Object { + "abc": Array [ + 123, + ], + }, + "function": "test", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + test('functions can be reordered', () => { + const exp = buildExpression(ast); + const fn = buildExpressionFunction('test', { abc: [123] }); + exp.functions.push(fn); + expect(exp.functions.map((f) => f.name)).toEqual(['foo', 'test']); + const testFn = exp.functions[1]; + exp.functions[1] = exp.functions[0]; + exp.functions[0] = testFn; + expect(exp.functions.map((f) => f.name)).toEqual(['test', 'foo']); + const barFn = buildExpressionFunction('bar', {}); + const fooFn = exp.functions[1]; + exp.functions[1] = barFn; + exp.functions[2] = fooFn; + expect(exp.functions.map((f) => f.name)).toEqual(['test', 'bar', 'foo']); + }); + + test('functions can be removed', () => { + const exp = buildExpression(ast); + const fn = buildExpressionFunction('test', { abc: [123] }); + exp.functions.push(fn); + expect(exp.functions.map((f) => f.name)).toEqual(['foo', 'test']); + exp.functions.shift(); + expect(exp.functions.map((f) => f.name)).toEqual(['test']); + }); + }); + + describe('#toAst', () => { + test('generates the AST for an expression', () => { + const exp = buildExpression('foo | bar hello=true hello=false'); + expect(exp.toAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "foo", + "type": "function", + }, + Object { + "arguments": Object { + "hello": Array [ + true, + false, + ], + }, + "function": "bar", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + test('throws when called on an expression with no functions', () => { + ast.chain = []; + const exp = buildExpression(ast); + expect(() => { + exp.toAst(); + }).toThrowError(); + }); + }); + + describe('#toString', () => { + test('generates an expression string from the AST', () => { + const exp = buildExpression(ast); + expect(exp.toString()).toMatchInlineSnapshot( + `"foo bar=\\"baz\\" subexp={hello world=false world=true}"` + ); + }); + + test('throws when called on an expression with no functions', () => { + ast.chain = []; + const exp = buildExpression(ast); + expect(() => { + exp.toString(); + }).toThrowError(); + }); + }); + + describe('#findFunction', () => { + test('finds a function by name', () => { + const exp = buildExpression(`where | is | waldo`); + const fns: ExpressionAstFunctionBuilder[] = exp.findFunction('waldo'); + expect(fns.map((fn) => fn.toAst())).toMatchInlineSnapshot(` + Array [ + Object { + "arguments": Object {}, + "function": "waldo", + "type": "function", + }, + ] + `); + }); + + test('recursively finds nested subexpressions', () => { + const exp = buildExpression( + `miss | miss sub={miss} | miss sub={hit sub={miss sub={hit sub={hit}}}} sub={miss}` + ); + const fns: ExpressionAstFunctionBuilder[] = exp.findFunction('hit'); + expect(fns.map((fn) => fn.name)).toMatchInlineSnapshot(` + Array [ + "hit", + "hit", + "hit", + ] + `); + }); + + test('retains references back to the original expression so you can perform migrations', () => { + const before = ` + foo sub={baz | bar a=1 sub={foo}} + | bar a=1 + | baz sub={bar a=1 c=4 sub={bar a=1 c=5}} + `; + + // Migrates all `bar` functions in the expression + const exp = buildExpression(before); + exp.findFunction('bar').forEach((fn) => { + const arg = fn.getArgument('a'); + if (arg) { + fn.replaceArgument('a', [1, 2]); + fn.addArgument('b', 3); + fn.removeArgument('c'); + } + }); + + expect(exp.toString()).toMatchInlineSnapshot(` + "foo sub={baz | bar a=1 a=2 sub={foo} b=3} + | bar a=1 a=2 b=3 + | baz sub={bar a=1 a=2 sub={bar a=1 a=2 b=3} b=3}" + `); + }); + + test('returns any subexpressions as expression builder instances', () => { + const exp = buildExpression( + `miss | miss sub={miss} | miss sub={hit sub={miss sub={hit sub={hit}}}} sub={miss}` + ); + const fns: ExpressionAstFunctionBuilder[] = exp.findFunction('hit'); + const subexpressionArgs = fns.map((fn) => + fn.getArgument('sub')?.map((arg) => isExpressionAstBuilder(arg)) + ); + expect(subexpressionArgs).toEqual([undefined, [true], [true]]); + }); + }); +}); diff --git a/src/plugins/expressions/common/ast/build_expression.ts b/src/plugins/expressions/common/ast/build_expression.ts new file mode 100644 index 00000000000000..b0a560600883ac --- /dev/null +++ b/src/plugins/expressions/common/ast/build_expression.ts @@ -0,0 +1,169 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnyExpressionFunctionDefinition } from '../expression_functions/types'; +import { ExpressionAstExpression, ExpressionAstFunction } from './types'; +import { + buildExpressionFunction, + ExpressionAstFunctionBuilder, + InferFunctionDefinition, +} from './build_function'; +import { format } from './format'; +import { parse } from './parse'; + +/** + * Type guard that checks whether a given value is an + * `ExpressionAstExpressionBuilder`. This is useful when working + * with subexpressions, where you might be retrieving a function + * argument, and need to know whether it is an expression builder + * instance which you can perform operations on. + * + * @example + * const arg = myFunction.getArgument('foo'); + * if (isExpressionAstBuilder(foo)) { + * foo.toAst(); + * } + * + * @param val Value you want to check. + * @return boolean + */ +export function isExpressionAstBuilder(val: any): val is ExpressionAstExpressionBuilder { + return val?.type === 'expression_builder'; +} + +/** @internal */ +export function isExpressionAst(val: any): val is ExpressionAstExpression { + return val?.type === 'expression'; +} + +export interface ExpressionAstExpressionBuilder { + /** + * Used to identify expression builder objects. + */ + type: 'expression_builder'; + /** + * Array of each of the `buildExpressionFunction()` instances + * in this expression. Use this to remove or reorder functions + * in the expression. + */ + functions: ExpressionAstFunctionBuilder[]; + /** + * Recursively searches expression for all ocurrences of the + * function, including in subexpressions. + * + * Useful when performing migrations on a specific function, + * as you can iterate over the array of references and update + * all functions at once. + * + * @param fnName Name of the function to search for. + * @return `ExpressionAstFunctionBuilder[]` + */ + findFunction: ( + fnName: InferFunctionDefinition['name'] + ) => Array> | []; + /** + * Converts expression to an AST. + * + * @return `ExpressionAstExpression` + */ + toAst: () => ExpressionAstExpression; + /** + * Converts expression to an expression string. + * + * @return `string` + */ + toString: () => string; +} + +const generateExpressionAst = (fns: ExpressionAstFunctionBuilder[]): ExpressionAstExpression => ({ + type: 'expression', + chain: fns.map((fn) => fn.toAst()), +}); + +/** + * Makes it easy to progressively build, update, and traverse an + * expression AST. You can either start with an empty AST, or + * provide an expression string, AST, or array of expression + * function builders to use as initial state. + * + * @param initialState Optional. An expression string, AST, or array of `ExpressionAstFunctionBuilder[]`. + * @return `this` + */ +export function buildExpression( + initialState?: ExpressionAstFunctionBuilder[] | ExpressionAstExpression | string +): ExpressionAstExpressionBuilder { + const chainToFunctionBuilder = (chain: ExpressionAstFunction[]): ExpressionAstFunctionBuilder[] => + chain.map((fn) => buildExpressionFunction(fn.function, fn.arguments)); + + // Takes `initialState` and converts it to an array of `ExpressionAstFunctionBuilder` + const extractFunctionsFromState = ( + state: ExpressionAstFunctionBuilder[] | ExpressionAstExpression | string + ): ExpressionAstFunctionBuilder[] => { + if (typeof state === 'string') { + return chainToFunctionBuilder(parse(state, 'expression').chain); + } else if (!Array.isArray(state)) { + // If it isn't an array, it is an `ExpressionAstExpression` + return chainToFunctionBuilder(state.chain); + } + return state; + }; + + const fns: ExpressionAstFunctionBuilder[] = initialState + ? extractFunctionsFromState(initialState) + : []; + + return { + type: 'expression_builder', + functions: fns, + + findFunction( + fnName: InferFunctionDefinition['name'] + ) { + const foundFns: Array> = []; + return fns.reduce((found, currFn) => { + Object.values(currFn.arguments).forEach((values) => { + values.forEach((value) => { + if (isExpressionAstBuilder(value)) { + // `value` is a subexpression, recurse and continue searching + found = found.concat(value.findFunction(fnName)); + } + }); + }); + if (currFn.name === fnName) { + found.push(currFn as ExpressionAstFunctionBuilder); + } + return found; + }, foundFns); + }, + + toAst() { + if (fns.length < 1) { + throw new Error('Functions have not been added to the expression builder'); + } + return generateExpressionAst(fns); + }, + + toString() { + if (fns.length < 1) { + throw new Error('Functions have not been added to the expression builder'); + } + return format(generateExpressionAst(fns), 'expression'); + }, + }; +} diff --git a/src/plugins/expressions/common/ast/build_function.test.ts b/src/plugins/expressions/common/ast/build_function.test.ts new file mode 100644 index 00000000000000..a2b54f31f6f8f3 --- /dev/null +++ b/src/plugins/expressions/common/ast/build_function.test.ts @@ -0,0 +1,399 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstExpression } from './types'; +import { buildExpression } from './build_expression'; +import { buildExpressionFunction } from './build_function'; + +describe('buildExpressionFunction()', () => { + let subexp: ExpressionAstExpression; + let ast: ExpressionAstExpression; + + beforeEach(() => { + subexp = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'hello', + arguments: { + world: [false, true], + }, + }, + ], + }; + ast = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'foo', + arguments: { + bar: ['baz'], + subexp: [subexp], + }, + }, + ], + }; + }); + + test('accepts an args object as initial state', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(fn.toAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "world": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + } + `); + }); + + test('wraps any args in initial state in an array', () => { + const fn = buildExpressionFunction('hello', { world: true }); + expect(fn.arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + true, + ], + } + `); + }); + + test('returns all expected properties', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(Object.keys(fn)).toMatchInlineSnapshot(` + Array [ + "type", + "name", + "arguments", + "addArgument", + "getArgument", + "replaceArgument", + "removeArgument", + "toAst", + "toString", + ] + `); + }); + + test('handles subexpressions in initial state', () => { + const fn = buildExpressionFunction(ast.chain[0].function, ast.chain[0].arguments); + expect(fn.toAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "bar": Array [ + "baz", + ], + "subexp": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "foo", + "type": "function", + } + `); + }); + + test('handles subexpressions in multi-args in initial state', () => { + const subexpression = buildExpression([buildExpressionFunction('mySubexpression', {})]); + const fn = buildExpressionFunction('hello', { world: [true, subexpression] }); + expect(fn.toAst().arguments.world).toMatchInlineSnapshot(` + Array [ + true, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "mySubexpression", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + describe('handles subexpressions as args', () => { + test('when provided an AST for the subexpression', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.addArgument('subexp', buildExpression(subexp).toAst()); + expect(fn.toAst().arguments.subexp).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "world": Array [ + false, + true, + ], + }, + "function": "hello", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + test('when provided a function builder for the subexpression', () => { + // test using `markdownVis`, which expects a subexpression + // using the `font` function + const anotherSubexpression = buildExpression([buildExpressionFunction('font', { size: 12 })]); + const fn = buildExpressionFunction('markdownVis', { + markdown: 'hello', + openLinksInNewTab: true, + font: anotherSubexpression, + }); + expect(fn.toAst().arguments.font).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "size": Array [ + 12, + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + test('when subexpressions are changed by reference', () => { + const fontFn = buildExpressionFunction('font', { size: 12 }); + const fn = buildExpressionFunction('markdownVis', { + markdown: 'hello', + openLinksInNewTab: true, + font: buildExpression([fontFn]), + }); + fontFn.addArgument('color', 'blue'); + fontFn.replaceArgument('size', [72]); + expect(fn.toAst().arguments.font).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "color": Array [ + "blue", + ], + "size": Array [ + 72, + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + }); + + describe('#addArgument', () => { + test('allows you to add a new argument', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.addArgument('world', false); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + true, + false, + ], + } + `); + }); + + test('creates new args if they do not yet exist', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.addArgument('foo', 'bar'); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "foo": Array [ + "bar", + ], + "world": Array [ + true, + ], + } + `); + }); + + test('mutates a function already associated with an expression', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + const exp = buildExpression([fn]); + fn.addArgument('foo', 'bar'); + expect(exp.toAst().chain).toMatchInlineSnapshot(` + Array [ + Object { + "arguments": Object { + "foo": Array [ + "bar", + ], + "world": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + }, + ] + `); + fn.removeArgument('foo'); + expect(exp.toAst().chain).toMatchInlineSnapshot(` + Array [ + Object { + "arguments": Object { + "world": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + }, + ] + `); + }); + }); + + describe('#getArgument', () => { + test('retrieves an arg by name', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(fn.getArgument('world')).toEqual([true]); + }); + + test(`returns undefined when an arg doesn't exist`, () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(fn.getArgument('test')).toBe(undefined); + }); + + test('returned array can be updated to add/remove multiargs', () => { + const fn = buildExpressionFunction('hello', { world: [0, 1] }); + const arg = fn.getArgument('world'); + arg!.push(2); + expect(fn.getArgument('world')).toEqual([0, 1, 2]); + fn.replaceArgument( + 'world', + arg!.filter((a) => a !== 1) + ); + expect(fn.getArgument('world')).toEqual([0, 2]); + }); + }); + + describe('#toAst', () => { + test('returns a function AST', () => { + const fn = buildExpressionFunction('hello', { foo: [true] }); + expect(fn.toAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "foo": Array [ + true, + ], + }, + "function": "hello", + "type": "function", + } + `); + }); + }); + + describe('#toString', () => { + test('returns a function String', () => { + const fn = buildExpressionFunction('hello', { foo: [true], bar: ['hi'] }); + expect(fn.toString()).toMatchInlineSnapshot(`"hello foo=true bar=\\"hi\\""`); + }); + }); + + describe('#replaceArgument', () => { + test('allows you to replace an existing argument', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.replaceArgument('world', [false]); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + false, + ], + } + `); + }); + + test('allows you to replace an existing argument with multi args', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + fn.replaceArgument('world', [true, false]); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "world": Array [ + true, + false, + ], + } + `); + }); + + test('throws an error when replacing a non-existant arg', () => { + const fn = buildExpressionFunction('hello', { world: [true] }); + expect(() => { + fn.replaceArgument('whoops', [false]); + }).toThrowError(); + }); + }); + + describe('#removeArgument', () => { + test('removes an argument by name', () => { + const fn = buildExpressionFunction('hello', { foo: [true], bar: [false] }); + fn.removeArgument('bar'); + expect(fn.toAst().arguments).toMatchInlineSnapshot(` + Object { + "foo": Array [ + true, + ], + } + `); + }); + }); +}); diff --git a/src/plugins/expressions/common/ast/build_function.ts b/src/plugins/expressions/common/ast/build_function.ts new file mode 100644 index 00000000000000..5a1bd615d64507 --- /dev/null +++ b/src/plugins/expressions/common/ast/build_function.ts @@ -0,0 +1,243 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstFunction } from './types'; +import { + AnyExpressionFunctionDefinition, + ExpressionFunctionDefinition, +} from '../expression_functions/types'; +import { + buildExpression, + ExpressionAstExpressionBuilder, + isExpressionAstBuilder, + isExpressionAst, +} from './build_expression'; +import { format } from './format'; + +// Infers the types from an ExpressionFunctionDefinition. +// @internal +export type InferFunctionDefinition< + FnDef extends AnyExpressionFunctionDefinition +> = FnDef extends ExpressionFunctionDefinition< + infer Name, + infer Input, + infer Arguments, + infer Output, + infer Context +> + ? { name: Name; input: Input; arguments: Arguments; output: Output; context: Context } + : never; + +// Shortcut for inferring args from a function definition. +type FunctionArgs = InferFunctionDefinition< + FnDef +>['arguments']; + +// Gets a list of possible arg names for a given function. +type FunctionArgName = { + [A in keyof FunctionArgs]: A extends string ? A : never; +}[keyof FunctionArgs]; + +// Gets all optional string keys from an interface. +type OptionalKeys = { + [K in keyof T]-?: {} extends Pick ? (K extends string ? K : never) : never; +}[keyof T]; + +// Represents the shape of arguments as they are stored +// in the function builder. +interface FunctionBuilderArguments { + [key: string]: Array[string] | ExpressionAstExpressionBuilder>; +} + +export interface ExpressionAstFunctionBuilder< + FnDef extends AnyExpressionFunctionDefinition = AnyExpressionFunctionDefinition +> { + /** + * Used to identify expression function builder objects. + */ + type: 'expression_function_builder'; + /** + * Name of this expression function. + */ + name: InferFunctionDefinition['name']; + /** + * Object of all args currently added to the function. This is + * structured similarly to `ExpressionAstFunction['arguments']`, + * however any subexpressions are returned as expression builder + * instances instead of expression ASTs. + */ + arguments: FunctionBuilderArguments; + /** + * Adds an additional argument to the function. For multi-args, + * this should be called once for each new arg. Note that TS + * will not enforce whether multi-args are available, so only + * use this to update an existing arg if you are certain it + * is a multi-arg. + * + * @param name The name of the argument to add. + * @param value The value of the argument to add. + * @return `this` + */ + addArgument: >( + name: A, + value: FunctionArgs[A] | ExpressionAstExpressionBuilder + ) => this; + /** + * Retrieves an existing argument by name. + * Useful when you want to retrieve the current array of args and add + * something to it before calling `replaceArgument`. Any subexpression + * arguments will be returned as expression builder instances. + * + * @param name The name of the argument to retrieve. + * @return `ExpressionAstFunctionBuilderArgument[] | undefined` + */ + getArgument: >( + name: A + ) => Array[A] | ExpressionAstExpressionBuilder> | undefined; + /** + * Overwrites an existing argument with a new value. + * In order to support multi-args, the value given must always be + * an array. + * + * @param name The name of the argument to replace. + * @param value The value of the argument. Must always be an array. + * @return `this` + */ + replaceArgument: >( + name: A, + value: Array[A] | ExpressionAstExpressionBuilder> + ) => this; + /** + * Removes an (optional) argument from the function. + * + * TypeScript will enforce that you only remove optional + * arguments. For manipulating required args, use `replaceArgument`. + * + * @param name The name of the argument to remove. + * @return `this` + */ + removeArgument: >>(name: A) => this; + /** + * Converts function to an AST. + * + * @return `ExpressionAstFunction` + */ + toAst: () => ExpressionAstFunction; + /** + * Converts function to an expression string. + * + * @return `string` + */ + toString: () => string; +} + +/** + * Manages an AST for a single expression function. The return value + * can be provided to `buildExpression` to add this function to an + * expression. + * + * Note that to preserve type safety and ensure no args are missing, + * all required arguments for the specified function must be provided + * up front. If desired, they can be changed or removed later. + * + * @param fnName String representing the name of this expression function. + * @param initialArgs Object containing the arguments to this function. + * @return `this` + */ +export function buildExpressionFunction< + FnDef extends AnyExpressionFunctionDefinition = AnyExpressionFunctionDefinition +>( + fnName: InferFunctionDefinition['name'], + /** + * To support subexpressions, we override all args to also accept an + * ExpressionBuilder. This isn't perfectly typesafe since we don't + * know with certainty that the builder's output matches the required + * argument input, so we trust that folks using subexpressions in the + * builder know what they're doing. + */ + initialArgs: { + [K in keyof FunctionArgs]: + | FunctionArgs[K] + | ExpressionAstExpressionBuilder + | ExpressionAstExpressionBuilder[]; + } +): ExpressionAstFunctionBuilder { + const args = Object.entries(initialArgs).reduce((acc, [key, value]) => { + if (Array.isArray(value)) { + acc[key] = value.map((v) => { + return isExpressionAst(v) ? buildExpression(v) : v; + }); + } else { + acc[key] = isExpressionAst(value) ? [buildExpression(value)] : [value]; + } + return acc; + }, initialArgs as FunctionBuilderArguments); + + return { + type: 'expression_function_builder', + name: fnName, + arguments: args, + + addArgument(key, value) { + if (!args.hasOwnProperty(key)) { + args[key] = []; + } + args[key].push(value); + return this; + }, + + getArgument(key) { + if (!args.hasOwnProperty(key)) { + return; + } + return args[key]; + }, + + replaceArgument(key, values) { + if (!args.hasOwnProperty(key)) { + throw new Error('Argument to replace does not exist on this function'); + } + args[key] = values; + return this; + }, + + removeArgument(key) { + delete args[key]; + return this; + }, + + toAst() { + const ast: ExpressionAstFunction['arguments'] = {}; + return { + type: 'function', + function: fnName, + arguments: Object.entries(args).reduce((acc, [key, values]) => { + acc[key] = values.map((val) => { + return isExpressionAstBuilder(val) ? val.toAst() : val; + }); + return acc; + }, ast), + }; + }, + + toString() { + return format({ type: 'expression', chain: [this.toAst()] }, 'expression'); + }, + }; +} diff --git a/src/plugins/expressions/common/ast/format.test.ts b/src/plugins/expressions/common/ast/format.test.ts index d680ab2e30ce47..3d443c87b1ae20 100644 --- a/src/plugins/expressions/common/ast/format.test.ts +++ b/src/plugins/expressions/common/ast/format.test.ts @@ -17,11 +17,12 @@ * under the License. */ -import { formatExpression } from './format'; +import { ExpressionAstExpression, ExpressionAstArgument } from './types'; +import { format } from './format'; -describe('formatExpression()', () => { - test('converts expression AST to string', () => { - const str = formatExpression({ +describe('format()', () => { + test('formats an expression AST', () => { + const ast: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -32,8 +33,13 @@ describe('formatExpression()', () => { function: 'foo', }, ], - }); + }; - expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + expect(format(ast, 'expression')).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + }); + + test('formats an argument', () => { + const ast: ExpressionAstArgument = 'foo'; + expect(format(ast, 'argument')).toMatchInlineSnapshot(`"\\"foo\\""`); }); }); diff --git a/src/plugins/expressions/common/ast/format.ts b/src/plugins/expressions/common/ast/format.ts index 985f07008b33d3..7af0ab3350ab6c 100644 --- a/src/plugins/expressions/common/ast/format.ts +++ b/src/plugins/expressions/common/ast/format.ts @@ -22,13 +22,9 @@ import { ExpressionAstExpression, ExpressionAstArgument } from './types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { toExpression } = require('@kbn/interpreter/common'); -export function format( - ast: ExpressionAstExpression | ExpressionAstArgument, - type: 'expression' | 'argument' +export function format( + ast: T, + type: T extends ExpressionAstExpression ? 'expression' : 'argument' ): string { return toExpression(ast, type); } - -export function formatExpression(ast: ExpressionAstExpression): string { - return format(ast, 'expression'); -} diff --git a/src/plugins/expressions/common/ast/format_expression.test.ts b/src/plugins/expressions/common/ast/format_expression.test.ts new file mode 100644 index 00000000000000..933fe78fc4dcaf --- /dev/null +++ b/src/plugins/expressions/common/ast/format_expression.test.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { formatExpression } from './format_expression'; + +describe('formatExpression()', () => { + test('converts expression AST to string', () => { + const str = formatExpression({ + type: 'expression', + chain: [ + { + type: 'function', + arguments: { + bar: ['baz'], + }, + function: 'foo', + }, + ], + }); + + expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`); + }); +}); diff --git a/src/plugins/expressions/common/ast/format_expression.ts b/src/plugins/expressions/common/ast/format_expression.ts new file mode 100644 index 00000000000000..cc9fe05fb85d22 --- /dev/null +++ b/src/plugins/expressions/common/ast/format_expression.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionAstExpression } from './types'; +import { format } from './format'; + +/** + * Given expression pipeline AST, returns formatted string. + * + * @param ast Expression pipeline AST. + */ +export function formatExpression(ast: ExpressionAstExpression): string { + return format(ast, 'expression'); +} diff --git a/src/plugins/expressions/common/ast/index.ts b/src/plugins/expressions/common/ast/index.ts index 398718e8092b35..45ef8d45422ebf 100644 --- a/src/plugins/expressions/common/ast/index.ts +++ b/src/plugins/expressions/common/ast/index.ts @@ -17,7 +17,10 @@ * under the License. */ -export * from './types'; -export * from './parse'; -export * from './parse_expression'; +export * from './build_expression'; +export * from './build_function'; +export * from './format_expression'; export * from './format'; +export * from './parse_expression'; +export * from './parse'; +export * from './types'; diff --git a/src/plugins/expressions/common/ast/parse.test.ts b/src/plugins/expressions/common/ast/parse.test.ts index 967091a52082f6..77487f0a1ee909 100644 --- a/src/plugins/expressions/common/ast/parse.test.ts +++ b/src/plugins/expressions/common/ast/parse.test.ts @@ -37,6 +37,12 @@ describe('parse()', () => { }); }); + test('throws on malformed expression', () => { + expect(() => { + parse('{ intentionally malformed }', 'expression'); + }).toThrowError(); + }); + test('parses an argument', () => { const arg = parse('foo', 'argument'); expect(arg).toBe('foo'); diff --git a/src/plugins/expressions/common/ast/parse.ts b/src/plugins/expressions/common/ast/parse.ts index 0204694d1926de..f02c51d7b67997 100644 --- a/src/plugins/expressions/common/ast/parse.ts +++ b/src/plugins/expressions/common/ast/parse.ts @@ -22,10 +22,10 @@ import { ExpressionAstExpression, ExpressionAstArgument } from './types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { parse: parseRaw } = require('@kbn/interpreter/common'); -export function parse( - expression: string, - startRule: 'expression' | 'argument' -): ExpressionAstExpression | ExpressionAstArgument { +export function parse( + expression: E, + startRule: S +): S extends 'expression' ? ExpressionAstExpression : ExpressionAstArgument { try { return parseRaw(String(expression), { startRule }); } catch (e) { diff --git a/src/plugins/expressions/common/ast/parse_expression.ts b/src/plugins/expressions/common/ast/parse_expression.ts index ae4d80bd1fb5bf..1ae542aa3d0c78 100644 --- a/src/plugins/expressions/common/ast/parse_expression.ts +++ b/src/plugins/expressions/common/ast/parse_expression.ts @@ -26,5 +26,5 @@ import { parse } from './parse'; * @param expression Expression pipeline string. */ export function parseExpression(expression: string): ExpressionAstExpression { - return parse(expression, 'expression') as ExpressionAstExpression; + return parse(expression, 'expression'); } diff --git a/src/plugins/expressions/common/expression_functions/specs/clog.ts b/src/plugins/expressions/common/expression_functions/specs/clog.ts index 7839f1fc7998dd..28294af04c881a 100644 --- a/src/plugins/expressions/common/expression_functions/specs/clog.ts +++ b/src/plugins/expressions/common/expression_functions/specs/clog.ts @@ -19,7 +19,9 @@ import { ExpressionFunctionDefinition } from '../types'; -export const clog: ExpressionFunctionDefinition<'clog', unknown, {}, unknown> = { +export type ExpressionFunctionClog = ExpressionFunctionDefinition<'clog', unknown, {}, unknown>; + +export const clog: ExpressionFunctionClog = { name: 'clog', args: {}, help: 'Outputs the context to the console', diff --git a/src/plugins/expressions/common/expression_functions/specs/font.ts b/src/plugins/expressions/common/expression_functions/specs/font.ts index c8016bfacc710a..c46ce0adadef06 100644 --- a/src/plugins/expressions/common/expression_functions/specs/font.ts +++ b/src/plugins/expressions/common/expression_functions/specs/font.ts @@ -52,7 +52,9 @@ interface Arguments { weight?: FontWeight; } -export const font: ExpressionFunctionDefinition<'font', null, Arguments, Style> = { +export type ExpressionFunctionFont = ExpressionFunctionDefinition<'font', null, Arguments, Style>; + +export const font: ExpressionFunctionFont = { name: 'font', aliases: [], type: 'style', diff --git a/src/plugins/expressions/common/expression_functions/specs/var.ts b/src/plugins/expressions/common/expression_functions/specs/var.ts index e90a21101c5572..4bc185a4cadfde 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var.ts @@ -24,7 +24,12 @@ interface Arguments { name: string; } -type ExpressionFunctionVar = ExpressionFunctionDefinition<'var', unknown, Arguments, unknown>; +export type ExpressionFunctionVar = ExpressionFunctionDefinition< + 'var', + unknown, + Arguments, + unknown +>; export const variable: ExpressionFunctionVar = { name: 'var', diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index 0bf89f5470b3d1..8f15bc8b90042e 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -25,7 +25,14 @@ interface Arguments { value?: any; } -export const variableSet: ExpressionFunctionDefinition<'var_set', unknown, Arguments, unknown> = { +export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< + 'var_set', + unknown, + Arguments, + unknown +>; + +export const variableSet: ExpressionFunctionVarSet = { name: 'var_set', help: i18n.translate('expressions.functions.varset.help', { defaultMessage: 'Updates kibana global context', diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index b91deea36aee89..5979bcffb3175e 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -21,6 +21,14 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { ArgumentType } from './arguments'; import { TypeToString } from '../types/common'; import { ExecutionContext } from '../execution/types'; +import { + ExpressionFunctionClog, + ExpressionFunctionFont, + ExpressionFunctionKibanaContext, + ExpressionFunctionKibana, + ExpressionFunctionVarSet, + ExpressionFunctionVar, +} from './specs'; /** * `ExpressionFunctionDefinition` is the interface plugins have to implement to @@ -29,7 +37,7 @@ import { ExecutionContext } from '../execution/types'; export interface ExpressionFunctionDefinition< Name extends string, Input, - Arguments, + Arguments extends Record, Output, Context extends ExecutionContext = ExecutionContext > { @@ -93,4 +101,25 @@ export interface ExpressionFunctionDefinition< /** * Type to capture every possible expression function definition. */ -export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition; +export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition< + string, + any, + Record, + any +>; + +/** + * A mapping of `ExpressionFunctionDefinition`s for functions which the + * Expressions services provides out-of-the-box. Any new functions registered + * by the Expressions plugin should have their types added here. + * + * @public + */ +export interface ExpressionFunctionDefinitions { + clog: ExpressionFunctionClog; + font: ExpressionFunctionFont; + kibana_context: ExpressionFunctionKibanaContext; + kibana: ExpressionFunctionKibana; + var_set: ExpressionFunctionVarSet; + var: ExpressionFunctionVar; +} diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 336a80d98a1106..87406db89a2a8a 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -42,6 +42,8 @@ export { AnyExpressionFunctionDefinition, AnyExpressionTypeDefinition, ArgumentType, + buildExpression, + buildExpressionFunction, Datatable, DatatableColumn, DatatableColumnType, @@ -57,10 +59,13 @@ export { ExecutorState, ExpressionAstArgument, ExpressionAstExpression, + ExpressionAstExpressionBuilder, ExpressionAstFunction, + ExpressionAstFunctionBuilder, ExpressionAstNode, ExpressionFunction, ExpressionFunctionDefinition, + ExpressionFunctionDefinitions, ExpressionFunctionKibana, ExpressionFunctionParameter, ExpressionImage, @@ -90,6 +95,7 @@ export { IInterpreterRenderHandlers, InterpreterErrorType, IRegistry, + isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, KibanaDatatable, diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index 61d3838466bef3..9b2f0b794258b0 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -34,6 +34,8 @@ export { AnyExpressionFunctionDefinition, AnyExpressionTypeDefinition, ArgumentType, + buildExpression, + buildExpressionFunction, Datatable, DatatableColumn, DatatableColumnType, @@ -48,10 +50,13 @@ export { ExecutorState, ExpressionAstArgument, ExpressionAstExpression, + ExpressionAstExpressionBuilder, ExpressionAstFunction, + ExpressionAstFunctionBuilder, ExpressionAstNode, ExpressionFunction, ExpressionFunctionDefinition, + ExpressionFunctionDefinitions, ExpressionFunctionKibana, ExpressionFunctionParameter, ExpressionImage, @@ -81,6 +86,7 @@ export { IInterpreterRenderHandlers, InterpreterErrorType, IRegistry, + isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, KibanaDatatable, From a5c9c4ec4324f7432dbe083ba7eb1c2a63896a45 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 17 Jun 2020 16:24:40 -0400 Subject: [PATCH 21/26] [CI] Add baseline trigger job --- .ci/Jenkinsfile_baseline_trigger | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .ci/Jenkinsfile_baseline_trigger diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger new file mode 100644 index 00000000000000..05daeebdc058ca --- /dev/null +++ b/.ci/Jenkinsfile_baseline_trigger @@ -0,0 +1,64 @@ +#!/bin/groovy + +def MAXIMUM_COMMITS_TO_CHECK = 10 +def MAXIMUM_COMMITS_TO_BUILD = 5 + +if (!params.branches_yaml) { + error "'branches_yaml' parameter must be specified" +} + +def additionalBranches = [] + +def branches = readYaml(text: params.branches_yaml) + additionalBranches + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +withGithubCredentials { + branches.each { branch -> + stage(branch) { + def commits = getCommits(branch, MAXIMUM_COMMITS_TO_CHECK, MAXIMUM_COMMITS_TO_BUILD) + + commits.take(MAXIMUM_COMMITS_TO_BUILD).each { commit -> + catchErrors { + githubCommitStatus.create(commit, 'pending', 'Baseline started.', context = 'kibana-ci-baseline') + + build( + propagate: false, + wait: false, + job: 'elastic+kibana+baseline', + parameters: [ + string(name: 'branch_specifier', value: branch), + string(name: 'commit', value: commit), + ] + ) + } + } + } + } +} + +def getCommits(String branch, maximumCommitsToCheck, maximumCommitsToBuild) { + print "Getting latest commits for ${branch}..." + def commits = githubApi.get("repos/elastic/kibana/commits?sha=${branch}").take(maximumCommitsToCheck).collect { it.sha } + def commitsToBuild = [] + + for (commit in commits) { + print "Getting statuses for ${commit}" + def status = githubApi.get("repos/elastic/kibana/statuses/${commit}").find { it.context == 'kibana-ci-baseline' } + print "Commit '${commit}' already built? ${status ? 'Yes' : 'No'}" + + if (!status) { + commitsToBuild << commit + } else { + // Stop at the first commit we find that's already been triggered + break + } + + if (commitsToBuild.size() >= maximumCommitsToBuild) { + break + } + } + + return commitsToBuild.reverse() // We want the builds to trigger oldest-to-newest +} From a81d8b55ab2d941010137e4019c015ff77687721 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 13 Jul 2020 16:15:48 -0700 Subject: [PATCH 22/26] rename visual_baseline -> baseline_capture --- ..._visual_baseline => Jenkinsfile_baseline_capture} | 0 test/scripts/jenkins_xpack_visual_regression.sh | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) rename .ci/{Jenkinsfile_visual_baseline => Jenkinsfile_baseline_capture} (100%) diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_baseline_capture similarity index 100% rename from .ci/Jenkinsfile_visual_baseline rename to .ci/Jenkinsfile_baseline_capture diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index ac567a188a6d40..06a53277b8688d 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -11,6 +11,12 @@ installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +# cd "$KIBANA_DIR" +# source "test/scripts/jenkins_xpack_page_load_metrics.sh" + +cd "$KIBANA_DIR" +source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" + echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" yarn percy exec -t 10000 -- -- \ @@ -18,9 +24,3 @@ yarn percy exec -t 10000 -- -- \ --debug --bail \ --kibana-install-dir "$installDir" \ --config test/visual_regression/config.ts; - -# cd "$KIBANA_DIR" -# source "test/scripts/jenkins_xpack_page_load_metrics.sh" - -cd "$KIBANA_DIR" -source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" From 0e7c3c7ff09e2e1daa4b1eba93c62059eb5fe3c1 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Jul 2020 16:07:22 -0600 Subject: [PATCH 23/26] [Maps] increase DEFAULT_MAX_BUCKETS_LIMIT to 65535 (#70313) Co-authored-by: Elastic Machine --- x-pack/plugins/maps/common/constants.ts | 2 +- .../maps/public/classes/fields/es_agg_field.ts | 6 ++++-- .../sources/es_geo_grid_source/es_geo_grid_source.js | 3 +++ .../plugins/maps/public/elasticsearch_geo_utils.js | 5 +++-- .../maps/public/elasticsearch_geo_utils.test.js | 12 ++++++------ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 98464427cc3482..cf67ac4dd999f7 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -90,7 +90,7 @@ export const DECIMAL_DEGREES_PRECISION = 5; // meters precision export const ZOOM_PRECISION = 2; export const DEFAULT_MAX_RESULT_WINDOW = 10000; export const DEFAULT_MAX_INNER_RESULT_WINDOW = 100; -export const DEFAULT_MAX_BUCKETS_LIMIT = 10000; +export const DEFAULT_MAX_BUCKETS_LIMIT = 65535; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index e0f5c79f1d4278..15779d22681c0f 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -17,6 +17,8 @@ import { TopTermPercentageField } from './top_term_percentage_field'; import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; import { ESAggTooltipProperty } from '../tooltips/es_agg_tooltip_property'; +const TERMS_AGG_SHARD_SIZE = 5; + export interface IESAggField extends IField { getValueAggDsl(indexPattern: IndexPattern): unknown | null; getBucketCount(): number; @@ -100,7 +102,7 @@ export class ESAggField implements IESAggField { const field = getField(indexPattern, this.getRootName()); const aggType = this.getAggType(); - const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: 1 } : {}; + const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: TERMS_AGG_SHARD_SIZE } : {}; return { [aggType]: addFieldToDSL(aggBody, field), }; @@ -108,7 +110,7 @@ export class ESAggField implements IESAggField { getBucketCount(): number { // terms aggregation increases the overall number of buckets per split bucket - return this.getAggType() === AGG_TYPE.TERMS ? 1 : 0; + return this.getAggType() === AGG_TYPE.TERMS ? TERMS_AGG_SHARD_SIZE : 0; } supportsFieldMeta(): boolean { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index 3902709eeb8414..92f6c258af5979 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -161,6 +161,7 @@ export class ESGeoGridSource extends AbstractESAggSource { bounds: makeESBbox(bufferedExtent), field: this._descriptor.geoField, precision, + size: DEFAULT_MAX_BUCKETS_LIMIT, }, }, }, @@ -245,6 +246,8 @@ export class ESGeoGridSource extends AbstractESAggSource { bounds: makeESBbox(bufferedExtent), field: this._descriptor.geoField, precision, + size: DEFAULT_MAX_BUCKETS_LIMIT, + shard_size: DEFAULT_MAX_BUCKETS_LIMIT, }, aggs: { gridCentroid: { diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index efd243595db3e3..0d247d389f4785 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -400,8 +400,9 @@ export function getBoundingBoxGeometry(geometry) { export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { // GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons // when the shape crosses the dateline - const left = minLon; - const right = maxLon; + const lonDelta = maxLon - minLon; + const left = lonDelta > 360 ? -180 : minLon; + const right = lonDelta > 360 ? 180 : maxLon; const top = clampToLatBounds(maxLat); const bottom = clampToLatBounds(minLat); const topLeft = [left, top]; diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index a1e4e43f3ab75c..adaeae66bee143 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -421,7 +421,7 @@ describe('createExtentFilter', () => { }); }); - it('should not clamp longitudes to -180 to 180', () => { + it('should clamp longitudes to -180 to 180 when lonitude wraps globe', () => { const mapExtent = { maxLat: 39, maxLon: 209, @@ -436,11 +436,11 @@ describe('createExtentFilter', () => { shape: { coordinates: [ [ - [-191, 39], - [-191, 35], - [209, 35], - [209, 39], - [-191, 39], + [-180, 39], + [-180, 35], + [180, 35], + [180, 39], + [-180, 39], ], ], type: 'Polygon', From e42630d1c58c2587e34959c8037e4ac6b9d27472 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 14 Jul 2020 18:08:20 -0400 Subject: [PATCH 24/26] [Security Solution] [DETECTIONS] Set rule status to failure only on large gaps (#71549) * only display gap error when a gap is too large for the gap mitigation code to cover, general code cleanup, adds some tests for separate function * removes throwing of errors and log error and return null for maxCatchup, ratio, and gapDiffInUnits properties * forgot to delete commented out code * remove math.abs since we fixed this bug by switching around logic when calculating gapDiffInUnits in getGapMaxCatchupRatio fn * updates tests for when a gap error should be written to rule status * fix typo --- .../signals/signal_rule_alert_type.test.ts | 36 ++- .../signals/signal_rule_alert_type.ts | 36 ++- .../lib/detection_engine/signals/types.ts | 5 + .../detection_engine/signals/utils.test.ts | 47 ++++ .../lib/detection_engine/signals/utils.ts | 218 ++++++++++++------ 5 files changed, 258 insertions(+), 84 deletions(-) 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 5832b4075a40b5..b0c855afa8be99 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 @@ -10,7 +10,13 @@ import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; -import { getGapBetweenRuns, getListsClient, getExceptions, sortExceptionItems } from './utils'; +import { + getGapBetweenRuns, + getGapMaxCatchupRatio, + getListsClient, + getExceptions, + sortExceptionItems, +} from './utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -97,6 +103,7 @@ describe('rules_notification_alert_type', () => { exceptionsWithValueLists: [], }); (searchAfterAndBulkCreate as jest.Mock).mockClear(); + (getGapMaxCatchupRatio as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, searchAfterTimes: [], @@ -126,22 +133,39 @@ describe('rules_notification_alert_type', () => { }); describe('executor', () => { - it('should warn about the gap between runs', async () => { - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1000)); + it('should warn about the gap between runs if gap is very large', async () => { + (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(100, 'm')); + (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ + maxCatchup: 4, + ratio: 20, + gapDiffInUnits: 95, + }); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); expect(logger.warn.mock.calls[0][0]).toContain( - 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.' + '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' ); expect(ruleStatusService.error).toHaveBeenCalled(); expect(ruleStatusService.error.mock.calls[0][0]).toContain( - 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.' + '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' ); expect(ruleStatusService.error.mock.calls[0][1]).toEqual({ - gap: 'a few seconds', + gap: '2 hours', }); }); + it('should NOT warn about the gap between runs if gap small', async () => { + (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1, 'm')); + (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ + maxCatchup: 1, + ratio: 1, + gapDiffInUnits: 1, + }); + await alert.executor(payload); + expect(logger.warn).toHaveBeenCalledTimes(0); + expect(ruleStatusService.error).toHaveBeenCalledTimes(0); + }); + it("should set refresh to 'wait_for' when actions are present", async () => { const ruleAlert = getResult(); ruleAlert.actions = [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 49efc30b9704d4..0e859ecef31c6c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -22,7 +22,14 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } from './utils'; +import { + getGapBetweenRuns, + parseScheduleDates, + getListsClient, + getExceptions, + getGapMaxCatchupRatio, + MAX_RULE_GAP_RATIO, +} from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; @@ -130,15 +137,26 @@ export const signalRulesAlertType = ({ const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); if (gap != null && gap.asMilliseconds() > 0) { - const gapString = gap.humanize(); - const gapMessage = buildRuleMessage( - `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, - 'Consider increasing your look behind time or adding more Kibana instances.' - ); - logger.warn(gapMessage); + const fromUnit = from[from.length - 1]; + const { ratio } = getGapMaxCatchupRatio({ + logger, + buildRuleMessage, + previousStartedAt, + ruleParamsFrom: from, + interval, + unit: fromUnit, + }); + if (ratio && ratio >= MAX_RULE_GAP_RATIO) { + const gapString = gap.humanize(); + const gapMessage = buildRuleMessage( + `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, + 'Consider increasing your look behind time or adding more Kibana instances.' + ); + logger.warn(gapMessage); - hasError = true; - await ruleStatusService.error(gapMessage, { gap: gapString }); + hasError = true; + await ruleStatusService.error(gapMessage, { gap: gapString }); + } } try { const { listClient, exceptionsClient } = await getListsClient({ 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 5d6bafc5a6d09f..bfc72a169566e1 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 @@ -11,6 +11,11 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; import { SearchResponse } from '../../types'; +// used for gap detection code +export type unitType = 's' | 'm' | 'h'; +export const isValidUnit = (unitParam: string): unitParam is unitType => + ['s', 'm', 'h'].includes(unitParam); + export interface SignalsParams { signalIds: string[] | undefined | null; query: object | undefined | null; 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 0cc3ca092a4dcd..a6130a20f9c520 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 @@ -21,6 +21,7 @@ import { parseScheduleDates, getDriftTolerance, getGapBetweenRuns, + getGapMaxCatchupRatio, errorAggregator, getListsClient, hasLargeValueList, @@ -716,6 +717,52 @@ describe('utils', () => { }); }); + describe('getMaxCatchupRatio', () => { + test('should return null if rule has never run before', () => { + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger: mockLogger, + previousStartedAt: null, + interval: '30s', + ruleParamsFrom: 'now-30s', + buildRuleMessage, + unit: 's', + }); + expect(maxCatchup).toBeNull(); + expect(ratio).toBeNull(); + expect(gapDiffInUnits).toBeNull(); + }); + + test('should should have non-null values when gap is present', () => { + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger: mockLogger, + previousStartedAt: moment().subtract(65, 's').toDate(), + interval: '50s', + ruleParamsFrom: 'now-55s', + buildRuleMessage, + unit: 's', + }); + expect(maxCatchup).toEqual(0.2); + expect(ratio).toEqual(0.2); + expect(gapDiffInUnits).toEqual(10); + }); + + // when a rule runs sooner than expected we don't + // consider that a gap as that is a very rare circumstance + test('should return null when given a negative gap (rule ran sooner than expected)', () => { + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger: mockLogger, + previousStartedAt: moment().subtract(-15, 's').toDate(), + interval: '10s', + ruleParamsFrom: 'now-13s', + buildRuleMessage, + unit: 's', + }); + expect(maxCatchup).toBeNull(); + expect(ratio).toBeNull(); + expect(gapDiffInUnits).toBeNull(); + }); + }); + describe('#getExceptions', () => { test('it successfully returns array of exception list items', async () => { const client = listMock.getExceptionListClient(); 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 0016765b9dbe90..0b95ff6786b013 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 @@ -12,7 +12,7 @@ import { AlertServices, parseDuration } from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { EntriesArray, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; -import { BulkResponse, BulkResponseErrorAggregation } from './types'; +import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; interface SortExceptionsReturn { @@ -20,6 +20,101 @@ interface SortExceptionsReturn { exceptionsWithoutValueLists: ExceptionListItemSchema[]; } +export const MAX_RULE_GAP_RATIO = 4; + +export const shorthandMap = { + s: { + momentString: 'seconds', + asFn: (duration: moment.Duration) => duration.asSeconds(), + }, + m: { + momentString: 'minutes', + asFn: (duration: moment.Duration) => duration.asMinutes(), + }, + h: { + momentString: 'hours', + asFn: (duration: moment.Duration) => duration.asHours(), + }, +}; + +export const getGapMaxCatchupRatio = ({ + logger, + previousStartedAt, + unit, + buildRuleMessage, + ruleParamsFrom, + interval, +}: { + logger: Logger; + ruleParamsFrom: string; + previousStartedAt: Date | null | undefined; + interval: string; + buildRuleMessage: BuildRuleMessage; + unit: string; +}): { + maxCatchup: number | null; + ratio: number | null; + gapDiffInUnits: number | null; +} => { + if (previousStartedAt == null) { + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; + } + if (!isValidUnit(unit)) { + logger.error(buildRuleMessage(`unit: ${unit} failed isValidUnit check`)); + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; + } + /* + we need the total duration from now until the last time the rule ran. + the next few lines can be summed up as calculating + "how many second | minutes | hours have passed since the last time this ran?" + */ + const nowToGapDiff = moment.duration(moment().diff(previousStartedAt)); + // rule ran early, no gap + if (shorthandMap[unit].asFn(nowToGapDiff) < 0) { + // rule ran early, no gap + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; + } + const calculatedFrom = `now-${ + parseInt(shorthandMap[unit].asFn(nowToGapDiff).toString(), 10) + unit + }`; + logger.debug(buildRuleMessage(`calculatedFrom: ${calculatedFrom}`)); + + const intervalMoment = moment.duration(parseInt(interval, 10), unit); + logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); + const calculatedFromAsMoment = dateMath.parse(calculatedFrom); + const dateMathRuleParamsFrom = dateMath.parse(ruleParamsFrom); + if (dateMathRuleParamsFrom != null && intervalMoment != null) { + const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; + const gapDiffInUnits = dateMathRuleParamsFrom.diff(calculatedFromAsMoment, momentUnit); + + const ratio = gapDiffInUnits / shorthandMap[unit].asFn(intervalMoment); + + // maxCatchup is to ensure we are not trying to catch up too far back. + // This allows for a maximum of 4 consecutive rule execution misses + // to be included in the number of signals generated. + const maxCatchup = ratio < MAX_RULE_GAP_RATIO ? ratio : MAX_RULE_GAP_RATIO; + return { maxCatchup, ratio, gapDiffInUnits }; + } + logger.error(buildRuleMessage('failed to parse calculatedFrom and intervalMoment')); + return { + maxCatchup: null, + ratio: null, + gapDiffInUnits: null, + }; +}; + export const getListsClient = async ({ lists, spaceId, @@ -294,8 +389,6 @@ export const getSignalTimeTuples = ({ from: moment.Moment | undefined; maxSignals: number; }> => { - type unitType = 's' | 'm' | 'h'; - const isValidUnit = (unit: string): unit is unitType => ['s', 'm', 'h'].includes(unit); let totalToFromTuples: Array<{ to: moment.Moment | undefined; from: moment.Moment | undefined; @@ -305,20 +398,6 @@ export const getSignalTimeTuples = ({ const fromUnit = ruleParamsFrom[ruleParamsFrom.length - 1]; if (isValidUnit(fromUnit)) { const unit = fromUnit; // only seconds (s), minutes (m) or hours (h) - const shorthandMap = { - s: { - momentString: 'seconds', - asFn: (duration: moment.Duration) => duration.asSeconds(), - }, - m: { - momentString: 'minutes', - asFn: (duration: moment.Duration) => duration.asMinutes(), - }, - h: { - momentString: 'hours', - asFn: (duration: moment.Duration) => duration.asHours(), - }, - }; /* we need the total duration from now until the last time the rule ran. @@ -333,62 +412,63 @@ export const getSignalTimeTuples = ({ const intervalMoment = moment.duration(parseInt(interval, 10), unit); logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); - const calculatedFromAsMoment = dateMath.parse(calculatedFrom); - if (calculatedFromAsMoment != null && intervalMoment != null) { - const dateMathRuleParamsFrom = dateMath.parse(ruleParamsFrom); - const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; - const gapDiffInUnits = calculatedFromAsMoment.diff(dateMathRuleParamsFrom, momentUnit); - - const ratio = Math.abs(gapDiffInUnits / shorthandMap[unit].asFn(intervalMoment)); - - // maxCatchup is to ensure we are not trying to catch up too far back. - // This allows for a maximum of 4 consecutive rule execution misses - // to be included in the number of signals generated. - const maxCatchup = ratio < 4 ? ratio : 4; - logger.debug(buildRuleMessage(`maxCatchup: ${ratio}`)); + const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; + // maxCatchup is to ensure we are not trying to catch up too far back. + // This allows for a maximum of 4 consecutive rule execution misses + // to be included in the number of signals generated. + const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ + logger, + buildRuleMessage, + previousStartedAt, + unit, + ruleParamsFrom, + interval, + }); + logger.debug(buildRuleMessage(`maxCatchup: ${maxCatchup}, ratio: ${ratio}`)); + if (maxCatchup == null || ratio == null || gapDiffInUnits == null) { + throw new Error( + buildRuleMessage('failed to calculate maxCatchup, ratio, or gapDiffInUnits') + ); + } + let tempTo = dateMath.parse(ruleParamsFrom); + if (tempTo == null) { + // return an error + throw new Error(buildRuleMessage('dateMath parse failed')); + } - let tempTo = dateMath.parse(ruleParamsFrom); - if (tempTo == null) { - // return an error - throw new Error('dateMath parse failed'); + let beforeMutatedFrom: moment.Moment | undefined; + while (totalToFromTuples.length < maxCatchup) { + // if maxCatchup is less than 1, we calculate the 'from' differently + // and maxSignals becomes some less amount of maxSignals + // in order to maintain maxSignals per full rule interval. + if (maxCatchup > 0 && maxCatchup < 1) { + totalToFromTuples.push({ + to: tempTo.clone(), + from: tempTo.clone().subtract(gapDiffInUnits, momentUnit), + maxSignals: ruleParamsMaxSignals * maxCatchup, + }); + break; } + const beforeMutatedTo = tempTo.clone(); - let beforeMutatedFrom: moment.Moment | undefined; - while (totalToFromTuples.length < maxCatchup) { - // if maxCatchup is less than 1, we calculate the 'from' differently - // and maxSignals becomes some less amount of maxSignals - // in order to maintain maxSignals per full rule interval. - if (maxCatchup > 0 && maxCatchup < 1) { - totalToFromTuples.push({ - to: tempTo.clone(), - from: tempTo.clone().subtract(Math.abs(gapDiffInUnits), momentUnit), - maxSignals: ruleParamsMaxSignals * maxCatchup, - }); - break; - } - const beforeMutatedTo = tempTo.clone(); - - // moment.subtract mutates the moment so we need to clone again.. - beforeMutatedFrom = tempTo.clone().subtract(intervalMoment, momentUnit); - const tuple = { - to: beforeMutatedTo, - from: beforeMutatedFrom, - maxSignals: ruleParamsMaxSignals, - }; - totalToFromTuples = [...totalToFromTuples, tuple]; - tempTo = beforeMutatedFrom; - } - totalToFromTuples = [ - { - to: dateMath.parse(ruleParamsTo), - from: dateMath.parse(ruleParamsFrom), - maxSignals: ruleParamsMaxSignals, - }, - ...totalToFromTuples, - ]; - } else { - logger.debug(buildRuleMessage('calculatedFromMoment was null or intervalMoment was null')); + // moment.subtract mutates the moment so we need to clone again.. + beforeMutatedFrom = tempTo.clone().subtract(intervalMoment, momentUnit); + const tuple = { + to: beforeMutatedTo, + from: beforeMutatedFrom, + maxSignals: ruleParamsMaxSignals, + }; + totalToFromTuples = [...totalToFromTuples, tuple]; + tempTo = beforeMutatedFrom; } + totalToFromTuples = [ + { + to: dateMath.parse(ruleParamsTo), + from: dateMath.parse(ruleParamsFrom), + maxSignals: ruleParamsMaxSignals, + }, + ...totalToFromTuples, + ]; } } else { totalToFromTuples = [ From b1433e6317b34e39c572df48d952c27e32eaec2b Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 14 Jul 2020 15:08:11 -0700 Subject: [PATCH 25/26] remove unnecessary context reference from trigger job (cherry picked from commit 817fdf9b439e85c3ddfda126b3efb4e45c36006b) --- .ci/Jenkinsfile_baseline_trigger | 2 +- vars/githubCommitStatus.groovy | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger index 05daeebdc058ca..752334dbb6cc99 100644 --- a/.ci/Jenkinsfile_baseline_trigger +++ b/.ci/Jenkinsfile_baseline_trigger @@ -21,7 +21,7 @@ withGithubCredentials { commits.take(MAXIMUM_COMMITS_TO_BUILD).each { commit -> catchErrors { - githubCommitStatus.create(commit, 'pending', 'Baseline started.', context = 'kibana-ci-baseline') + githubCommitStatus.create(commit, 'pending', 'Baseline started.', 'kibana-ci-baseline') build( propagate: false, diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy index 4cd4228d55f03c..17d3c234f69283 100644 --- a/vars/githubCommitStatus.groovy +++ b/vars/githubCommitStatus.groovy @@ -35,7 +35,12 @@ def onFinish() { // state: error|failure|pending|success def create(sha, state, description, context = 'kibana-ci') { withGithubCredentials { - return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, description: description, context: context, target_url: env.BUILD_URL ]) + return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ + state: state, + description: description, + context: context, + target_url: env.BUILD_URL + ]) } } From e318ea76dc290442d385f0134aaada2cbb52d2bd Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 14 Jul 2020 15:10:01 -0700 Subject: [PATCH 26/26] fix triggered job name --- .ci/Jenkinsfile_baseline_trigger | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger index 752334dbb6cc99..cc9fb47ca49936 100644 --- a/.ci/Jenkinsfile_baseline_trigger +++ b/.ci/Jenkinsfile_baseline_trigger @@ -26,7 +26,7 @@ withGithubCredentials { build( propagate: false, wait: false, - job: 'elastic+kibana+baseline', + job: 'elastic+kibana+baseline-capture', parameters: [ string(name: 'branch_specifier', value: branch), string(name: 'commit', value: commit),