From 07f32d03b30a160511f67a9e59b563cde4200fbb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 25 Mar 2021 13:36:25 -0400 Subject: [PATCH 01/98] [Cases] Adding feature flag for sub cases (#95122) * Adding feature flag for sub cases * Disabling case as a connector in security solution * Fix connector test * Switching feature flag to global variable * Removing this.config use * Fixing circular import and renaming flag --- x-pack/plugins/cases/common/constants.ts | 9 +- .../cases/server/client/cases/create.ts | 13 +- .../plugins/cases/server/client/cases/get.ts | 32 +- .../plugins/cases/server/client/cases/push.ts | 9 +- .../cases/server/client/cases/update.ts | 21 + .../cases/server/client/comments/add.ts | 23 +- .../server/connectors/case/index.test.ts | 29 +- .../cases/server/connectors/case/index.ts | 7 + x-pack/plugins/cases/server/plugin.ts | 24 +- .../api/cases/comments/delete_all_comments.ts | 19 +- .../api/cases/comments/delete_comment.ts | 8 +- .../api/cases/comments/find_comments.ts | 8 +- .../api/cases/comments/get_all_comment.ts | 13 +- .../api/cases/comments/patch_comment.ts | 8 +- .../routes/api/cases/comments/post_comment.ts | 23 +- .../server/routes/api/cases/delete_cases.ts | 16 +- .../cases/server/routes/api/cases/get_case.ts | 8 +- .../plugins/cases/server/routes/api/index.ts | 17 +- x-pack/plugins/cases/server/services/index.ts | 19 +- .../security_solution/common/constants.ts | 3 +- .../rules/rule_actions_field/index.test.tsx | 21 +- .../tests/cases/comments/delete_comment.ts | 23 +- .../tests/cases/comments/find_comments.ts | 13 +- .../tests/cases/comments/get_all_comments.ts | 104 ++- .../basic/tests/cases/comments/get_comment.ts | 33 +- .../tests/cases/comments/patch_comment.ts | 23 +- .../tests/cases/comments/post_comment.ts | 15 +- .../basic/tests/cases/delete_cases.ts | 3 +- .../basic/tests/cases/find_cases.ts | 3 +- .../basic/tests/cases/get_case.ts | 10 + .../basic/tests/cases/patch_cases.ts | 31 +- .../basic/tests/cases/push_case.ts | 3 +- .../tests/cases/sub_cases/delete_sub_cases.ts | 126 +-- .../tests/cases/sub_cases/find_sub_cases.ts | 396 ++++----- .../tests/cases/sub_cases/get_sub_case.ts | 132 +-- .../tests/cases/sub_cases/patch_sub_cases.ts | 792 +++++++++--------- .../basic/tests/connectors/case.ts | 21 +- .../case_api_integration/common/lib/utils.ts | 4 +- 38 files changed, 1216 insertions(+), 846 deletions(-) diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 1e7cff99a00bd..148b81c346b6e 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants'; - export const APP_ID = 'cases'; /** @@ -53,5 +51,12 @@ export const SUPPORTED_CONNECTORS = [ * Alerts */ +// this value is from x-pack/plugins/security_solution/common/constants.ts +const DEFAULT_MAX_SIGNALS = 100; export const MAX_ALERTS_PER_SUB_CASE = 5000; export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS; + +/** + * This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete. + */ +export const ENABLE_CASE_CONNECTOR = false; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 59f9688836341..650b9aa81c990 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -35,6 +35,7 @@ import { CaseUserActionServiceSetup, } from '../../services'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface CreateCaseArgs { caseConfigureService: CaseConfigureServiceSetup; @@ -60,9 +61,19 @@ export const create = async ({ }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; + + if (!ENABLE_CASE_CONNECTOR && type === CaseType.collection) { + throw Boom.badRequest( + 'Case type cannot be collection when the case connector feature is disabled' + ); + } + const query = pipe( // decode with the defaulted type field - excess(CasesClientPostRequestRt).decode({ type, ...nonTypeCaseFields }), + excess(CasesClientPostRequestRt).decode({ + type, + ...nonTypeCaseFields, + }), fold(throwErrors(Boom.badRequest), identity) ); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index fa556986ee8d3..50725879278e4 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { SavedObjectsClientContract, Logger, SavedObject } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; -import { CaseResponseRt, CaseResponse } from '../../../common/api'; +import { CaseResponseRt, CaseResponse, ESCaseAttributes } from '../../../common/api'; import { CaseServiceSetup } from '../../services'; import { countAlertsForID } from '../../common'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; @@ -33,15 +34,26 @@ export const get = async ({ includeSubCaseComments = false, }: GetParams): Promise => { try { - const [theCase, subCasesForCaseId] = await Promise.all([ - caseService.getCase({ + let theCase: SavedObject; + let subCaseIds: string[] = []; + + if (ENABLE_CASE_CONNECTOR) { + const [caseInfo, subCasesForCaseId] = await Promise.all([ + caseService.getCase({ + client: savedObjectsClient, + id, + }), + caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + ]); + + theCase = caseInfo; + subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + } else { + theCase = await caseService.getCase({ client: savedObjectsClient, id, - }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), - ]); - - const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + }); + } if (!includeComments) { return CaseResponseRt.encode( @@ -58,7 +70,7 @@ export const get = async ({ sortField: 'created_at', sortOrder: 'asc', }, - includeSubCaseComments, + includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments, }); return CaseResponseRt.encode( diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 3217178768f89..216ef109534fb 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -40,6 +40,7 @@ import { } from '../../services'; import { CasesClientHandler } from '../client'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -92,7 +93,11 @@ export const push = async ({ try { [theCase, connector, userActions] = await Promise.all([ - casesClient.get({ id: caseId, includeComments: true, includeSubCaseComments: true }), + casesClient.get({ + id: caseId, + includeComments: true, + includeSubCaseComments: ENABLE_CASE_CONNECTOR, + }), actionsClient.get({ id: connectorId }), casesClient.getUserActions({ caseId }), ]); @@ -183,7 +188,7 @@ export const push = async ({ page: 1, perPage: theCase?.totalComment ?? 0, }, - includeSubCaseComments: true, + includeSubCaseComments: ENABLE_CASE_CONNECTOR, }), ]); } catch (e) { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index ff3c0a62407a1..b39bfe6ec4eb7 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -55,6 +55,7 @@ import { CasesClientHandler } from '..'; import { createAlertUpdateRequest } from '../../common'; import { UpdateAlertRequest } from '../types'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -97,6 +98,22 @@ function throwIfUpdateTypeCollectionToIndividual( } } +/** + * Throws an error if any of the requests attempt to update the type of a case. + */ +function throwIfUpdateType(requests: ESCasePatchRequest[]) { + const requestsUpdatingType = requests.filter((req) => req.type !== undefined); + + if (requestsUpdatingType.length > 0) { + const ids = requestsUpdatingType.map((req) => req.id); + throw Boom.badRequest( + `Updating the type of a case when sub cases are disabled is not allowed ids: [${ids.join( + ', ' + )}]` + ); + } +} + /** * Throws an error if any of the requests attempt to update an individual style cases' type field to a collection * when alerts are attached to the case. @@ -396,6 +413,10 @@ export const update = async ({ return acc; }, new Map>()); + if (!ENABLE_CASE_CONNECTOR) { + throwIfUpdateType(updateFilterCases); + } + throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); await throwIfInvalidUpdateOfTypeWithAlerts({ diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/comments/add.ts index 45746613dc1d4..5a119432b3ccb 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/comments/add.ts @@ -36,7 +36,10 @@ import { CommentableCase, createAlertUpdateRequest } from '../../common'; import { CasesClientHandler } from '..'; import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; -import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { + ENABLE_CASE_CONNECTOR, + MAX_GENERATED_ALERTS_PER_SUB_CASE, +} from '../../../common/constants'; async function getSubCase({ caseService, @@ -224,10 +227,14 @@ async function getCombinedCase({ client, id, }), - service.getSubCase({ - client, - id, - }), + ...(ENABLE_CASE_CONNECTOR + ? [ + service.getSubCase({ + client, + id, + }), + ] + : [Promise.reject('case connector feature is disabled')]), ]); if (subCasePromise.status === 'fulfilled') { @@ -287,6 +294,12 @@ export const addComment = async ({ ); if (isCommentRequestTypeGenAlert(comment)) { + if (!ENABLE_CASE_CONNECTOR) { + throw Boom.badRequest( + 'Attempting to add a generated alert when case connector feature is disabled' + ); + } + return addGeneratedAlerts({ caseId, comment, diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index fa2b10a0ccbdb..8a025ed0f79b7 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -886,7 +886,34 @@ describe('case connector', () => { }); }); - describe('execute', () => { + it('should throw an error when executing the connector', async () => { + expect.assertions(2); + const actionId = 'some-id'; + const params: CaseExecutorParams = { + // @ts-expect-error + subAction: 'not-supported', + // @ts-expect-error + subActionParams: {}, + }; + + const executorOptions: CaseActionTypeExecutorOptions = { + actionId, + config: {}, + params, + secrets: {}, + services, + }; + + try { + await caseActionType.executor(executorOptions); + } catch (e) { + expect(e).not.toBeNull(); + expect(e.message).toBe('[Action][Case] connector not supported'); + } + }); + + // ENABLE_CASE_CONNECTOR: enable these tests after the case connector feature is completed + describe.skip('execute', () => { it('allows only supported sub-actions', async () => { expect.assertions(2); const actionId = 'some-id'; diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index da993faf0ef5c..c5eb609e260ae 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -27,6 +27,7 @@ import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; import { nullUser } from '../../common'; import { createCaseError } from '../../common/error'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; @@ -70,6 +71,12 @@ async function executor( }: GetActionTypeParams, execOptions: CaseActionTypeExecutorOptions ): Promise> { + if (!ENABLE_CASE_CONNECTOR) { + const msg = '[Action][Case] connector not supported'; + logger.error(msg); + throw new Error(msg); + } + const { actionId, params, services } = execOptions; const { subAction, subActionParams } = params; let data: CaseExecutorResponse | null = null; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 0c661cc18c21b..8b53fd77d98a5 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -10,7 +10,7 @@ import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; -import { APP_ID } from '../common/constants'; +import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -70,7 +70,6 @@ export class CasePlugin { core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); core.savedObjects.registerType(caseSavedObjectType); - core.savedObjects.registerType(subCaseSavedObjectType); core.savedObjects.registerType(caseUserActionSavedObjectType); this.log.debug( @@ -111,15 +110,18 @@ export class CasePlugin { router, }); - registerConnectors({ - actionsRegisterType: plugins.actions.registerType, - logger: this.log, - caseService: this.caseService, - caseConfigureService: this.caseConfigureService, - connectorMappingsService: this.connectorMappingsService, - userActionService: this.userActionService, - alertsService: this.alertsService, - }); + if (ENABLE_CASE_CONNECTOR) { + core.savedObjects.registerType(subCaseSavedObjectType); + registerConnectors({ + actionsRegisterType: plugins.actions.registerType, + logger: this.log, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + connectorMappingsService: this.connectorMappingsService, + userActionService: this.userActionService, + alertsService: this.alertsService, + }); + } } public start(core: CoreStart) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts index fd250b74fff1e..7f6cfb224fada 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts @@ -5,12 +5,12 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; - import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { AssociationType } from '../../../../../common/api'; export function initDeleteAllCommentsApi({ @@ -35,18 +35,23 @@ export function initDeleteAllCommentsApi({ }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const client = context.core.savedObjects.client; // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - const id = request.query?.subCaseId ?? request.params.case_id; + const subCaseId = request.query?.subCaseId; + const id = subCaseId ?? request.params.case_id; const comments = await caseService.getCommentsByAssociation({ client, id, - associationType: request.query?.subCaseId - ? AssociationType.subCase - : AssociationType.case, + associationType: subCaseId ? AssociationType.subCase : AssociationType.case, }); await Promise.all( @@ -66,7 +71,7 @@ export function initDeleteAllCommentsApi({ actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: request.params.case_id, - subCaseId: request.query?.subCaseId, + subCaseId, commentId: comment.id, fields: ['comment'], }) diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index f1c5fdc2b7cc8..f8771f92c417f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -12,7 +12,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_obje import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; export function initDeleteCommentApi({ caseService, @@ -37,6 +37,12 @@ export function initDeleteCommentApi({ }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const client = context.core.savedObjects.client; // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index 57ddd84e8742c..9468b2b01fe37 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -22,7 +22,7 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, transformComments, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { defaultPage, defaultPerPage } from '../..'; const FindQueryParamsRt = rt.partial({ @@ -49,6 +49,12 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe fold(throwErrors(Boom.badRequest), identity) ); + if (!ENABLE_CASE_CONNECTOR && query.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const id = query.subCaseId ?? request.params.case_id; const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; const args = query diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 770efe0109744..2699f7a0307f7 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -5,13 +5,14 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { SavedObjectsFindResponse } from 'kibana/server'; import { AllCommentsResponseRt, CommentAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObjects, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { defaultSortField } from '../../../../common'; export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { @@ -35,6 +36,16 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps const client = context.core.savedObjects.client; let comments: SavedObjectsFindResponse; + if ( + !ENABLE_CASE_CONNECTOR && + (request.query?.subCaseId !== undefined || + request.query?.includeSubCaseComments !== undefined) + ) { + throw Boom.badRequest( + 'The `subCaseId` and `includeSubCaseComments` are not supported when the case connector feature is disabled' + ); + } + if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ client, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts index f5db2dc004a1d..519692d2d78a1 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts @@ -19,7 +19,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_obje import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError, decodeCommentRequest } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { CaseServiceSetup } from '../../../../services'; interface CombinedCaseParams { @@ -82,6 +82,12 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } + const client = context.core.savedObjects.client; const query = pipe( CommentPatchRequestRt.decode(request.body), diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts index 110a16a610014..8658f9ba0aac5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts @@ -5,10 +5,11 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { escapeHatch, wrapError } from '../../utils'; import { RouteDeps } from '../../types'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; import { CommentRequest } from '../../../../../common/api'; export function initPostCommentApi({ router, logger }: RouteDeps) { @@ -28,15 +29,21 @@ export function initPostCommentApi({ router, logger }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.cases) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } - const casesClient = context.cases.getCasesClient(); - const caseId = request.query?.subCaseId ?? request.params.case_id; - const comment = request.body as CommentRequest; + if (!context.cases) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + + const casesClient = context.cases.getCasesClient(); + const caseId = request.query?.subCaseId ?? request.params.case_id; + const comment = request.body as CommentRequest; - try { return response.ok({ body: await casesClient.addComment({ caseId, comment }), }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 5f2a6c67220c3..d91859d4e8cbb 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -11,7 +11,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; async function deleteSubCases({ @@ -91,7 +91,10 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log ); } - await deleteSubCases({ caseService, client, caseIds: request.query.ids }); + if (ENABLE_CASE_CONNECTOR) { + await deleteSubCases({ caseService, client, caseIds: request.query.ids }); + } + // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); @@ -104,7 +107,14 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: id, - fields: ['comment', 'description', 'status', 'tags', 'title', 'sub_case'], + fields: [ + 'comment', + 'description', + 'status', + 'tags', + 'title', + ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), + ], }) ), }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index f464f7e47fe7a..e8e35d875f42f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -7,9 +7,10 @@ import { schema } from '@kbn/config-schema'; +import Boom from '@hapi/boom'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { CASE_DETAILS_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( @@ -27,6 +28,11 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { + if (!ENABLE_CASE_CONNECTOR && request.query.includeSubCaseComments !== undefined) { + throw Boom.badRequest( + 'The `subCaseId` is not supported when the case connector feature is disabled' + ); + } const casesClient = context.cases.getCasesClient(); const id = request.params.case_id; diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index 12d1da36077c7..c5b7aa85dc33e 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -37,6 +37,7 @@ import { initGetSubCaseApi } from './cases/sub_case/get_sub_case'; import { initPatchSubCasesApi } from './cases/sub_case/patch_sub_cases'; import { initFindSubCasesApi } from './cases/sub_case/find_sub_cases'; import { initDeleteSubCasesApi } from './cases/sub_case/delete_sub_cases'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** * Default page number when interacting with the saved objects API. @@ -56,12 +57,16 @@ export function initCaseApi(deps: RouteDeps) { initPostCaseApi(deps); initPushCaseApi(deps); initGetAllCaseUserActionsApi(deps); - initGetAllSubCaseUserActionsApi(deps); - // Sub cases - initGetSubCaseApi(deps); - initPatchSubCasesApi(deps); - initFindSubCasesApi(deps); - initDeleteSubCasesApi(deps); + + if (ENABLE_CASE_CONNECTOR) { + // Sub cases + initGetAllSubCaseUserActionsApi(deps); + initGetSubCaseApi(deps); + initPatchSubCasesApi(deps); + initFindSubCasesApi(deps); + initDeleteSubCasesApi(deps); + } + // Comments initDeleteCommentApi(deps); initDeleteAllCommentsApi(deps); diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 11ceb48d11e9f..7c5f06d48bb05 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -34,6 +34,7 @@ import { caseTypeField, CasesFindRequest, } from '../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; import { combineFilters, defaultSortField, groupTotalAlertsByID } from '../common'; import { defaultPage, defaultPerPage } from '../routes/api'; import { @@ -282,13 +283,15 @@ export class CaseService implements CaseServiceSetup { options: caseOptions, }); - const subCasesResp = await this.findSubCasesGroupByCase({ - client, - options: subCaseOptions, - ids: cases.saved_objects - .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) - .map((caseInfo) => caseInfo.id), - }); + const subCasesResp = ENABLE_CASE_CONNECTOR + ? await this.findSubCasesGroupByCase({ + client, + options: subCaseOptions, + ids: cases.saved_objects + .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) + .map((caseInfo) => caseInfo.id), + }) + : { subCasesMap: new Map(), page: 0, perPage: 0 }; const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => { const subCasesForCase = subCasesResp.subCasesMap.get(caseInfo.id); @@ -407,7 +410,7 @@ export class CaseService implements CaseServiceSetup { let subCasesTotal = 0; - if (subCaseOptions) { + if (ENABLE_CASE_CONNECTOR && subCaseOptions) { subCasesTotal = await this.findSubCaseStatusStats({ client, options: subCaseOptions, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 143384d160471..4c62179f9ed54 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ENABLE_CASE_CONNECTOR } from '../../cases/common/constants'; + export const APP_ID = 'securitySolution'; export const SERVER_APP_ID = 'siem'; export const APP_NAME = 'Security'; @@ -171,7 +173,6 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID]; /* Rule notifications options */ -export const ENABLE_CASE_CONNECTOR = true; export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx index 5dbe1f1cef5be..fb71c6c4b0350 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx @@ -94,7 +94,26 @@ describe('RuleActionsField', () => { `); }); - it('if we do NOT have an error on case action creation, we are supporting case connector', () => { + // sub-cases-enabled: remove this once the sub cases and connector feature is completed + // https://github.com/elastic/kibana/issues/94115 + it('should not contain the case connector as a supported action', () => { + expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".jira", + "minimumLicenseRequired": "gold", + "name": "My Jira", + }, + ] + `); + }); + + // sub-cases-enabled: unskip after sub cases and the case connector is supported + // https://github.com/elastic/kibana/issues/94115 + it.skip('if we do NOT have an error on case action creation, we are supporting case connector', () => { expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(` Array [ Object { diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index 7cb66b6815b98..fece9abd5fa35 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -85,7 +85,28 @@ export default ({ getService }: FtrProviderContext): void => { .expect(404); }); - describe('sub case comments', () => { + it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('subCaseId'); + }); + + it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index 7bbc8e344ee23..44ff1c7ebffe1 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -111,7 +111,18 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - describe('sub case comments', () => { + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments/_find?search=unique&subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts index 723c9eba33beb..e73614d88ca95 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts @@ -24,13 +24,6 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); describe('get_all_comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); afterEach(async () => { await deleteAllCaseItems(es); }); @@ -63,47 +56,78 @@ export default ({ getService }: FtrProviderContext): void => { expect(comments.length).to.eql(2); }); - it('should get comments from a case and its sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?subCaseId=value`) .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) - .expect(200); + .send() + .expect(400); - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); + expect(body.message).to.contain('subCaseId'); }); - it('should get comments from a sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) + it('should return a 400 when passing the includeSubCaseComments parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?includeSubCaseComments=true`) .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .expect(200); + .send() + .expect(400); - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); + expect(body.message).to.contain('includeSubCaseComments'); }); - it('should not find any comments for an invalid case id', async () => { - const { body } = await supertest - .get(`${CASES_URL}/fake-id/comments`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - expect(body.length).to.eql(0); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + + it('should get comments from a case and its sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should get comments from a sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should not find any comments for an invalid case id', async () => { + const { body } = await supertest + .get(`${CASES_URL}/fake-id/comments`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 1a1bb727bd429..a74d8f86d225b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -24,13 +24,6 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); describe('get_comment', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); afterEach(async () => { await deleteAllCaseItems(es); }); @@ -57,14 +50,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(comment).to.eql(patchedCase.comments[0]); }); - it('should get a sub case comment', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: comment }: { body: CommentResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) - .expect(200); - expect(comment.type).to.be(CommentType.generatedAlert); - }); - it('unhappy path - 404s when comment is not there', async () => { await supertest .get(`${CASES_URL}/fake-id/comments/fake-comment`) @@ -72,5 +57,23 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(404); }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + it('should get a sub case comment', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const { body: comment }: { body: CommentResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) + .expect(200); + expect(comment.type).to.be(CommentType.generatedAlert); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index bddc620535dda..b59a248ee07e4 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -39,7 +39,28 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - describe('sub case comments', () => { + it('should return a 400 when the subCaseId parameter is passed', async () => { + const { body } = await supertest + .patch(`${CASES_URL}/case-id}/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send({ + id: 'id', + version: 'version', + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }) + .expect(400); + + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index 5e48e39164e6b..c46698a0905b4 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -227,7 +227,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - it('400s when adding an alert to a collection case', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('400s when adding an alert to a collection case', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -376,7 +377,17 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('sub case comments', () => { + it('should return a 400 when passing the subCaseId', async () => { + const { body } = await supertest + .post(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(400); + expect(body.message).to.contain('subCaseId'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index b5187931a9f01..706fded263282 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -90,7 +90,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(404); }); - describe('sub cases', () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { let actionID: string; before(async () => { actionID = await createCaseAction(supertest); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index b808ff4ccdf35..7277c93b7b75b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -248,7 +248,8 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.count_in_progress_cases).to.eql(1); }); - describe('stats with sub cases', () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('stats with sub cases', () => { let collection: CreateSubCaseResp; let actionID: string; before(async () => { diff --git a/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts index fb4ab2c86469a..43bd7a616e729 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts @@ -43,6 +43,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql(postCaseResp(postedCase.id)); }); + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('subCaseId'); + }); + it('unhappy path - 404s when case is not there', async () => { await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index c202111f0e5e4..f43b47da19ade 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -134,7 +134,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(404); }); - it('should 400 and not allow converting a collection back to an individual case', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should 400 and not allow converting a collection back to an individual case', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -156,7 +157,8 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - it('should allow converting an individual case to a collection when it does not have alerts', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -212,7 +214,30 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); }); - it("should 400 when attempting to update a collection case's status", async () => { + it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.collection, + }, + ], + }) + .expect(400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip("should 400 when attempting to update a collection case's status", async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index 2db15eb603f7c..47842eeca6f1c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -234,7 +234,8 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.status).to.eql('closed'); }); - it('should push a collection case but not close it when closure_type: close-by-pushing', async () => { + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { body: connector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts index d179120cd3d85..15b3b9311e3c3 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts @@ -26,75 +26,87 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - describe('delete_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('delete_sub_cases disabled routes', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["sub-case-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); }); - it('should delete a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('delete_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); - const { body: subCase } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(200); + it('should delete a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); - expect(subCase.id).to.not.eql(undefined); + const { body: subCase } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(200); - const { body } = await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + expect(subCase.id).to.not.eql(undefined); - expect(body).to.eql({}); - await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(404); - }); + const { body } = await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); - it(`should delete a sub case's comments when that case gets deleted`, async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(404); + }); - // there should be two comments on the sub case now - const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .query({ subCaseId: caseInfo.subCases![0].id }) - .send(postCommentUserReq) - .expect(200); + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); - const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.comments![1].id - }`; - // make sure we can get the second comment - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + // there should be two comments on the sub case now + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseId: caseInfo.subCases![0].id }) + .send(postCommentUserReq) + .expect(200); - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); - }); + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); - it('unhappy path - 404s when sub case id is invalid', async () => { - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + + it('unhappy path - 404s when sub case id is invalid', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts index 2c1bd9c7bd883..b7f2094b0acc3 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts @@ -29,226 +29,234 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('find_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('find_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(`${getSubCasesUrl('case-id')}/_find`).expect(404); }); - it('should not find any sub cases when none exist', async () => { - const { body: caseResp }: { body: CaseResponse } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - const { body: findSubCases } = await supertest - .get(`${getSubCasesUrl(caseResp.id)}/_find`) - .expect(200); - - expect(findSubCases).to.eql({ - page: 1, - per_page: 20, - total: 0, - subCases: [], - count_open_cases: 0, - count_closed_cases: 0, - count_in_progress_cases: 0, + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('find_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); }); - }); - - it('should return a sub cases with comment stats', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 1, - // find should not return the comments themselves only the stats - subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], - count_open_cases: 1, + after(async () => { + await deleteCaseAction(supertest, actionID); }); - }); - - it('should return multiple sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 2, - // find should not return the comments themselves only the stats - subCases: [ - { - // there should only be 1 closed sub case - ...subCase2Resp.modifiedSubCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - status: CaseStatuses.closed, - }, - { - ...subCase2Resp.newSubCaseInfo.subCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - }, - ], - count_open_cases: 1, - count_closed_cases: 1, + afterEach(async () => { + await deleteAllCaseItems(es); }); - }); - - it('should only return open when filtering for open', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) - .expect(200); - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should only return closed when filtering for closed', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + it('should not find any sub cases when none exist', async () => { + const { body: caseResp }: { body: CaseResponse } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCollectionReq) + .expect(200); + + const { body: findSubCases } = await supertest + .get(`${getSubCasesUrl(caseResp.id)}/_find`) + .expect(200); + + expect(findSubCases).to.eql({ + page: 1, + per_page: 20, + total: 0, + subCases: [], + count_open_cases: 0, + count_closed_cases: 0, + count_in_progress_cases: 0, + }); + }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) - .expect(200); + it('should return a sub cases with comment stats', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); - it('should only return in progress when filtering for in progress', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + expect(body).to.eql({ + ...findSubCasesResp, + total: 1, + // find should not return the comments themselves only the stats + subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], + count_open_cases: 1, + }); }); - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + it('should return multiple sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); + + expect(body).to.eql({ + ...findSubCasesResp, + total: 2, + // find should not return the comments themselves only the stats + subCases: [ + { + // there should only be 1 closed sub case + ...subCase2Resp.modifiedSubCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + status: CaseStatuses.closed, + }, + { + ...subCase2Resp.newSubCaseInfo.subCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + }, + ], + count_open_cases: 1, + count_closed_cases: 1, + }); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) - .expect(200); + it('should only return open when filtering for open', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); - }); + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) + .expect(200); - it('should sort on createdAt field in descending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) - .expect(200); + it('should only return closed when filtering for closed', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.subCases[1].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) + .expect(200); - it('should sort on createdAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should sort on updatedAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, + it('should only return in progress when filtering for in progress', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); }); - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + it('should sort on createdAt field in descending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.subCases[1].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); }); - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) - .expect(200); + it('should sort on createdAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); + it('should sort on updatedAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts index 440731cd07fe7..8d4ffafbf763a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts @@ -37,79 +37,87 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('get_sub_case', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('get_sub_case disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(getSubCaseDetailsUrl('case-id', 'sub-case-id')).expect(404); }); - it('should return a case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('get_sub_case', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + it('should return a case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], - associationType: AssociationType.subCase, - }) - ); + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) - ); - }); + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], + associationType: AssociationType.subCase, + }) + ); - it('should return the correct number of alerts with multiple types of alerts', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) + ); + }); - const { body: singleAlert }: { body: CaseResponse } = await supertest - .post(getCaseCommentsUrl(caseInfo.id)) - .query({ subCaseId: caseInfo.subCases![0].id }) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); + it('should return the correct number of alerts with multiple types of alerts', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const { body: singleAlert }: { body: CaseResponse } = await supertest + .post(getCaseCommentsUrl(caseInfo.id)) + .query({ subCaseId: caseInfo.subCases![0].id }) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [ - { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, - { - comment: postCommentAlertReq, - id: singleAlert.comments![1].id, - }, - ], - associationType: AssociationType.subCase, - }) - ); + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) - ); - }); + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [ + { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, + { + comment: postCommentAlertReq, + id: singleAlert.comments![1].id, + }, + ], + associationType: AssociationType.subCase, + }) + ); + + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) + ); + }); - it('unhappy path - 404s when case is not there', async () => { - await supertest - .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index e5cc2489a12e9..d993a627d186b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -36,463 +36,475 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); - describe('patch_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - beforeEach(async () => { - await esArchiver.load('cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/default'); - await deleteAllCaseItems(es); + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('patch_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .patch(SUB_CASES_PATCH_DEL_URL) + .set('kbn-xsrf', 'true') + .send({ subCases: [] }) + .expect(404); }); - it('should update the status of a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('patch_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); }); - const { body: subCase }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .expect(200); - - expect(subCase.status).to.eql(CaseStatuses['in-progress']); - }); - - it('should update the status of one alert attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - }, + after(async () => { + await deleteCaseAction(supertest, actionID); }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); }); - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of multiple alerts attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + it('should update the status of a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, + await setStatus({ + supertest, + cases: [ { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, - ]), - type: CommentType.generatedAlert, - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); + ], + type: 'sub_case', + }); + const { body: subCase }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .expect(200); - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], + expect(subCase.status).to.eql(CaseStatuses['in-progress']); }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], + it('should update the status of one alert attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, - ], - type: 'sub_case', - }); + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); - it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, - }, - }, - comment: { - alerts: createAlertsString([ + await setStatus({ + supertest, + cases: [ { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, - ]), - type: CommentType.generatedAlert, - }, - }); + ], + type: 'sub_case', + }); - // This will close the first sub case and create a new one - const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ - supertest, - actionID, - caseID: initialCaseInfo.id, - comment: { - alerts: createAlertsString([ - { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - }, - }); + await es.indices.refresh({ index: defaultSignalsIndex }); - await es.indices.refresh({ index: defaultSignalsIndex }); + signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: collectionWithSecondSub.subCases![0].id, - version: collectionWithSecondSub.subCases![0].version, - status: CaseStatuses['in-progress'], + it('should update the status of multiple alerts attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, - ], - type: 'sub_case', - }); + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - // There still should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + await setStatus({ + supertest, cases: [ { - id: collectionWithSecondSub.id, - version: collectionWithSecondSub.version, - settings: { syncAlerts: true }, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, ], - }) - .expect(200); + type: 'sub_case', + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); - it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - const { body: individualCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, + const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, + }, + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, }); - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, + // This will close the first sub case and create a new one + const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ + supertest, + actionID, + caseID: initialCaseInfo.id, + comment: { + alerts: createAlertsString([ + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, }, - }, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - }, - }); + }); - const { body: updatedIndWithComment } = await supertest - .post(`${CASES_URL}/${individualCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + await es.indices.refresh({ index: defaultSignalsIndex }); - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); - const updatedIndWithStatus = ( await setStatus({ supertest, cases: [ { - id: updatedIndWithComment.id, - version: updatedIndWithComment.version, - status: CaseStatuses.closed, + id: collectionWithSecondSub.subCases![0].id, + version: collectionWithSecondSub.subCases![0].version, + status: CaseStatuses['in-progress'], }, ], - type: 'case', - }) - )[0]; // there should only be a single entry in the response + type: 'sub_case', + }); - await es.indices.refresh({ index: defaultSignalsIndex }); + await es.indices.refresh({ index: defaultSignalsIndex }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There still should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: collectionWithSecondSub.id, + version: collectionWithSecondSub.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); - // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); + it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { body: individualCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, }, - ], - }) - .expect(200); + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + }, + }); - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + const { body: updatedIndWithComment } = await supertest + .post(`${CASES_URL}/${individualCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, cases: [ { - id: updatedIndWithStatus.id, - version: updatedIndWithStatus.version, - settings: { syncAlerts: true }, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], }, ], - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); + type: 'sub_case', + }); - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + const updatedIndWithStatus = ( + await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment.id, + version: updatedIndWithComment.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + }) + )[0]; // there should only be a single entry in the response + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - }); + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: updatedIndWithStatus.id, + version: updatedIndWithStatus.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); - it('404s when sub case id is invalid', async () => { - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: 'fake-id', - version: 'blah', - status: CaseStatuses.open, - }, - ], - }) - .expect(404); - }); + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + }); - it('406s when updating invalid fields for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + it('404s when sub case id is invalid', async () => { + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: 'fake-id', + version: 'blah', + status: CaseStatuses.open, + }, + ], + }) + .expect(404); + }); - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - type: 'blah', - }, - ], - }) - .expect(406); + it('406s when updating invalid fields for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + type: 'blah', + }, + ], + }) + .expect(406); + }); }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index ee4d671f7880f..0f9cba4b51f75 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -36,7 +36,20 @@ export default ({ getService }: FtrProviderContext): void => { describe('case_connector', () => { let createdActionId = ''; - it('should return 200 when creating a case action successfully', async () => { + it('should return 400 when creating a case action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should return 200 when creating a case action successfully', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -70,7 +83,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('create', () => { + describe.skip('create', () => { it('should respond with a 400 Bad Request when creating a case without title', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') @@ -500,7 +513,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('update', () => { + describe.skip('update', () => { it('should respond with a 400 Bad Request when updating a case without id', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') @@ -624,7 +637,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('addComment', () => { + describe.skip('addComment', () => { it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 6fb108f69ad22..f7ff49727df33 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import type { estypes } from '@elastic/elasticsearch'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import * as st from 'supertest'; @@ -48,7 +48,7 @@ export const getSignalsWithES = async ({ indices: string | string[]; ids: string | string[]; }): Promise>>> => { - const signals = await es.search({ + const signals: ApiResponse> = await es.search({ index: indices, body: { size: 10000, From 50bdbfc18e7b121d38f64d015e5dbddf8b4ae552 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 25 Mar 2021 13:47:15 -0400 Subject: [PATCH 02/98] [Dashboard] Fix Title BWC (#95355) * allow saving blank string to panel title --- .../common/embeddable/embeddable_saved_object_converters.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index c67cd325572ff..96725d4405112 100644 --- a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -29,9 +29,6 @@ export function convertPanelStateToSavedDashboardPanel( panelState: DashboardPanelState, version: string ): SavedDashboardPanel { - const customTitle: string | undefined = panelState.explicitInput.title - ? (panelState.explicitInput.title as string) - : undefined; const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId; return { version, @@ -39,7 +36,7 @@ export function convertPanelStateToSavedDashboardPanel( gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), - ...(customTitle && { title: customTitle }), + ...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; } From 02fce982544a63efc20de4fe027ba66833bab9b8 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 25 Mar 2021 12:51:05 -0500 Subject: [PATCH 03/98] skip flaky test. #89389 --- .../tests/exception_operators_data_types/ip.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts index 521a5c01a1203..058ae16dac8a9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts @@ -626,7 +626,8 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql(['127.0.0.1', '127.0.0.3']); }); - it('will return 4 results if we have a list that excludes all ips', async () => { + // flaky https://github.com/elastic/kibana/issues/89389 + it.skip('will return 4 results if we have a list that excludes all ips', async () => { await importFile( supertest, 'ip', From 6a571486fc1905835cccc714cee7c3a3d3be8b43 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 25 Mar 2021 19:25:49 +0100 Subject: [PATCH 04/98] [Security Solution][Detections] Improves indicator match Cypress tests (#94913) * updates the data used in the test * adds matches test * adds enrichment test * improves speed and adds missing files * fixes type check issue * adds 'data-test-subj' for the json view tab * refactor * fixes typecheck issue * updates tests with latest master changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indicator_match_rule.spec.ts | 113 +- .../security_solution/cypress/objects/rule.ts | 14 +- .../cypress/screens/alerts_details.ts | 12 + .../cypress/screens/fields_browser.ts | 4 +- .../cypress/screens/rule_details.ts | 7 + .../cypress/tasks/alerts_details.ts | 17 + .../cypress/tasks/api_calls/rules.ts | 10 +- .../cypress/tasks/create_new_rule.ts | 2 +- .../cypress/tasks/fields_browser.ts | 11 +- .../cypress/tasks/rule_details.ts | 9 + .../event_details/event_details.tsx | 1 + .../suspicious_source_event/data.json | 13 + .../suspicious_source_event/mappings.json | 29 + .../es_archives/threat_data/data.json.gz | Bin 1086 -> 0 bytes .../es_archives/threat_data/mappings.json | 3577 ----------------- .../es_archives/threat_indicator/data.json | 71 +- .../threat_indicator/mappings.json | 800 +++- 17 files changed, 1075 insertions(+), 3615 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/screens/alerts_details.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts create mode 100644 x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json create mode 100644 x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json delete mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz delete mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index ef9c7f49cb371..e1e78f8e310e1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -16,6 +16,7 @@ import { ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; +import { JSON_LINES } from '../../screens/alerts_details'; import { CUSTOM_RULES_BTN, RISK_SCORE, @@ -50,14 +51,17 @@ import { SCHEDULE_DETAILS, SEVERITY_DETAILS, TAGS_DETAILS, + TIMELINE_FIELD, TIMELINE_TEMPLATE_DETAILS, } from '../../screens/rule_details'; import { + expandFirstAlert, goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; +import { openJsonView, scrollJsonViewToBottom } from '../../tasks/alerts_details'; import { changeRowsPerPageTo300, duplicateFirstRule, @@ -98,7 +102,7 @@ import { import { waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { goBackToAllRulesTable } from '../../tasks/rule_details'; +import { addsFieldsToTimeline, goBackToAllRulesTable } from '../../tasks/rule_details'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -114,11 +118,11 @@ describe('indicator match', () => { before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); - esArchiverLoad('threat_data'); + esArchiverLoad('suspicious_source_event'); }); after(() => { esArchiverUnload('threat_indicator'); - esArchiverUnload('threat_data'); + esArchiverUnload('suspicious_source_event'); }); describe('Creating new indicator match rules', () => { @@ -216,7 +220,7 @@ describe('indicator match', () => { it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getDefineContinueButton().click(); @@ -235,7 +239,7 @@ describe('indicator match', () => { it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: 'non-existent-value', validColumns: 'indexField', }); @@ -245,7 +249,7 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorAndButton().click(); @@ -267,14 +271,14 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: 'non-existent-value', validColumns: 'indexField', }); getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: 'second-non-existent-value', validColumns: 'indexField', }); @@ -305,7 +309,7 @@ describe('indicator match', () => { it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorDeleteButton().click(); @@ -317,7 +321,7 @@ describe('indicator match', () => { it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorAndButton().click(); @@ -330,16 +334,22 @@ describe('indicator match', () => { getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 3, - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorDeleteButton(2).click(); - getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorIndexComboField(1).should( + 'text', + newThreatIndicatorRule.indicatorMappingField + ); getIndicatorMappingComboField(1).should( 'text', newThreatIndicatorRule.indicatorIndexField ); - getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorIndexComboField(2).should( + 'text', + newThreatIndicatorRule.indicatorMappingField + ); getIndicatorMappingComboField(2).should( 'text', newThreatIndicatorRule.indicatorIndexField @@ -357,11 +367,14 @@ describe('indicator match', () => { getIndicatorOrButton().click(); fillIndicatorMatchRow({ rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, + indexField: newThreatIndicatorRule.indicatorMappingField, indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, }); getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorIndexComboField().should( + 'text', + newThreatIndicatorRule.indicatorMappingField + ); getIndicatorMappingComboField().should( 'text', newThreatIndicatorRule.indicatorIndexField @@ -441,7 +454,7 @@ describe('indicator match', () => { ); getDetails(INDICATOR_MAPPING).should( 'have.text', - `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + `${newThreatIndicatorRule.indicatorMappingField} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` ); getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); }); @@ -471,6 +484,74 @@ describe('indicator match', () => { }); }); + describe('Enrichment', () => { + const fieldSearch = 'threat.indicator.matched'; + const fields = [ + 'threat.indicator.matched.atomic', + 'threat.indicator.matched.type', + 'threat.indicator.matched.field', + ]; + const expectedFieldsText = [ + newThreatIndicatorRule.atomic, + newThreatIndicatorRule.type, + newThreatIndicatorRule.indicatorMappingField, + ]; + + const expectedEnrichment = [ + { line: 4, text: ' "threat": {' }, + { + line: 3, + text: + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + }, + { line: 2, text: ' }' }, + ]; + + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('suspicious_source_event'); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('suspicious_source_event'); + }); + + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + goToRuleDetails(); + }); + + it('Displays matches on the timeline', () => { + addsFieldsToTimeline(fieldSearch, fields); + + fields.forEach((field, index) => { + cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFieldsText[index]); + }); + }); + + it('Displays enrichment on the JSON view', () => { + expandFirstAlert(); + openJsonView(); + scrollJsonViewToBottom(); + + cy.get(JSON_LINES).then((elements) => { + const length = elements.length; + expectedEnrichment.forEach((enrichment) => { + cy.wrap(elements) + .eq(length - enrichment.line) + .should('have.text', enrichment.text); + }); + }); + }); + }); + describe('Duplicates the indicator rule', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 88dcd998fc06d..68c7796f7ca3b 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -71,8 +71,10 @@ export interface OverrideRule extends CustomRule { export interface ThreatIndicatorRule extends CustomRule { indicatorIndexPattern: string[]; - indicatorMapping: string; + indicatorMappingField: string; indicatorIndexField: string; + type?: string; + atomic?: string; } export interface MachineLearningRule { @@ -299,7 +301,7 @@ export const eqlSequenceRule: CustomRule = { export const newThreatIndicatorRule: ThreatIndicatorRule = { name: 'Threat Indicator Rule Test', description: 'The threat indicator rule description.', - index: ['threat-data-*'], + index: ['suspicious-*'], severity: 'Critical', riskScore: '20', tags: ['test', 'threat'], @@ -309,9 +311,11 @@ export const newThreatIndicatorRule: ThreatIndicatorRule = { note: '# test markdown', runsEvery, lookBack, - indicatorIndexPattern: ['threat-indicator-*'], - indicatorMapping: 'agent.id', - indicatorIndexField: 'agent.threat', + indicatorIndexPattern: ['filebeat-*'], + indicatorMappingField: 'myhash.mysha256', + indicatorIndexField: 'threatintel.indicator.file.hash.sha256', + type: 'file', + atomic: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', timeline, maxSignals: 100, }; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts new file mode 100644 index 0000000000000..417cf73de47f6 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const JSON_CONTENT = '[data-test-subj="jsonView"]'; + +export const JSON_LINES = '.ace_line'; + +export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index ea274c446c014..1115dfb00914e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -5,10 +5,12 @@ * 2.0. */ +export const CLOSE_BTN = '[data-test-subj="close"]'; + export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; export const FIELDS_BROWSER_CHECKBOX = (id: string) => { - return `[data-test-subj="field-${id}-checkbox`; + return `[data-test-subj="category-table-container"] [data-test-subj="field-${id}-checkbox"]`; }; export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index f9590b34a0a11..d94be17a0530a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -53,6 +53,9 @@ export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobS export const MITRE_ATTACK_DETAILS = 'MITRE ATT&CK'; +export const FIELDS_BROWSER_BTN = + '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]'; + export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]'; export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]'; @@ -92,6 +95,10 @@ export const TIMELINE_TEMPLATE_DETAILS = 'Timeline template'; export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override'; +export const TIMELINE_FIELD = (field: string) => { + return `[data-test-subj="draggable-content-${field}"]`; +}; + export const getDetails = (title: string) => cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts new file mode 100644 index 0000000000000..1582f35989e2c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JSON_CONTENT, JSON_VIEW_TAB } from '../screens/alerts_details'; + +export const openJsonView = () => { + cy.get(JSON_VIEW_TAB).click(); +}; + +export const scrollJsonViewToBottom = () => { + cy.get(JSON_CONTENT).click({ force: true }); + cy.get(JSON_CONTENT).type('{pagedown}{pagedown}{pagedown}'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 4bf5508c19aa9..0b051f3a26581 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -45,9 +45,9 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r { entries: [ { - field: rule.indicatorMapping, + field: rule.indicatorMappingField, type: 'mapping', - value: rule.indicatorMapping, + value: rule.indicatorIndexField, }, ], }, @@ -55,13 +55,13 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r threat_query: '*:*', threat_language: 'kuery', threat_filters: [], - threat_index: ['mock*'], + threat_index: rule.indicatorIndexPattern, threat_indicator_path: '', from: 'now-17520h', - index: ['exceptions-*'], + index: rule.index, query: rule.customQuery || '*:*', language: 'kuery', - enabled: false, + enabled: true, }, headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index b317f158ae614..0c663a95a4bda 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -426,7 +426,7 @@ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => { fillIndexAndIndicatorIndexPattern(rule.index, rule.indicatorIndexPattern); fillIndicatorMatchRow({ - indexField: rule.indicatorMapping, + indexField: rule.indicatorMappingField, indicatorIndexField: rule.indicatorIndexField, }); getDefineContinueButton().should('exist').click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index 9ee242dcebbe8..72945f557ac1b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -15,8 +15,15 @@ import { FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX, FIELDS_BROWSER_MESSAGE_CHECKBOX, FIELDS_BROWSER_RESET_FIELDS, + FIELDS_BROWSER_CHECKBOX, + CLOSE_BTN, } from '../screens/fields_browser'; -import { KQL_SEARCH_BAR } from '../screens/hosts/main'; + +export const addsFields = (fields: string[]) => { + fields.forEach((field) => { + cy.get(FIELDS_BROWSER_CHECKBOX(field)).click(); + }); +}; export const addsHostGeoCityNameToTimeline = () => { cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX).check({ @@ -44,7 +51,7 @@ export const clearFieldsBrowser = () => { }; export const closeFieldsBrowser = () => { - cy.get(KQL_SEARCH_BAR).click({ force: true }); + cy.get(CLOSE_BTN).click({ force: true }); }; export const filterFieldsBrowser = (fieldName: string) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 21a2745395419..37c425c5488bc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -20,10 +20,12 @@ import { ALERTS_TAB, BACK_TO_RULES, EXCEPTIONS_TAB, + FIELDS_BROWSER_BTN, REFRESH_BUTTON, REMOVE_EXCEPTION_BTN, RULE_SWITCH, } from '../screens/rule_details'; +import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser'; export const activatesRule = () => { cy.intercept('PATCH', '/api/detection_engine/rules/_bulk_update').as('bulk_update'); @@ -49,6 +51,13 @@ export const addsException = (exception: Exception) => { cy.get(CONFIRM_BTN).should('not.exist'); }; +export const addsFieldsToTimeline = (search: string, fields: string[]) => { + cy.get(FIELDS_BROWSER_BTN).click(); + filterFieldsBrowser(search); + addsFields(fields); + closeFieldsBrowser(); +}; + export const openExceptionModalFromRuleSettings = () => { cy.get(ADD_EXCEPTIONS_BTN).click(); cy.get(LOADING_SPINNER).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index ddb3d98cafca8..4979d70ce2d7b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -107,6 +107,7 @@ const EventDetailsComponent: React.FC = ({ }, { id: EventsViewType.jsonView, + 'data-test-subj': 'jsonViewTab', name: i18n.JSON_VIEW, content: ( <> diff --git a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json new file mode 100644 index 0000000000000..11b5e9bd0828b --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json @@ -0,0 +1,13 @@ +{ + "type": "doc", + "value": { + "id": "_eZE7mwBOpWiDweStB_c", + "index": "suspicious-source-event-001", + "source": { + "@timestamp": "2021-02-22T21:00:49.337Z", + "myhash": { + "mysha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json new file mode 100644 index 0000000000000..83b2b4d64a510 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/mappings.json @@ -0,0 +1,29 @@ +{ + "type": "index", + "value": { + "aliases": { + "thread-data": { + "is_write_index": false + }, + "beats": { + }, + "siem-read-alias": { + } + }, + "index": "suspicious-source-event-001", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "myhash": { + "properties": { + "mysha256": { + "type": "keyword" + } + } + } + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/threat_data/data.json.gz deleted file mode 100644 index ab63f9a47a7baa9e6ea0e830f55ec8c5e48df4c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1086 zcmV-E1i||siwFpR@w;CD17u-zVJ>QOZ*Bm+R!eW&I1s+)R|GzFfyyCeJq;|d2+~8b zD0*lX1+rKe6g9HBmPmo5?6^h#`;xNkkdlISFAah?o;PQ{`N;1#x3>#@YGJXyU6g_@ z-dn+e)SZ=lH($(GR$A=_o<5|_@&0rBl|3Bci@x8S&8-D5;n^DLodlwTl4uejgfDs} zI!Rw68p$7;HJ~(UTI&`foCnDK;zxwm5niKYiE#Qf_#1n&1+JX{Mg;8+8jz&koC{2F zM#DUTAdagvh;o90EJJC4(yTNJicpze0~-IGP@0pbKf3B9qqb-!j>I)Ohej((3q&EP zl9&cjnC1aVY{_wj%eW{ILWS#f=_u(+rVG;%S9t)bnBZ2QEzuG!2Gz^;u(W2A(-tQU z%7`N5R@ZkAqa_Y)s1Un(T0-}rtq*pkLfXiyEQ+$3#G)(xyyQSwO$t^secF5zygyf` z0%|HWy~lyyE^cPZy-_=D%u^mqu6maX7l5?TD&-!8bWuBPZC`^&v9TY zDTyqDa6UpS#lJxHe5p_qr5OzrgXT^511mvV>n&}kQ!EX>87KNY>zPr8GowuMWf(`x z;q#}*nW0H~pvq6{;0`bG9PZ#SfgPbk{RY7(9<*>xA(yr0(iaMfBKUb=@<4jd`u7cll-u| zzf+$-Ztp(+?(I2~a3vJc=|Xg7WoJn)bgxrMxEh#lp}lrp37@rxXu2GRq$wyh-jA&s z1Lm$%@~&X~u083U;48nYSM64aZ4Dc9PtyHH?cum72{h(Bv+$z!F$4~OWkHxf;>1wP zI$jxqM;?E{Gtf?x;!IWJik9gdCyWa6df87W4dY2y6v#t=asBd3$!2Dw=fQP^136Ef z#;?a;^&A=s^1)-DbYoH&ZZuzNC%WxtfZmV9-K==tm~m}b!@P36`x`BNh+fHMV{6%v zvXp1sFVJ&kezGb`qGXjog3#D;sKyb#+>HNwZAz!c(D~Ub>*n(J<>uw)KU(UR5_${( E0C|!QdjJ3c diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json deleted file mode 100644 index 3ccdee6bdb5eb..0000000000000 --- a/x-pack/test/security_solution_cypress/es_archives/threat_data/mappings.json +++ /dev/null @@ -1,3577 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - "thread-data": { - "is_write_index": false - }, - "beats": { - }, - "siem-read-alias": { - } - }, - "index": "threat-data-001", - "mappings": { - "_meta": { - "beat": "auditbeat", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "auditd": { - "properties": { - "data": { - "properties": { - "a0": { - "ignore_above": 1024, - "type": "keyword" - }, - "a1": { - "ignore_above": 1024, - "type": "keyword" - }, - "a2": { - "ignore_above": 1024, - "type": "keyword" - }, - "a3": { - "ignore_above": 1024, - "type": "keyword" - }, - "a[0-3]": { - "ignore_above": 1024, - "type": "keyword" - }, - "acct": { - "ignore_above": 1024, - "type": "keyword" - }, - "acl": { - "ignore_above": 1024, - "type": "keyword" - }, - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "added": { - "ignore_above": 1024, - "type": "keyword" - }, - "addr": { - "ignore_above": 1024, - "type": "keyword" - }, - "apparmor": { - "ignore_above": 1024, - "type": "keyword" - }, - "arch": { - "ignore_above": 1024, - "type": "keyword" - }, - "argc": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_backlog_limit": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_backlog_wait_time": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_enabled": { - "ignore_above": 1024, - "type": "keyword" - }, - "audit_failure": { - "ignore_above": 1024, - "type": "keyword" - }, - "banners": { - "ignore_above": 1024, - "type": "keyword" - }, - "bool": { - "ignore_above": 1024, - "type": "keyword" - }, - "bus": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fe": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fi": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fp": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fver": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "capability": { - "ignore_above": 1024, - "type": "keyword" - }, - "cgroup": { - "ignore_above": 1024, - "type": "keyword" - }, - "changed": { - "ignore_above": 1024, - "type": "keyword" - }, - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "cmd": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "compat": { - "ignore_above": 1024, - "type": "keyword" - }, - "daddr": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "default-context": { - "ignore_above": 1024, - "type": "keyword" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "dir": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "dmac": { - "ignore_above": 1024, - "type": "keyword" - }, - "dport": { - "ignore_above": 1024, - "type": "keyword" - }, - "enforcing": { - "ignore_above": 1024, - "type": "keyword" - }, - "entries": { - "ignore_above": 1024, - "type": "keyword" - }, - "exit": { - "ignore_above": 1024, - "type": "keyword" - }, - "fam": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "fd": { - "ignore_above": 1024, - "type": "keyword" - }, - "fe": { - "ignore_above": 1024, - "type": "keyword" - }, - "feature": { - "ignore_above": 1024, - "type": "keyword" - }, - "fi": { - "ignore_above": 1024, - "type": "keyword" - }, - "file": { - "ignore_above": 1024, - "type": "keyword" - }, - "flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "format": { - "ignore_above": 1024, - "type": "keyword" - }, - "fp": { - "ignore_above": 1024, - "type": "keyword" - }, - "fver": { - "ignore_above": 1024, - "type": "keyword" - }, - "grantors": { - "ignore_above": 1024, - "type": "keyword" - }, - "grp": { - "ignore_above": 1024, - "type": "keyword" - }, - "hook": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "icmp_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "igid": { - "ignore_above": 1024, - "type": "keyword" - }, - "img-ctx": { - "ignore_above": 1024, - "type": "keyword" - }, - "info": { - "ignore_above": 1024, - "type": "keyword" - }, - "inif": { - "ignore_above": 1024, - "type": "keyword" - }, - "ino": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode_gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode_uid": { - "ignore_above": 1024, - "type": "keyword" - }, - "invalid_context": { - "ignore_above": 1024, - "type": "keyword" - }, - "ioctlcmd": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "ignore_above": 1024, - "type": "keyword" - }, - "ipid": { - "ignore_above": 1024, - "type": "keyword" - }, - "ipx-net": { - "ignore_above": 1024, - "type": "keyword" - }, - "items": { - "ignore_above": 1024, - "type": "keyword" - }, - "iuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "ksize": { - "ignore_above": 1024, - "type": "keyword" - }, - "laddr": { - "ignore_above": 1024, - "type": "keyword" - }, - "len": { - "ignore_above": 1024, - "type": "keyword" - }, - "list": { - "ignore_above": 1024, - "type": "keyword" - }, - "lport": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "macproto": { - "ignore_above": 1024, - "type": "keyword" - }, - "maj": { - "ignore_above": 1024, - "type": "keyword" - }, - "major": { - "ignore_above": 1024, - "type": "keyword" - }, - "minor": { - "ignore_above": 1024, - "type": "keyword" - }, - "model": { - "ignore_above": 1024, - "type": "keyword" - }, - "msg": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "nargs": { - "ignore_above": 1024, - "type": "keyword" - }, - "net": { - "ignore_above": 1024, - "type": "keyword" - }, - "new": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-chardev": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-disk": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-enabled": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-fs": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-level": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-log_passwd": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-mem": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-net": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-range": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-rng": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-role": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-seuser": { - "ignore_above": 1024, - "type": "keyword" - }, - "new-vcpu": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_lock": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "new_pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "nlnk-fam": { - "ignore_above": 1024, - "type": "keyword" - }, - "nlnk-grp": { - "ignore_above": 1024, - "type": "keyword" - }, - "nlnk-pid": { - "ignore_above": 1024, - "type": "keyword" - }, - "oauid": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_uid": { - "ignore_above": 1024, - "type": "keyword" - }, - "ocomm": { - "ignore_above": 1024, - "type": "keyword" - }, - "oflag": { - "ignore_above": 1024, - "type": "keyword" - }, - "old": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-auid": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-chardev": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-disk": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-enabled": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-fs": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-level": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-log_passwd": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-mem": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-net": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-range": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-rng": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-role": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-ses": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-seuser": { - "ignore_above": 1024, - "type": "keyword" - }, - "old-vcpu": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_enforcing": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_lock": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pa": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_prom": { - "ignore_above": 1024, - "type": "keyword" - }, - "old_val": { - "ignore_above": 1024, - "type": "keyword" - }, - "op": { - "ignore_above": 1024, - "type": "keyword" - }, - "operation": { - "ignore_above": 1024, - "type": "keyword" - }, - "opid": { - "ignore_above": 1024, - "type": "keyword" - }, - "oses": { - "ignore_above": 1024, - "type": "keyword" - }, - "outif": { - "ignore_above": 1024, - "type": "keyword" - }, - "pa": { - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "ignore_above": 1024, - "type": "keyword" - }, - "per": { - "ignore_above": 1024, - "type": "keyword" - }, - "perm": { - "ignore_above": 1024, - "type": "keyword" - }, - "perm_mask": { - "ignore_above": 1024, - "type": "keyword" - }, - "permissive": { - "ignore_above": 1024, - "type": "keyword" - }, - "pfs": { - "ignore_above": 1024, - "type": "keyword" - }, - "pi": { - "ignore_above": 1024, - "type": "keyword" - }, - "pp": { - "ignore_above": 1024, - "type": "keyword" - }, - "printer": { - "ignore_above": 1024, - "type": "keyword" - }, - "profile": { - "ignore_above": 1024, - "type": "keyword" - }, - "prom": { - "ignore_above": 1024, - "type": "keyword" - }, - "proto": { - "ignore_above": 1024, - "type": "keyword" - }, - "qbytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "range": { - "ignore_above": 1024, - "type": "keyword" - }, - "reason": { - "ignore_above": 1024, - "type": "keyword" - }, - "removed": { - "ignore_above": 1024, - "type": "keyword" - }, - "res": { - "ignore_above": 1024, - "type": "keyword" - }, - "resrc": { - "ignore_above": 1024, - "type": "keyword" - }, - "rport": { - "ignore_above": 1024, - "type": "keyword" - }, - "sauid": { - "ignore_above": 1024, - "type": "keyword" - }, - "scontext": { - "ignore_above": 1024, - "type": "keyword" - }, - "selected-context": { - "ignore_above": 1024, - "type": "keyword" - }, - "seperm": { - "ignore_above": 1024, - "type": "keyword" - }, - "seperms": { - "ignore_above": 1024, - "type": "keyword" - }, - "seqno": { - "ignore_above": 1024, - "type": "keyword" - }, - "seresult": { - "ignore_above": 1024, - "type": "keyword" - }, - "ses": { - "ignore_above": 1024, - "type": "keyword" - }, - "seuser": { - "ignore_above": 1024, - "type": "keyword" - }, - "sig": { - "ignore_above": 1024, - "type": "keyword" - }, - "sigev_signo": { - "ignore_above": 1024, - "type": "keyword" - }, - "smac": { - "ignore_above": 1024, - "type": "keyword" - }, - "socket": { - "properties": { - "addr": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "ignore_above": 1024, - "type": "keyword" - }, - "saddr": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "spid": { - "ignore_above": 1024, - "type": "keyword" - }, - "sport": { - "ignore_above": 1024, - "type": "keyword" - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "subj": { - "ignore_above": 1024, - "type": "keyword" - }, - "success": { - "ignore_above": 1024, - "type": "keyword" - }, - "syscall": { - "ignore_above": 1024, - "type": "keyword" - }, - "table": { - "ignore_above": 1024, - "type": "keyword" - }, - "tclass": { - "ignore_above": 1024, - "type": "keyword" - }, - "tcontext": { - "ignore_above": 1024, - "type": "keyword" - }, - "terminal": { - "ignore_above": 1024, - "type": "keyword" - }, - "tty": { - "ignore_above": 1024, - "type": "keyword" - }, - "unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "uri": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "val": { - "ignore_above": 1024, - "type": "keyword" - }, - "ver": { - "ignore_above": 1024, - "type": "keyword" - }, - "virt": { - "ignore_above": 1024, - "type": "keyword" - }, - "vm": { - "ignore_above": 1024, - "type": "keyword" - }, - "vm-ctx": { - "ignore_above": 1024, - "type": "keyword" - }, - "vm-pid": { - "ignore_above": 1024, - "type": "keyword" - }, - "watch": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "paths": { - "properties": { - "cap_fe": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fi": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fp": { - "ignore_above": 1024, - "type": "keyword" - }, - "cap_fver": { - "ignore_above": 1024, - "type": "keyword" - }, - "dev": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "item": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "nametype": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_role": { - "ignore_above": 1024, - "type": "keyword" - }, - "obj_user": { - "ignore_above": 1024, - "type": "keyword" - }, - "objtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "ogid": { - "ignore_above": 1024, - "type": "keyword" - }, - "ouid": { - "ignore_above": 1024, - "type": "keyword" - }, - "rdev": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "sequence": { - "type": "long" - }, - "session": { - "ignore_above": 1024, - "type": "keyword" - }, - "summary": { - "properties": { - "actor": { - "properties": { - "primary": { - "ignore_above": 1024, - "type": "keyword" - }, - "secondary": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "how": { - "ignore_above": 1024, - "type": "keyword" - }, - "object": { - "properties": { - "primary": { - "ignore_above": 1024, - "type": "keyword" - }, - "secondary": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "client": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "instance": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "container": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "origin": { - "fields": { - "raw": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "selinux": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "role": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "setgid": { - "type": "boolean" - }, - "setuid": { - "type": "boolean" - }, - "size": { - "type": "long" - }, - "target_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "flow": { - "properties": { - "complete": { - "type": "boolean" - }, - "final": { - "type": "boolean" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geoip": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "blake2b_256": { - "ignore_above": 1024, - "type": "keyword" - }, - "blake2b_384": { - "ignore_above": 1024, - "type": "keyword" - }, - "blake2b_512": { - "ignore_above": 1024, - "type": "keyword" - }, - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha224": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha384": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_224": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_384": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha3_512": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512_224": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512_256": { - "ignore_above": 1024, - "type": "keyword" - }, - "xxh64": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "jolokia": { - "properties": { - "agent": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "secured": { - "type": "boolean" - }, - "server": { - "properties": { - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "properties": { - "annotations": { - "type": "object" - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "type": "object" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "observer": { - "properties": { - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "sha1": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "working_directory": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "ip": { - "type": "ip" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "socket": { - "properties": { - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "user": { - "properties": { - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "system": { - "properties": { - "audit": { - "properties": { - "host": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "boottime": { - "type": "date" - }, - "containerized": { - "type": "boolean" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timezone": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "offset": { - "properties": { - "sec": { - "type": "long" - } - } - } - } - }, - "uptime": { - "type": "long" - } - } - }, - "newsocket": { - "properties": { - "egid": { - "type": "long" - }, - "euid": { - "type": "long" - }, - "gid": { - "type": "long" - }, - "internal_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel_sock_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "type": "long" - } - } - }, - "package": { - "properties": { - "arch": { - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "installtime": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "release": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "summary": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "socket": { - "properties": { - "egid": { - "type": "long" - }, - "euid": { - "type": "long" - }, - "gid": { - "type": "long" - }, - "internal_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel_sock_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "type": "long" - } - } - }, - "user": { - "properties": { - "dir": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "properties": { - "last_changed": { - "type": "date" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "shell": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - }, - "user_information": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "properties": { - "audit": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "effective": { - "properties": { - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "filesystem": { - "properties": { - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "full_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "name_map": { - "type": "object" - }, - "saved": { - "properties": { - "group": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "selinux": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "role": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "terminal": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "lifecycle": { - "indexing_complete": "true", - "name": "auditbeat-8.0.0", - "rollover_alias": "auditbeat-8.0.0" - }, - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "client.address", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.user.email", - "client.user.full_name", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "ecs.version", - "error.code", - "error.id", - "error.message", - "event.action", - "event.category", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.timezone", - "event.type", - "file.device", - "file.extension", - "file.gid", - "file.group", - "file.inode", - "file.mode", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.id", - "group.name", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.email", - "host.user.full_name", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.original", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "process.args", - "process.executable", - "process.name", - "process.title", - "process.working_directory", - "server.address", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.user.email", - "server.user.full_name", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.user.email", - "source.user.full_name", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "url.domain", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.scheme", - "url.username", - "user.email", - "user.full_name", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "agent.hostname", - "error.type", - "cloud.project.id", - "host.os.build", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "jolokia.agent.version", - "jolokia.agent.id", - "jolokia.server.product", - "jolokia.server.version", - "jolokia.server.vendor", - "jolokia.url", - "raw", - "file.origin", - "file.selinux.user", - "file.selinux.role", - "file.selinux.domain", - "file.selinux.level", - "user.audit.id", - "user.audit.name", - "user.effective.id", - "user.effective.name", - "user.effective.group.id", - "user.effective.group.name", - "user.filesystem.id", - "user.filesystem.name", - "user.filesystem.group.id", - "user.filesystem.group.name", - "user.saved.id", - "user.saved.name", - "user.saved.group.id", - "user.saved.group.name", - "user.selinux.user", - "user.selinux.role", - "user.selinux.domain", - "user.selinux.level", - "user.selinux.category", - "source.path", - "destination.path", - "auditd.message_type", - "auditd.session", - "auditd.result", - "auditd.summary.actor.primary", - "auditd.summary.actor.secondary", - "auditd.summary.object.type", - "auditd.summary.object.primary", - "auditd.summary.object.secondary", - "auditd.summary.how", - "auditd.paths.inode", - "auditd.paths.dev", - "auditd.paths.obj_user", - "auditd.paths.obj_role", - "auditd.paths.obj_domain", - "auditd.paths.obj_level", - "auditd.paths.objtype", - "auditd.paths.ouid", - "auditd.paths.rdev", - "auditd.paths.nametype", - "auditd.paths.ogid", - "auditd.paths.item", - "auditd.paths.mode", - "auditd.paths.name", - "auditd.data.action", - "auditd.data.minor", - "auditd.data.acct", - "auditd.data.addr", - "auditd.data.cipher", - "auditd.data.id", - "auditd.data.entries", - "auditd.data.kind", - "auditd.data.ksize", - "auditd.data.spid", - "auditd.data.arch", - "auditd.data.argc", - "auditd.data.major", - "auditd.data.unit", - "auditd.data.table", - "auditd.data.terminal", - "auditd.data.grantors", - "auditd.data.direction", - "auditd.data.op", - "auditd.data.tty", - "auditd.data.syscall", - "auditd.data.data", - "auditd.data.family", - "auditd.data.mac", - "auditd.data.pfs", - "auditd.data.items", - "auditd.data.a0", - "auditd.data.a1", - "auditd.data.a2", - "auditd.data.a3", - "auditd.data.hostname", - "auditd.data.lport", - "auditd.data.rport", - "auditd.data.exit", - "auditd.data.fp", - "auditd.data.laddr", - "auditd.data.sport", - "auditd.data.capability", - "auditd.data.nargs", - "auditd.data.new-enabled", - "auditd.data.audit_backlog_limit", - "auditd.data.dir", - "auditd.data.cap_pe", - "auditd.data.model", - "auditd.data.new_pp", - "auditd.data.old-enabled", - "auditd.data.oauid", - "auditd.data.old", - "auditd.data.banners", - "auditd.data.feature", - "auditd.data.vm-ctx", - "auditd.data.opid", - "auditd.data.seperms", - "auditd.data.seresult", - "auditd.data.new-rng", - "auditd.data.old-net", - "auditd.data.sigev_signo", - "auditd.data.ino", - "auditd.data.old_enforcing", - "auditd.data.old-vcpu", - "auditd.data.range", - "auditd.data.res", - "auditd.data.added", - "auditd.data.fam", - "auditd.data.nlnk-pid", - "auditd.data.subj", - "auditd.data.a[0-3]", - "auditd.data.cgroup", - "auditd.data.kernel", - "auditd.data.ocomm", - "auditd.data.new-net", - "auditd.data.permissive", - "auditd.data.class", - "auditd.data.compat", - "auditd.data.fi", - "auditd.data.changed", - "auditd.data.msg", - "auditd.data.dport", - "auditd.data.new-seuser", - "auditd.data.invalid_context", - "auditd.data.dmac", - "auditd.data.ipx-net", - "auditd.data.iuid", - "auditd.data.macproto", - "auditd.data.obj", - "auditd.data.ipid", - "auditd.data.new-fs", - "auditd.data.vm-pid", - "auditd.data.cap_pi", - "auditd.data.old-auid", - "auditd.data.oses", - "auditd.data.fd", - "auditd.data.igid", - "auditd.data.new-disk", - "auditd.data.parent", - "auditd.data.len", - "auditd.data.oflag", - "auditd.data.uuid", - "auditd.data.code", - "auditd.data.nlnk-grp", - "auditd.data.cap_fp", - "auditd.data.new-mem", - "auditd.data.seperm", - "auditd.data.enforcing", - "auditd.data.new-chardev", - "auditd.data.old-rng", - "auditd.data.outif", - "auditd.data.cmd", - "auditd.data.hook", - "auditd.data.new-level", - "auditd.data.sauid", - "auditd.data.sig", - "auditd.data.audit_backlog_wait_time", - "auditd.data.printer", - "auditd.data.old-mem", - "auditd.data.perm", - "auditd.data.old_pi", - "auditd.data.state", - "auditd.data.format", - "auditd.data.new_gid", - "auditd.data.tcontext", - "auditd.data.maj", - "auditd.data.watch", - "auditd.data.device", - "auditd.data.grp", - "auditd.data.bool", - "auditd.data.icmp_type", - "auditd.data.new_lock", - "auditd.data.old_prom", - "auditd.data.acl", - "auditd.data.ip", - "auditd.data.new_pi", - "auditd.data.default-context", - "auditd.data.inode_gid", - "auditd.data.new-log_passwd", - "auditd.data.new_pe", - "auditd.data.selected-context", - "auditd.data.cap_fver", - "auditd.data.file", - "auditd.data.net", - "auditd.data.virt", - "auditd.data.cap_pp", - "auditd.data.old-range", - "auditd.data.resrc", - "auditd.data.new-range", - "auditd.data.obj_gid", - "auditd.data.proto", - "auditd.data.old-disk", - "auditd.data.audit_failure", - "auditd.data.inif", - "auditd.data.vm", - "auditd.data.flags", - "auditd.data.nlnk-fam", - "auditd.data.old-fs", - "auditd.data.old-ses", - "auditd.data.seqno", - "auditd.data.fver", - "auditd.data.qbytes", - "auditd.data.seuser", - "auditd.data.cap_fe", - "auditd.data.new-vcpu", - "auditd.data.old-level", - "auditd.data.old_pp", - "auditd.data.daddr", - "auditd.data.old-role", - "auditd.data.ioctlcmd", - "auditd.data.smac", - "auditd.data.apparmor", - "auditd.data.fe", - "auditd.data.perm_mask", - "auditd.data.ses", - "auditd.data.cap_fi", - "auditd.data.obj_uid", - "auditd.data.reason", - "auditd.data.list", - "auditd.data.old_lock", - "auditd.data.bus", - "auditd.data.old_pe", - "auditd.data.new-role", - "auditd.data.prom", - "auditd.data.uri", - "auditd.data.audit_enabled", - "auditd.data.old-log_passwd", - "auditd.data.old-seuser", - "auditd.data.per", - "auditd.data.scontext", - "auditd.data.tclass", - "auditd.data.ver", - "auditd.data.new", - "auditd.data.val", - "auditd.data.img-ctx", - "auditd.data.old-chardev", - "auditd.data.old_val", - "auditd.data.success", - "auditd.data.inode_uid", - "auditd.data.removed", - "auditd.data.socket.port", - "auditd.data.socket.saddr", - "auditd.data.socket.addr", - "auditd.data.socket.family", - "auditd.data.socket.path", - "geoip.continent_name", - "geoip.city_name", - "geoip.region_name", - "geoip.country_iso_code", - "hash.blake2b_256", - "hash.blake2b_384", - "hash.blake2b_512", - "hash.md5", - "hash.sha1", - "hash.sha224", - "hash.sha256", - "hash.sha384", - "hash.sha3_224", - "hash.sha3_256", - "hash.sha3_384", - "hash.sha3_512", - "hash.sha512", - "hash.sha512_224", - "hash.sha512_256", - "hash.xxh64", - "event.origin", - "user.entity_id", - "user.terminal", - "process.entity_id", - "socket.entity_id", - "system.audit.host.timezone.name", - "system.audit.host.hostname", - "system.audit.host.id", - "system.audit.host.architecture", - "system.audit.host.mac", - "system.audit.host.os.platform", - "system.audit.host.os.name", - "system.audit.host.os.family", - "system.audit.host.os.version", - "system.audit.host.os.kernel", - "system.audit.package.entity_id", - "system.audit.package.name", - "system.audit.package.version", - "system.audit.package.release", - "system.audit.package.arch", - "system.audit.package.license", - "system.audit.package.summary", - "system.audit.package.url", - "system.audit.user.name", - "system.audit.user.uid", - "system.audit.user.gid", - "system.audit.user.dir", - "system.audit.user.shell", - "system.audit.user.user_information", - "system.audit.user.password.type", - "fields.*" - ] - }, - "refresh_interval": "5s" - } - } - } -} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index dfe0444e0bbd4..9573372d02e9c 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -1,12 +1,75 @@ { "type": "doc", "value": { - "id": "_uZE6nwBOpWiDweSth_D", - "index": "threat-indicator-0001", + "id": "84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index": "filebeat-7.12.0-2021.03.10-000001", "source": { - "@timestamp": "2019-09-01T00:41:06.527Z", + "@timestamp": "2021-03-10T14:51:05.766Z", "agent": { - "threat": "03ccb0ce-f65c-4279-a619-05f1d5bb000b" + "ephemeral_id": "34c78500-8df5-4a07-ba87-1cc738b98431", + "hostname": "test", + "id": "08a3d064-8f23-41f3-84b2-f917f6ff9588", + "name": "test", + "type": "filebeat", + "version": "7.12.0" + }, + "fileset": { + "name": "abusemalware" + }, + "threatintel": { + "indicator": { + "first_seen": "2021-03-10T08:02:14.000Z", + "file": { + "size": 80280, + "pe": {}, + "type": "elf", + "hash": { + "sha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "tlsh": "6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE", + "ssdeep": "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL", + "md5": "9b6c3518a91d23ed77504b5416bfb5b3" + } + }, + "type": "file" + }, + "abusemalware": { + "virustotal": { + "result": "38 / 61", + "link": "https://www.virustotal.com/gui/file/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/detection/f-a04ac6d", + "percent": "62.30" + } + } + }, + "tags": [ + "threatintel-abusemalware", + "forwarded" + ], + "input": { + "type": "httpjson" + }, + "@timestamp": "2021-03-10T14:51:07.663Z", + "ecs": { + "version": "1.6.0" + }, + "related": { + "hash": [ + "9b6c3518a91d23ed77504b5416bfb5b3", + "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL" + ] + }, + "service": { + "type": "threatintel" + }, + "event": { + "reference": "https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/", + "ingested": "2021-03-10T14:51:09.809069Z", + "created": "2021-03-10T14:51:07.663Z", + "kind": "enrichment", + "module": "threatintel", + "category": "threat", + "type": "indicator", + "dataset": "threatintel.abusemalware" } } } diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json index 0c24fa429d908..efd23c5a6bba4 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -2,29 +2,821 @@ "type": "index", "value": { "aliases": { - "threat-indicator": { - "is_write_index": false + "filebeat-7.12.0": { + "is_write_index": true } }, - "index": "threat-indicator-0001", + "index": "filebeat-7.12.0-2021.03.10-000001", "mappings": { + "_meta": { + "beat": "filebeat", + "version": "7.12.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "kubernetes.service.selectors.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.service.selectors.*" + } + }, + { + "docker.attrs": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.attrs.*" + } + }, + { + "azure.activitylogs.identity.claims.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.activitylogs.identity.claims.*" + } + }, + { + "azure.platformlogs.properties.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.platformlogs.properties.*" + } + }, + { + "kibana.log.meta": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "kibana.log.meta.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], "properties": { "@timestamp": { "type": "date" }, "agent": { "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "ephemeral_id": { "ignore_above": 1024, "type": "keyword" }, - "threat": { + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { "ignore_above": 1024, "type": "keyword" } } + }, + "apache": { + "properties": { + "access": { + "properties": { + "ssl": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "module": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "fileset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "threatintel": { + "properties": { + "abusemalware": { + "properties": { + "file_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_download": { + "ignore_above": 1024, + "type": "keyword" + }, + "virustotal": { + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "percent": { + "type": "float" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "abuseurl": { + "properties": { + "blacklists": { + "properties": { + "spamhaus_dbl": { + "ignore_above": 1024, + "type": "keyword" + }, + "surbl": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "larted": { + "type": "boolean" + }, + "reporter": { + "ignore_above": 1024, + "type": "keyword" + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "ignore_above": 1024, + "type": "keyword" + }, + "url_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "anomali": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "labels": { + "ignore_above": 1024, + "type": "keyword" + }, + "modified": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_marking_refs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pattern": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "valid_from": { + "type": "date" + } + } + }, + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + }, + "tlsh": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "imphash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "strings": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "misp": { + "properties": { + "attribute": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "comment": { + "ignore_above": 1024, + "type": "keyword" + }, + "deleted": { + "type": "boolean" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "type": "long" + }, + "event_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_relation": { + "ignore_above": 1024, + "type": "keyword" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "to_ids": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "attribute_count": { + "type": "long" + }, + "date": { + "type": "date" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "ignore_above": 1024, + "type": "keyword" + }, + "extends_uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "info": { + "ignore_above": 1024, + "type": "keyword" + }, + "locked": { + "type": "boolean" + }, + "org": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "org_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "orgc": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "orgc_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "proposal_email_lock": { + "type": "boolean" + }, + "publish_timestamp": { + "type": "date" + }, + "published": { + "type": "boolean" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat_level_id": { + "type": "long" + }, + "timestamp": { + "type": "date" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "otx": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } } } + }, + "settings": { + "index": { + "lifecycle": { + "name": "filebeat", + "rollover_alias": "filebeat-7.12.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } } } } From fad3b74f2f61d2445ade1d63f826e699a36dcc64 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 25 Mar 2021 19:33:16 +0100 Subject: [PATCH 05/98] [Discover] Unskip functional test of saved queries (#94705) --- test/functional/apps/discover/_saved_queries.ts | 14 ++++++++++++-- .../services/saved_query_management_component.ts | 7 +++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 23f3af37bbdf6..9726b097c8f62 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,8 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/89477 - describe.skip('saved queries saved objects', function describeIndexTests() { + describe('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); @@ -120,6 +119,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('does not allow saving a query with a non-unique name', async () => { + // this check allows this test to run stand alone, also should fix occacional flakiness + const savedQueryExists = await savedQueryManagementComponent.savedQueryExist('OkResponse'); + if (!savedQueryExists) { + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + } await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse'); }); diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index a39032af43295..7398e6ca8c12e 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -139,6 +139,13 @@ export function SavedQueryManagementComponentProvider({ await testSubjects.click('savedQueryFormSaveButton'); } + async savedQueryExist(title: string) { + await this.openSavedQueryManagementComponent(); + const exists = testSubjects.exists(`~load-saved-query-${title}-button`); + await this.closeSavedQueryManagementComponent(); + return exists; + } + async savedQueryExistOrFail(title: string) { await this.openSavedQueryManagementComponent(); await testSubjects.existOrFail(`~load-saved-query-${title}-button`); From 43e3d558fd6d7e50949f6bde95591114d1d182db Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 25 Mar 2021 14:41:58 -0400 Subject: [PATCH 06/98] [Snapshot + Restore] Add callout when restoring snapshot (#95104) --- .../public/doc_links/doc_links_service.ts | 1 + .../helpers/restore_snapshot.helpers.ts | 13 ++++++- .../restore_snapshot.test.ts | 38 ++++++++++++++++--- .../steps/step_settings/step_settings.tsx | 2 +- .../restore_snapshot_form.tsx | 3 +- .../steps/step_logistics/step_logistics.tsx | 38 ++++++++++++++++--- .../system_indices_overwritten_callout.tsx | 29 ++++++++++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6279d62d2c40e..9711d546fc947 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -284,6 +284,7 @@ export class DocLinksService { registerSourceOnly: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-source-only-repository`, registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, + restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, }, ingest: { pipelines: `${ELASTICSEARCH_DOCS}ingest.html`, diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts index 644ad6ea3089b..c0ffae81a4258 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; import { RestoreSnapshot } from '../../../public/application/sections/restore_snapshot'; @@ -23,11 +24,19 @@ const initTestBed = registerTestBed( ); const setupActions = (testBed: TestBed) => { - const { find } = testBed; + const { find, component, form } = testBed; return { findDataStreamCallout() { return find('dataStreamWarningCallOut'); }, + + toggleGlobalState() { + act(() => { + form.toggleEuiSwitch('includeGlobalStateSwitch'); + }); + + component.update(); + }, }; }; @@ -48,4 +57,6 @@ export const setup = async (): Promise => { export type RestoreSnapshotFormTestSubject = | 'snapshotRestoreStepLogistics' + | 'includeGlobalStateSwitch' + | 'systemIndicesInfoCallOut' | 'dataStreamWarningCallOut'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts index 36cd178060f83..2fecce36f09df 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { act } from 'react-dom/test-utils'; -import { nextTick, pageHelpers, setupEnvironment } from './helpers'; +import { pageHelpers, setupEnvironment } from './helpers'; import { RestoreSnapshotTestBed } from './helpers/restore_snapshot.helpers'; import * as fixtures from '../../test/fixtures'; @@ -20,11 +21,15 @@ describe('', () => { afterAll(() => { server.restore(); }); + describe('with data streams', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); - testBed = await setup(); - await nextTick(); + + await act(async () => { + testBed = await setup(); + }); + testBed.component.update(); }); @@ -37,8 +42,10 @@ describe('', () => { describe('without data streams', () => { beforeEach(async () => { httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot({ totalDataStreams: 0 })); - testBed = await setup(); - await nextTick(); + await act(async () => { + testBed = await setup(); + }); + testBed.component.update(); }); @@ -47,4 +54,25 @@ describe('', () => { expect(exists('dataStreamWarningCallOut')).toBe(false); }); }); + + describe('global state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + it('shows an info callout when include_global_state is enabled', () => { + const { exists, actions } = testBed; + + expect(exists('systemIndicesInfoCallOut')).toBe(false); + + actions.toggleGlobalState(); + + expect(exists('systemIndicesInfoCallOut')).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index dcaad024eb0f7..fc230affc980b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -142,7 +142,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ description={ } fullWidth diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx index 4a281b270210c..f672300db8821 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx @@ -112,7 +112,8 @@ export const RestoreSnapshotForm: React.FunctionComponent = ({ errors={validation.errors} updateCurrentStep={updateCurrentStep} /> - + + {saveError ? ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index bb66585579d7d..de30eadad4543 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -7,6 +7,7 @@ import React, { Fragment, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import semverGt from 'semver/functions/gt'; import { EuiButtonEmpty, EuiDescribedFormGroup, @@ -38,6 +39,8 @@ import { DataStreamsGlobalStateCallOut } from './data_streams_global_state_call_ import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; +import { SystemIndicesOverwrittenCallOut } from './system_indices_overwritten_callout'; + export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({ snapshotDetails, restoreSettings, @@ -50,6 +53,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = indices: unfilteredSnapshotIndices, dataStreams: snapshotDataStreams = [], includeGlobalState: snapshotIncludeGlobalState, + version, } = snapshotDetails; const snapshotIndices = unfilteredSnapshotIndices.filter( @@ -564,11 +568,34 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } description={ - + <> + + {i18n.translate( + 'xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink', + { defaultMessage: 'Learn more.' } + )} + + ), + }} + /> + + {/* Only display callout if include_global_state is enabled and the snapshot was created by ES 7.12+ + * Note: Once we support features states in the UI, we will also need to add a check here for that + * See https://github.com/elastic/kibana/issues/95128 more details + */} + {includeGlobalState && semverGt(version, '7.12.0') && ( + <> + + + + )} + } fullWidth > @@ -594,6 +621,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = checked={includeGlobalState === undefined ? false : includeGlobalState} onChange={(e) => updateRestoreSettings({ includeGlobalState: e.target.checked })} disabled={!snapshotIncludeGlobalState} + data-test-subj="includeGlobalStateSwitch" /> diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx new file mode 100644 index 0000000000000..fac21de0bce22 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +export const SystemIndicesOverwrittenCallOut: FunctionComponent = () => { + return ( + + ); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 290ad19718efc..5a6e9a6164cd1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21323,7 +21323,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesToggleListLink": "データストリームとインデックスを選択", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "すべて選択解除", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "スナップショットと復元ドキュメント", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続的な設定も復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c982931f91e13..dce5c3ad85d5a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21661,7 +21661,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesToggleListLink": "选择数据流和索引", "xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "取消全选", "xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "快照和还原文档", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态", From 6b6404954edf6cac1610f143da3c2670fbc6c216 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:59:18 +0000 Subject: [PATCH 07/98] [Logs / Metrics UI] Separate logs / metrics source configuration awareness (#95334) * Remove metrics awareness of logs fields --- .../resolve_log_source_configuration.ts | 19 ++ .../infra/common/metrics_sources/index.ts | 81 +++++++++ .../source_configuration.ts} | 165 +++++++++--------- .../inventory/components/expression.test.tsx | 2 +- .../inventory/components/expression.tsx | 5 +- .../components/expression.test.tsx | 2 +- .../metric_anomaly/components/expression.tsx | 5 +- .../components/expression.test.tsx | 2 +- .../components/expression.tsx | 5 +- .../components/expression_chart.test.tsx | 7 +- .../components/expression_chart.tsx | 4 +- .../components/expression_row.test.tsx | 2 +- .../hooks/use_metrics_explorer_chart_data.ts | 4 +- .../components/source_configuration/index.ts | 10 -- .../log_columns_configuration_form_state.tsx | 156 ----------------- .../{source => metrics_source}/index.ts | 0 .../{source => metrics_source}/source.tsx | 36 ++-- .../use_source_via_http.ts | 51 +++--- .../containers/saved_view/saved_view.tsx | 4 +- .../containers/with_source/with_source.tsx | 22 +-- x-pack/plugins/infra/public/lib/lib.ts | 4 +- .../infra/public/metrics_overview_fetchers.ts | 4 +- .../redirect_to_host_detail_via_ip.tsx | 4 +- .../settings/fields_configuration_panel.tsx | 2 +- .../settings/indices_configuration_panel.tsx | 2 +- .../logs/stream/page_no_indices_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 8 +- .../inventory_view/components/layout.tsx | 2 +- .../anomalies_table/anomalies_table.tsx | 2 +- .../anomaly_detection_flyout.tsx | 3 +- .../ml/anomaly_detection/job_setup_screen.tsx | 5 +- .../node_details/tabs/metrics/metrics.tsx | 2 +- .../node_details/tabs/properties/index.tsx | 2 +- .../inventory_view/components/search_bar.tsx | 2 +- .../components/timeline/timeline.tsx | 2 +- .../components/toolbars/toolbar.tsx | 4 +- .../components/toolbars/toolbar_wrapper.tsx | 2 +- .../waffle/conditional_tooltip.test.tsx | 2 +- .../components/waffle/conditional_tooltip.tsx | 2 +- .../inventory_view/hooks/use_process_list.ts | 2 +- .../hooks/use_waffle_filters.test.ts | 2 +- .../hooks/use_waffle_filters.ts | 2 +- .../pages/metrics/inventory_view/index.tsx | 4 +- .../lib/create_uptime_link.test.ts | 1 - .../metric_detail/components/invalid_node.tsx | 2 +- .../pages/metrics/metric_detail/index.tsx | 2 +- .../metrics/metric_detail/page_providers.tsx | 2 +- .../metrics_explorer/components/chart.tsx | 4 +- .../components/chart_context_menu.tsx | 6 +- .../metrics_explorer/components/charts.tsx | 4 +- .../components/helpers/create_tsvb_link.ts | 4 +- .../hooks/use_metric_explorer_state.ts | 4 +- .../hooks/use_metrics_explorer_data.test.tsx | 4 +- .../hooks/use_metrics_explorer_data.ts | 4 +- .../pages/metrics/metrics_explorer/index.tsx | 4 +- .../infra/public/pages/metrics/settings.tsx | 2 +- .../settings}/fields_configuration_panel.tsx | 2 +- .../indices_configuration_form_state.ts | 18 +- .../settings}/indices_configuration_panel.tsx | 4 +- .../settings}/ml_configuration_panel.tsx | 2 +- .../source_configuration_form_state.tsx | 60 ++----- .../source_configuration_settings.tsx | 10 +- .../public/utils/source_configuration.ts | 10 +- x-pack/plugins/infra/server/infra_server.ts | 4 +- .../metrics/kibana_metrics_adapter.ts | 6 +- .../evaluate_condition.ts | 6 +- .../inventory_metric_threshold_executor.ts | 6 + ...review_inventory_metric_threshold_alert.ts | 7 +- .../metric_threshold/lib/evaluate_alert.ts | 2 +- .../preview_metric_threshold_alert.ts | 2 +- .../infra/server/lib/domains/fields_domain.ts | 9 +- .../log_entries_domain/log_entries_domain.ts | 4 +- .../plugins/infra/server/lib/infra_types.ts | 4 +- .../plugins/infra/server/lib/metrics/index.ts | 2 +- .../infra/server/lib/sources/defaults.ts | 2 +- .../plugins/infra/server/lib/sources/index.ts | 2 +- ...0_add_new_indexing_strategy_index_names.ts | 2 +- .../infra/server/lib/sources/sources.ts | 2 +- x-pack/plugins/infra/server/plugin.ts | 4 +- .../infra/server/routes/alerting/preview.ts | 11 +- .../{source => metrics_sources}/index.ts | 60 +++---- .../infra/server/routes/snapshot/index.ts | 8 +- ...alculate_index_pattern_based_on_metrics.ts | 23 --- .../server/routes/snapshot/lib/get_nodes.ts | 70 +++++++- ...ransform_request_to_metrics_api_request.ts | 11 +- .../log_queries/get_log_query_fields.ts | 32 ++++ .../apis/metrics_ui/http_source.ts | 28 +-- .../apis/metrics_ui/sources.ts | 51 +----- .../services/infraops_source_configuration.ts | 15 +- 89 files changed, 538 insertions(+), 632 deletions(-) create mode 100644 x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts create mode 100644 x-pack/plugins/infra/common/metrics_sources/index.ts rename x-pack/plugins/infra/common/{http_api/source_api.ts => source_configuration/source_configuration.ts} (61%) delete mode 100644 x-pack/plugins/infra/public/components/source_configuration/index.ts delete mode 100644 x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx rename x-pack/plugins/infra/public/containers/{source => metrics_source}/index.ts (100%) rename x-pack/plugins/infra/public/containers/{source => metrics_source}/source.tsx (79%) rename x-pack/plugins/infra/public/containers/{source => metrics_source}/use_source_via_http.ts (62%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/fields_configuration_panel.tsx (98%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/indices_configuration_form_state.ts (91%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/indices_configuration_panel.tsx (93%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/ml_configuration_panel.tsx (96%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/source_configuration_form_state.tsx (57%) rename x-pack/plugins/infra/public/{components/source_configuration => pages/metrics/settings}/source_configuration_settings.tsx (94%) rename x-pack/plugins/infra/server/routes/{source => metrics_sources}/index.ts (69%) delete mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts create mode 100644 x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts diff --git a/x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts new file mode 100644 index 0000000000000..ad4b2963a41bd --- /dev/null +++ b/x-pack/plugins/infra/common/log_sources/resolve_log_source_configuration.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogSourceConfigurationProperties } from '../http_api/log_sources'; + +// NOTE: Type will change, see below. +type ResolvedLogsSourceConfiguration = LogSourceConfigurationProperties; + +// NOTE: This will handle real resolution for https://github.com/elastic/kibana/issues/92650, via the index patterns service, but for now just +// hands back properties from the saved object (and therefore looks pointless...). +export const resolveLogSourceConfiguration = ( + sourceConfiguration: LogSourceConfigurationProperties +): ResolvedLogsSourceConfiguration => { + return sourceConfiguration; +}; diff --git a/x-pack/plugins/infra/common/metrics_sources/index.ts b/x-pack/plugins/infra/common/metrics_sources/index.ts new file mode 100644 index 0000000000000..a697c65e5a0aa --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_sources/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { omit } from 'lodash'; +import { + SourceConfigurationRT, + SourceStatusRuntimeType, +} from '../source_configuration/source_configuration'; +import { DeepPartial } from '../utility_types'; + +/** + * Properties specific to the Metrics Source Configuration. + */ +export const metricsSourceConfigurationPropertiesRT = rt.strict({ + name: SourceConfigurationRT.props.name, + description: SourceConfigurationRT.props.description, + metricAlias: SourceConfigurationRT.props.metricAlias, + inventoryDefaultView: SourceConfigurationRT.props.inventoryDefaultView, + metricsExplorerDefaultView: SourceConfigurationRT.props.metricsExplorerDefaultView, + fields: rt.strict(omit(SourceConfigurationRT.props.fields.props, 'message')), + anomalyThreshold: rt.number, +}); + +export type MetricsSourceConfigurationProperties = rt.TypeOf< + typeof metricsSourceConfigurationPropertiesRT +>; + +export const partialMetricsSourceConfigurationPropertiesRT = rt.partial({ + ...metricsSourceConfigurationPropertiesRT.type.props, + fields: rt.partial({ + ...metricsSourceConfigurationPropertiesRT.type.props.fields.type.props, + }), +}); + +export type PartialMetricsSourceConfigurationProperties = rt.TypeOf< + typeof partialMetricsSourceConfigurationPropertiesRT +>; + +const metricsSourceConfigurationOriginRT = rt.keyof({ + fallback: null, + internal: null, + stored: null, +}); + +export const metricsSourceStatusRT = rt.strict({ + metricIndicesExist: SourceStatusRuntimeType.props.metricIndicesExist, + indexFields: SourceStatusRuntimeType.props.indexFields, +}); + +export type MetricsSourceStatus = rt.TypeOf; + +export const metricsSourceConfigurationRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + origin: metricsSourceConfigurationOriginRT, + configuration: metricsSourceConfigurationPropertiesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + status: metricsSourceStatusRT, + }), + ]) +); + +export type MetricsSourceConfiguration = rt.TypeOf; +export type PartialMetricsSourceConfiguration = DeepPartial; + +export const metricsSourceConfigurationResponseRT = rt.type({ + source: metricsSourceConfigurationRT, +}); + +export type MetricsSourceConfigurationResponse = rt.TypeOf< + typeof metricsSourceConfigurationResponseRT +>; diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts similarity index 61% rename from x-pack/plugins/infra/common/http_api/source_api.ts rename to x-pack/plugins/infra/common/source_configuration/source_configuration.ts index f14151531ba35..ad68a7a019848 100644 --- a/x-pack/plugins/infra/common/http_api/source_api.ts +++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts @@ -5,8 +5,19 @@ * 2.0. */ +/** + * These are the core source configuration types that represent a Source Configuration in + * it's entirety. There are then subsets of this configuration that form the Logs Source Configuration + * and Metrics Source Configuration. The Logs Source Configuration is further expanded to it's resolved form. + * -> Source Configuration + * -> Logs source configuration + * -> Resolved Logs Source Configuration + * -> Metrics Source Configuration + */ + /* eslint-disable @typescript-eslint/no-empty-interface */ +import { omit } from 'lodash'; import * as rt from 'io-ts'; import moment from 'moment'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -29,121 +40,113 @@ export const TimestampFromString = new rt.Type( ); /** - * Stored source configuration as read from and written to saved objects + * Log columns */ -const SavedSourceConfigurationFieldsRuntimeType = rt.partial({ - container: rt.string, - host: rt.string, - pod: rt.string, - tiebreaker: rt.string, - timestamp: rt.string, -}); - -export type InfraSavedSourceConfigurationFields = rt.TypeOf< - typeof SavedSourceConfigurationFieldColumnRuntimeType ->; - -export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({ +export const SourceConfigurationTimestampColumnRuntimeType = rt.type({ timestampColumn: rt.type({ id: rt.string, }), }); export type InfraSourceConfigurationTimestampColumn = rt.TypeOf< - typeof SavedSourceConfigurationTimestampColumnRuntimeType + typeof SourceConfigurationTimestampColumnRuntimeType >; -export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({ +export const SourceConfigurationMessageColumnRuntimeType = rt.type({ messageColumn: rt.type({ id: rt.string, }), }); export type InfraSourceConfigurationMessageColumn = rt.TypeOf< - typeof SavedSourceConfigurationMessageColumnRuntimeType + typeof SourceConfigurationMessageColumnRuntimeType >; -export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({ +export const SourceConfigurationFieldColumnRuntimeType = rt.type({ fieldColumn: rt.type({ id: rt.string, field: rt.string, }), }); -export const SavedSourceConfigurationColumnRuntimeType = rt.union([ - SavedSourceConfigurationTimestampColumnRuntimeType, - SavedSourceConfigurationMessageColumnRuntimeType, - SavedSourceConfigurationFieldColumnRuntimeType, +export type InfraSourceConfigurationFieldColumn = rt.TypeOf< + typeof SourceConfigurationFieldColumnRuntimeType +>; + +export const SourceConfigurationColumnRuntimeType = rt.union([ + SourceConfigurationTimestampColumnRuntimeType, + SourceConfigurationMessageColumnRuntimeType, + SourceConfigurationFieldColumnRuntimeType, ]); -export type InfraSavedSourceConfigurationColumn = rt.TypeOf< - typeof SavedSourceConfigurationColumnRuntimeType ->; +export type InfraSourceConfigurationColumn = rt.TypeOf; -export const SavedSourceConfigurationRuntimeType = rt.partial({ +/** + * Fields + */ + +const SourceConfigurationFieldsRT = rt.type({ + container: rt.string, + host: rt.string, + pod: rt.string, + tiebreaker: rt.string, + timestamp: rt.string, + message: rt.array(rt.string), +}); + +/** + * Properties that represent a full source configuration, which is the result of merging static values with + * saved values. + */ +export const SourceConfigurationRT = rt.type({ name: rt.string, description: rt.string, metricAlias: rt.string, logAlias: rt.string, inventoryDefaultView: rt.string, metricsExplorerDefaultView: rt.string, - fields: SavedSourceConfigurationFieldsRuntimeType, - logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + fields: SourceConfigurationFieldsRT, + logColumns: rt.array(SourceConfigurationColumnRuntimeType), anomalyThreshold: rt.number, }); +/** + * Stored source configuration as read from and written to saved objects + */ +const SavedSourceConfigurationFieldsRuntimeType = rt.partial( + omit(SourceConfigurationFieldsRT.props, ['message']) +); + +export type InfraSavedSourceConfigurationFields = rt.TypeOf< + typeof SavedSourceConfigurationFieldsRuntimeType +>; + +export const SavedSourceConfigurationRuntimeType = rt.intersection([ + rt.partial(omit(SourceConfigurationRT.props, ['fields'])), + rt.partial({ + fields: SavedSourceConfigurationFieldsRuntimeType, + }), +]); + export interface InfraSavedSourceConfiguration extends rt.TypeOf {} export const pickSavedSourceConfiguration = ( value: InfraSourceConfiguration ): InfraSavedSourceConfiguration => { - const { - name, - description, - metricAlias, - logAlias, - fields, - inventoryDefaultView, - metricsExplorerDefaultView, - logColumns, - anomalyThreshold, - } = value; - const { container, host, pod, tiebreaker, timestamp } = fields; - - return { - name, - description, - metricAlias, - logAlias, - inventoryDefaultView, - metricsExplorerDefaultView, - fields: { container, host, pod, tiebreaker, timestamp }, - logColumns, - anomalyThreshold, - }; + return value; }; /** - * Static source configuration as read from the configuration file + * Static source configuration, the result of merging values from the config file and + * hardcoded defaults. */ -const StaticSourceConfigurationFieldsRuntimeType = rt.partial({ - ...SavedSourceConfigurationFieldsRuntimeType.props, - message: rt.array(rt.string), -}); - +const StaticSourceConfigurationFieldsRuntimeType = rt.partial(SourceConfigurationFieldsRT.props); export const StaticSourceConfigurationRuntimeType = rt.partial({ - name: rt.string, - description: rt.string, - metricAlias: rt.string, - logAlias: rt.string, - inventoryDefaultView: rt.string, - metricsExplorerDefaultView: rt.string, + ...SourceConfigurationRT.props, fields: StaticSourceConfigurationFieldsRuntimeType, - logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), - anomalyThreshold: rt.number, }); export interface InfraStaticSourceConfiguration @@ -153,18 +156,20 @@ export interface InfraStaticSourceConfiguration * Full source configuration type after all cleanup has been done at the edges */ -const SourceConfigurationFieldsRuntimeType = rt.type({ - ...StaticSourceConfigurationFieldsRuntimeType.props, -}); - -export type InfraSourceConfigurationFields = rt.TypeOf; +export type InfraSourceConfigurationFields = rt.TypeOf; export const SourceConfigurationRuntimeType = rt.type({ - ...SavedSourceConfigurationRuntimeType.props, - fields: SourceConfigurationFieldsRuntimeType, - logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + ...SourceConfigurationRT.props, + fields: SourceConfigurationFieldsRT, + logColumns: rt.array(SourceConfigurationColumnRuntimeType), }); +export interface InfraSourceConfiguration + extends rt.TypeOf {} + +/** + * Source status + */ const SourceStatusFieldRuntimeType = rt.type({ name: rt.string, type: rt.string, @@ -175,12 +180,17 @@ const SourceStatusFieldRuntimeType = rt.type({ export type InfraSourceIndexField = rt.TypeOf; -const SourceStatusRuntimeType = rt.type({ +export const SourceStatusRuntimeType = rt.type({ logIndicesExist: rt.boolean, metricIndicesExist: rt.boolean, indexFields: rt.array(SourceStatusFieldRuntimeType), }); +export interface InfraSourceStatus extends rt.TypeOf {} + +/** + * Source configuration along with source status and metadata + */ export const SourceRuntimeType = rt.intersection([ rt.type({ id: rt.string, @@ -198,11 +208,6 @@ export const SourceRuntimeType = rt.intersection([ }), ]); -export interface InfraSourceStatus extends rt.TypeOf {} - -export interface InfraSourceConfiguration - extends rt.TypeOf {} - export interface InfraSource extends rt.TypeOf {} export const SourceResponseRuntimeType = rt.type({ diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx index 88d72300c2d6d..b345e138accec 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx @@ -17,7 +17,7 @@ import { act } from 'react-dom/test-utils'; import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index b28c76d1cb374..c4f8b5a615b0f 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -43,7 +43,7 @@ import { AlertTypeParamsExpressionProps, } from '../../../../../triggers_actions_ui/public'; import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http'; import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items'; import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items'; @@ -124,14 +124,13 @@ export const Expressions: React.FC = (props) => { } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', fetch: http.fetch, toastWarning: notifications.toasts.addWarning, }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx index dd4cbe10b74ee..6b99aff9f903d 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { Expression, AlertContextMeta } from './expression'; import { act } from 'react-dom/test-utils'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx index 12cc2bf9fb3a9..afbd6ffa8b5f7 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -27,7 +27,7 @@ import { AlertTypeParamsExpressionProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/types'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http'; import { findInventoryModel } from '../../../../common/inventory_models'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { NodeTypeExpression } from './node_type'; @@ -75,12 +75,11 @@ export const Expression: React.FC = (props) => { } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', fetch: http.fetch, toastWarning: notifications.toasts.addWarning, }); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx index a6d74d4f461a6..667f5c061ce48 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx @@ -15,7 +15,7 @@ import { act } from 'react-dom/test-utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 3b8afc173c2bd..8835a7cd55ce8 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -35,7 +35,7 @@ import { import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../containers/metrics_source/use_source_via_http'; import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; @@ -73,14 +73,13 @@ export const Expressions: React.FC = (props) => { const { http, notifications } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', fetch: http.fetch, toastWarning: notifications.toasts.addWarning, }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index 7e4209e4253d7..caf8e32814fe5 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { coreMock as mockCoreMock } from 'src/core/public/mocks'; import { MetricExpression } from '../types'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import React from 'react'; import { ExpressionChart } from './expression_chart'; import { act } from 'react-dom/test-utils'; @@ -45,20 +45,17 @@ describe('ExpressionChart', () => { fields: [], }; - const source: InfraSource = { + const source: MetricsSourceConfiguration = { id: 'default', origin: 'fallback', configuration: { name: 'default', description: 'The default configuration', - logColumns: [], metricAlias: 'metricbeat-*', - logAlias: 'filebeat-*', inventoryDefaultView: 'host', metricsExplorerDefaultView: 'host', fields: { timestamp: '@timestamp', - message: ['message'], container: 'container.id', host: 'host.name', pod: 'kubernetes.pod.uid', diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 2a274c4b6d50f..e5558b961ab20 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -11,7 +11,7 @@ import { first, last } from 'lodash'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { Color } from '../../../../common/color_palette'; import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; @@ -35,7 +35,7 @@ import { ThresholdAnnotations } from '../../common/criterion_preview_chart/thres interface Props { expression: MetricExpression; derivedIndexPattern: IIndexPattern; - source: InfraSource | null; + source: MetricsSourceConfiguration | null; filterQuery?: string; groupBy?: string | string[]; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx index 54477a39c2626..90f75e6a94022 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx @@ -13,7 +13,7 @@ import { act } from 'react-dom/test-utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; -jest.mock('../../../containers/source/use_source_via_http', () => ({ +jest.mock('../../../containers/metrics_source/use_source_via_http', () => ({ useSourceViaHttp: () => ({ source: { id: 'default' }, createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 908372d13b6bc..e3006993216ae 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -7,7 +7,7 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { useMemo } from 'react'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { MetricExpression } from '../types'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; @@ -15,7 +15,7 @@ import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/ export const useMetricsExplorerChartData = ( expression: MetricExpression, derivedIndexPattern: IIndexPattern, - source: InfraSource | null, + source: MetricsSourceConfiguration | null, filterQuery?: string, groupBy?: string | string[] ) => { diff --git a/x-pack/plugins/infra/public/components/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts deleted file mode 100644 index 50db601234a8c..0000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './input_fields'; -export { SourceConfigurationSettings } from './source_configuration_settings'; -export { ViewSourceConfigurationButton } from './view_source_configuration_button'; diff --git a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx deleted file mode 100644 index b5b28cb25b83b..0000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo, useState } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - LogColumnConfiguration, - isTimestampLogColumnConfiguration, - isMessageLogColumnConfiguration, - TimestampLogColumnConfiguration, - MessageLogColumnConfiguration, - FieldLogColumnConfiguration, -} from '../../utils/source_configuration'; - -export interface TimestampLogColumnConfigurationProps { - logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn']; - remove: () => void; - type: 'timestamp'; -} - -export interface MessageLogColumnConfigurationProps { - logColumnConfiguration: MessageLogColumnConfiguration['messageColumn']; - remove: () => void; - type: 'message'; -} - -export interface FieldLogColumnConfigurationProps { - logColumnConfiguration: FieldLogColumnConfiguration['fieldColumn']; - remove: () => void; - type: 'field'; -} - -export type LogColumnConfigurationProps = - | TimestampLogColumnConfigurationProps - | MessageLogColumnConfigurationProps - | FieldLogColumnConfigurationProps; - -interface FormState { - logColumns: LogColumnConfiguration[]; -} - -type FormStateChanges = Partial; - -export const useLogColumnsConfigurationFormState = ({ - initialFormState = defaultFormState, -}: { - initialFormState?: FormState; -}) => { - const [formStateChanges, setFormStateChanges] = useState({}); - - const resetForm = useCallback(() => setFormStateChanges({}), []); - - const formState = useMemo( - () => ({ - ...initialFormState, - ...formStateChanges, - }), - [initialFormState, formStateChanges] - ); - - const logColumnConfigurationProps = useMemo( - () => - formState.logColumns.map( - (logColumn): LogColumnConfigurationProps => { - const remove = () => - setFormStateChanges((changes) => ({ - ...changes, - logColumns: formState.logColumns.filter((item) => item !== logColumn), - })); - - if (isTimestampLogColumnConfiguration(logColumn)) { - return { - logColumnConfiguration: logColumn.timestampColumn, - remove, - type: 'timestamp', - }; - } else if (isMessageLogColumnConfiguration(logColumn)) { - return { - logColumnConfiguration: logColumn.messageColumn, - remove, - type: 'message', - }; - } else { - return { - logColumnConfiguration: logColumn.fieldColumn, - remove, - type: 'field', - }; - } - } - ), - [formState.logColumns] - ); - - const addLogColumn = useCallback( - (logColumnConfiguration: LogColumnConfiguration) => - setFormStateChanges((changes) => ({ - ...changes, - logColumns: [...formState.logColumns, logColumnConfiguration], - })), - [formState.logColumns] - ); - - const moveLogColumn = useCallback( - (sourceIndex, destinationIndex) => { - if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) { - const newLogColumns = [...formState.logColumns]; - newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); - setFormStateChanges((changes) => ({ - ...changes, - logColumns: newLogColumns, - })); - } - }, - [formState.logColumns] - ); - - const errors = useMemo( - () => - logColumnConfigurationProps.length <= 0 - ? [ - , - ] - : [], - [logColumnConfigurationProps] - ); - - const isFormValid = useMemo(() => (errors.length <= 0 ? true : false), [errors]); - - const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); - - return { - addLogColumn, - moveLogColumn, - errors, - logColumnConfigurationProps, - formState, - formStateChanges, - isFormDirty, - isFormValid, - resetForm, - }; -}; - -const defaultFormState: FormState = { - logColumns: [], -}; diff --git a/x-pack/plugins/infra/public/containers/source/index.ts b/x-pack/plugins/infra/public/containers/metrics_source/index.ts similarity index 100% rename from x-pack/plugins/infra/public/containers/source/index.ts rename to x-pack/plugins/infra/public/containers/metrics_source/index.ts diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/metrics_source/source.tsx similarity index 79% rename from x-pack/plugins/infra/public/containers/source/source.tsx rename to x-pack/plugins/infra/public/containers/metrics_source/source.tsx index 8e2a8f29e03df..b730f8b007e43 100644 --- a/x-pack/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/plugins/infra/public/containers/metrics_source/source.tsx @@ -9,27 +9,25 @@ import createContainer from 'constate'; import { useEffect, useMemo, useState } from 'react'; import { - InfraSavedSourceConfiguration, - InfraSource, - SourceResponse, -} from '../../../common/http_api/source_api'; + MetricsSourceConfigurationResponse, + MetricsSourceConfiguration, + PartialMetricsSourceConfigurationProperties, +} from '../../../common/metrics_sources'; + import { useTrackedPromise } from '../../utils/use_tracked_promise'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; export const pickIndexPattern = ( - source: InfraSource | undefined, - type: 'logs' | 'metrics' | 'both' + source: MetricsSourceConfiguration | undefined, + type: 'metrics' ) => { if (!source) { return 'unknown-index'; } - if (type === 'logs') { - return source.configuration.logAlias; - } if (type === 'metrics') { return source.configuration.metricAlias; } - return `${source.configuration.logAlias},${source.configuration.metricAlias}`; + return `${source.configuration.metricAlias}`; }; const DEPENDENCY_ERROR_MESSAGE = 'Failed to load source: No fetch client available.'; @@ -39,7 +37,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const fetchService = kibana.services.http?.fetch; const API_URL = `/api/metrics/source/${sourceId}`; - const [source, setSource] = useState(undefined); + const [source, setSource] = useState(undefined); const [loadSourceRequest, loadSource] = useTrackedPromise( { @@ -49,7 +47,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { throw new Error(DEPENDENCY_ERROR_MESSAGE); } - return await fetchService(`${API_URL}/metrics`, { + return await fetchService(`${API_URL}`, { method: 'GET', }); }, @@ -62,12 +60,12 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const [createSourceConfigurationRequest, createSourceConfiguration] = useTrackedPromise( { - createPromise: async (sourceProperties: InfraSavedSourceConfiguration) => { + createPromise: async (sourceProperties: PartialMetricsSourceConfigurationProperties) => { if (!fetchService) { throw new Error(DEPENDENCY_ERROR_MESSAGE); } - return await fetchService(API_URL, { + return await fetchService(API_URL, { method: 'PATCH', body: JSON.stringify(sourceProperties), }); @@ -83,12 +81,12 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( { - createPromise: async (sourceProperties: InfraSavedSourceConfiguration) => { + createPromise: async (sourceProperties: PartialMetricsSourceConfigurationProperties) => { if (!fetchService) { throw new Error(DEPENDENCY_ERROR_MESSAGE); } - return await fetchService(API_URL, { + return await fetchService(API_URL, { method: 'PATCH', body: JSON.stringify(sourceProperties), }); @@ -102,7 +100,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { [fetchService, sourceId] ); - const createDerivedIndexPattern = (type: 'logs' | 'metrics' | 'both') => { + const createDerivedIndexPattern = (type: 'metrics') => { return { fields: source?.status ? source.status.indexFields : [], title: pickIndexPattern(source, type), @@ -129,9 +127,6 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { const sourceExists = useMemo(() => (source ? !!source.version : undefined), [source]); - const logIndicesExist = useMemo(() => source && source.status && source.status.logIndicesExist, [ - source, - ]); const metricIndicesExist = useMemo( () => source && source.status && source.status.metricIndicesExist, [source] @@ -144,7 +139,6 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { return { createSourceConfiguration, createDerivedIndexPattern, - logIndicesExist, isLoading, isLoadingSource: loadSourceRequest.state === 'pending', isUninitialized, diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/metrics_source/use_source_via_http.ts similarity index 62% rename from x-pack/plugins/infra/public/containers/source/use_source_via_http.ts rename to x-pack/plugins/infra/public/containers/metrics_source/use_source_via_http.ts index 548e6b8aa9cd9..2947f8fb09847 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/metrics_source/use_source_via_http.ts @@ -13,51 +13,47 @@ import createContainer from 'constate'; import { HttpHandler } from 'src/core/public'; import { ToastInput } from 'src/core/public'; import { - SourceResponseRuntimeType, - SourceResponse, - InfraSource, -} from '../../../common/http_api/source_api'; + metricsSourceConfigurationResponseRT, + MetricsSourceConfigurationResponse, + MetricsSourceConfiguration, +} from '../../../common/metrics_sources'; import { useHTTPRequest } from '../../hooks/use_http_request'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; export const pickIndexPattern = ( - source: InfraSource | undefined, - type: 'logs' | 'metrics' | 'both' + source: MetricsSourceConfiguration | undefined, + type: 'metrics' ) => { if (!source) { return 'unknown-index'; } - if (type === 'logs') { - return source.configuration.logAlias; - } if (type === 'metrics') { return source.configuration.metricAlias; } - return `${source.configuration.logAlias},${source.configuration.metricAlias}`; + return `${source.configuration.metricAlias}`; }; interface Props { sourceId: string; - type: 'logs' | 'metrics' | 'both'; fetch?: HttpHandler; toastWarning?: (input: ToastInput) => void; } -export const useSourceViaHttp = ({ - sourceId = 'default', - type = 'both', - fetch, - toastWarning, -}: Props) => { +export const useSourceViaHttp = ({ sourceId = 'default', fetch, toastWarning }: Props) => { const decodeResponse = (response: any) => { return pipe( - SourceResponseRuntimeType.decode(response), + metricsSourceConfigurationResponseRT.decode(response), fold(throwErrors(createPlainError), identity) ); }; - const { error, loading, response, makeRequest } = useHTTPRequest( - `/api/metrics/source/${sourceId}/${type}`, + const { + error, + loading, + response, + makeRequest, + } = useHTTPRequest( + `/api/metrics/source/${sourceId}`, 'GET', null, decodeResponse, @@ -71,15 +67,12 @@ export const useSourceViaHttp = ({ })(); }, [makeRequest]); - const createDerivedIndexPattern = useCallback( - (indexType: 'logs' | 'metrics' | 'both' = type) => { - return { - fields: response?.source.status ? response.source.status.indexFields : [], - title: pickIndexPattern(response?.source, indexType), - }; - }, - [response, type] - ); + const createDerivedIndexPattern = useCallback(() => { + return { + fields: response?.source.status ? response.source.status.indexFields : [], + title: pickIndexPattern(response?.source, 'metrics'), + }; + }, [response]); const source = useMemo(() => { return response ? response.source : null; diff --git a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx index 4c4835cbe4cdb..56a2a13e31ff7 100644 --- a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx +++ b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx @@ -17,10 +17,10 @@ import { useUrlState } from '../../utils/use_url_state'; import { useFindSavedObject } from '../../hooks/use_find_saved_object'; import { useCreateSavedObject } from '../../hooks/use_create_saved_object'; import { useDeleteSavedObject } from '../../hooks/use_delete_saved_object'; -import { Source } from '../source'; +import { Source } from '../metrics_source'; import { metricsExplorerViewSavedObjectName } from '../../../common/saved_objects/metrics_explorer_view'; import { inventoryViewSavedObjectName } from '../../../common/saved_objects/inventory_view'; -import { useSourceConfigurationFormState } from '../../components/source_configuration/source_configuration_form_state'; +import { useSourceConfigurationFormState } from '../../pages/metrics/settings/source_configuration_form_state'; import { useGetSavedObject } from '../../hooks/use_get_saved_object'; import { useUpdateSavedObject } from '../../hooks/use_update_saved_object'; diff --git a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx index 3b9f0d3e1eae2..f3ca57a40c4c7 100644 --- a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx +++ b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx @@ -9,17 +9,19 @@ import React, { useContext } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; import { - InfraSavedSourceConfiguration, - InfraSourceConfiguration, -} from '../../../common/http_api/source_api'; + MetricsSourceConfigurationProperties, + PartialMetricsSourceConfigurationProperties, +} from '../../../common/metrics_sources'; import { RendererFunction } from '../../utils/typed_react'; -import { Source } from '../source'; +import { Source } from '../metrics_source'; interface WithSourceProps { children: RendererFunction<{ - configuration?: InfraSourceConfiguration; - create: (sourceProperties: InfraSavedSourceConfiguration) => Promise | undefined; - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + configuration?: MetricsSourceConfigurationProperties; + create: ( + sourceProperties: PartialMetricsSourceConfigurationProperties + ) => Promise | undefined; + createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; exists?: boolean; hasFailed: boolean; isLoading: boolean; @@ -29,7 +31,9 @@ interface WithSourceProps { metricAlias?: string; metricIndicesExist?: boolean; sourceId: string; - update: (sourceProperties: InfraSavedSourceConfiguration) => Promise | undefined; + update: ( + sourceProperties: PartialMetricsSourceConfigurationProperties + ) => Promise | undefined; version?: string; }>; } @@ -42,7 +46,6 @@ export const WithSource: React.FunctionComponent = ({ children sourceExists, sourceId, metricIndicesExist, - logIndicesExist, isLoading, loadSource, hasFailedLoadingSource, @@ -60,7 +63,6 @@ export const WithSource: React.FunctionComponent = ({ children isLoading, lastFailureMessage: loadSourceFailureMessage, load: loadSource, - logIndicesExist, metricIndicesExist, sourceId, update: updateSourceConfiguration, diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 622e0c9d33845..4541eb6518788 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -14,7 +14,7 @@ import { SnapshotNodeMetric, SnapshotNodePath, } from '../../common/http_api/snapshot_api'; -import { InfraSourceConfigurationFields } from '../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../common/metrics_sources'; import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options'; export interface InfraWaffleMapNode { @@ -124,7 +124,7 @@ export enum InfraWaffleMapRuleOperator { } export interface InfraWaffleMapOptions { - fields?: InfraSourceConfigurationFields | null; + fields?: MetricsSourceConfigurationProperties['fields'] | null; formatter: InfraFormatterType; formatTemplate: string; metric: SnapshotMetricInput; diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts index 45b17aeb1f724..bcc2eec504209 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts @@ -14,9 +14,7 @@ export const createMetricsHasData = ( ) => async () => { const [coreServices] = await getStartServices(); const { http } = coreServices; - const results = await http.get<{ hasData: boolean }>( - '/api/metrics/source/default/metrics/hasData' - ); + const results = await http.get<{ hasData: boolean }>('/api/metrics/source/default/hasData'); return results.hasData; }; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index ea2e67abc4141..8377eadfbce1d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -14,7 +14,7 @@ import { useHostIpToName } from './use_host_ip_to_name'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { LoadingPage } from '../../components/loading_page'; import { Error } from '../error'; -import { useSource } from '../../containers/source/source'; +import { useSourceViaHttp } from '../../containers/metrics_source/use_source_via_http'; type RedirectToHostDetailType = RouteComponentProps<{ hostIp: string; @@ -26,7 +26,7 @@ export const RedirectToHostDetailViaIP = ({ }, location, }: RedirectToHostDetailType) => { - const { source } = useSource({ sourceId: 'default' }); + const { source } = useSourceViaHttp({ sourceId: 'default' }); const { error, name } = useHostIpToName( hostIp, diff --git a/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx index 13eea67fb2a5a..236817ce3890f 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InputFieldProps } from '../../../components/source_configuration'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface FieldsConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx index 72b5c35b958d6..e6f03e76255a2 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InputFieldProps } from '../../../components/source_configuration'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface IndicesConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index 5d6ff9544e187..bc3bc22f3f1b2 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { NoIndices } from '../../../components/empty_states/no_indices'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration/view_source_configuration_button'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useLinkProps } from '../../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 240cb778275b1..51cc4ca098483 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -12,7 +12,7 @@ import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/common'; -import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; @@ -24,7 +24,7 @@ import { } from './metrics_explorer/hooks/use_metrics_explorer_options'; import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; import { WithSource } from '../../containers/with_source'; -import { Source } from '../../containers/source'; +import { Source } from '../../containers/metrics_source'; import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './inventory_view'; import { MetricsSettingsPage } from './settings'; @@ -188,8 +188,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { }; const PageContent = (props: { - configuration: InfraSourceConfiguration; - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + configuration: MetricsSourceConfigurationProperties; + createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; }) => { const { createDerivedIndexPattern, configuration } = props; const { options } = useContext(MetricsExplorerOptionsContainer.Context); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 089ad9c237818..534132eb75fa1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -18,7 +18,7 @@ import { useSnapshot } from '../hooks/use_snaphot'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; -import { useSourceContext } from '../../../../containers/source'; +import { useSourceContext } from '../../../../containers/metrics_source'; import { InfraFormatterType } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx index 7f0424cf48758..409c11cbbe897 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx @@ -43,7 +43,7 @@ import { import { PaginationControls } from './pagination'; import { AnomalySummary } from './annomaly_summary'; import { AnomalySeverityIndicator } from '../../../../../../../components/logging/log_analysis_results/anomaly_severity_indicator'; -import { useSourceContext } from '../../../../../../../containers/source'; +import { useSourceContext } from '../../../../../../../containers/metrics_source'; import { createResultsUrl } from '../flyout_home'; import { useWaffleViewState, WaffleViewState } from '../../../../hooks/use_waffle_view_state'; type JobType = 'k8s' | 'hosts'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx index 326689e945e1d..387e739fab43f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx @@ -13,7 +13,7 @@ import { JobSetupScreen } from './job_setup_screen'; import { useInfraMLCapabilities } from '../../../../../../containers/ml/infra_ml_capabilities'; import { MetricHostsModuleProvider } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { MetricK8sModuleProvider } from '../../../../../../containers/ml/modules/metrics_k8s/module'; -import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../../../../containers/metrics_source/use_source_via_http'; import { useActiveKibanaSpace } from '../../../../../../hooks/use_kibana_space'; export const AnomalyDetectionFlyout = () => { @@ -23,7 +23,6 @@ export const AnomalyDetectionFlyout = () => { const [screenParams, setScreenParams] = useState(null); const { source } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', }); const { space } = useActiveKibanaSpace(); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index 894f76318bcfe..a210831eef865 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -17,7 +17,7 @@ import moment, { Moment } from 'moment'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http'; +import { useSourceViaHttp } from '../../../../../../containers/metrics_source/use_source_via_http'; import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module'; import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; @@ -42,7 +42,6 @@ export const JobSetupScreen = (props: Props) => { const [filterQuery, setFilterQuery] = useState(''); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', - type: 'metrics', }); const indicies = h.sourceConfiguration.indices; @@ -79,7 +78,7 @@ export const JobSetupScreen = (props: Props) => { } }, [props.jobType, k.jobSummaries, h.jobSummaries]); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern(), [ createDerivedIndexPattern, ]); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx index d89aaefe53fd1..5ab8eb380a657 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx @@ -23,7 +23,7 @@ import { EuiLoadingChart, EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/e import { TabContent, TabProps } from '../shared'; import { useSnapshot } from '../../../../hooks/use_snaphot'; import { useWaffleOptionsContext } from '../../../../hooks/use_waffle_options'; -import { useSourceContext } from '../../../../../../../containers/source'; +import { useSourceContext } from '../../../../../../../containers/metrics_source'; import { findInventoryFields } from '../../../../../../../../common/inventory_models'; import { convertKueryToElasticSearchQuery } from '../../../../../../../utils/kuery'; import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx index 9aa2cdfd90203..010a1a9941335 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLoadingChart } from '@elastic/eui'; import { TabContent, TabProps } from '../shared'; -import { Source } from '../../../../../../../containers/source'; +import { Source } from '../../../../../../../containers/metrics_source'; import { findInventoryModel } from '../../../../../../../../common/inventory_models'; import { InventoryItemType } from '../../../../../../../../common/inventory_models/types'; import { useMetadata } from '../../../../../metric_detail/hooks/use_metadata'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx index cae17c174772d..16f73734836d0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { Source } from '../../../../containers/source'; +import { Source } from '../../../../containers/metrics_source'; import { AutocompleteField } from '../../../../components/autocomplete_field'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index 0248241d616dc..0a657b5242427 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -29,7 +29,7 @@ import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_reac import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n'; import { MetricsExplorerAggregation } from '../../../../../../common/http_api'; import { colorTransformer, Color } from '../../../../../../common/color_palette'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { useTimeline } from '../../hooks/use_timeline'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx index cd05341156831..1c79807f139c3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -7,7 +7,7 @@ import React, { FunctionComponent } from 'react'; import { EuiFlexItem } from '@elastic/eui'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { SnapshotMetricInput, SnapshotGroupBy, @@ -24,7 +24,7 @@ import { WaffleOptionsState, WaffleSortOption } from '../../hooks/use_waffle_opt import { useInventoryMeta } from '../../hooks/use_inventory_meta'; export interface ToolbarProps extends Omit { - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; changeMetric: (payload: SnapshotMetricInput) => void; changeGroupBy: (payload: SnapshotGroupBy) => void; changeCustomOptions: (payload: InfraGroupByOptions[]) => void; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index abc0089e4fc2e..7fc332ead45c7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { fieldToName } from '../../lib/field_to_display_name'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher'; import { ToolbarProps } from './toolbar'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx index 523fa5f013b5a..6dde53efae761 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -17,7 +17,7 @@ import { InfraFormatterType, } from '../../../../../lib/lib'; -jest.mock('../../../../../containers/source', () => ({ +jest.mock('../../../../../containers/metrics_source', () => ({ useSourceContext: () => ({ sourceId: 'default' }), })); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index d0aeeca9850c4..6e334f4fbca75 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -11,7 +11,7 @@ import { first } from 'lodash'; import { getCustomMetricLabel } from '../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../common/http_api'; import { withTheme, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { useSourceContext } from '../../../../../containers/source'; +import { useSourceContext } from '../../../../../containers/metrics_source'; import { findInventoryModel } from '../../../../../../common/inventory_models'; import { InventoryItemType, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index d12bef2f3cdc0..e74abb2ecc459 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -13,7 +13,7 @@ import { useEffect, useState } from 'react'; import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { useHTTPRequest } from '../../../../hooks/use_http_request'; -import { useSourceContext } from '../../../../containers/source'; +import { useSourceContext } from '../../../../containers/metrics_source'; export interface SortBy { name: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts index 8d7e516d50b57..cc1108cb91e6d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -17,7 +17,7 @@ jest.mock('react-router-dom', () => ({ }), })); -jest.mock('../../../../containers/source', () => ({ +jest.mock('../../../../containers/metrics_source', () => ({ useSourceContext: () => ({ createDerivedIndexPattern: () => 'jestbeat-*', }), diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 30c15410e1199..90cf96330e758 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -13,7 +13,7 @@ import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { useUrlState } from '../../../../utils/use_url_state'; -import { useSourceContext } from '../../../../containers/source'; +import { useSourceContext } from '../../../../containers/metrics_source'; import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; import { esKuery } from '../../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 6b980d33c2559..57073fee13c18 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -17,8 +17,8 @@ import { ColumnarPage } from '../../../components/page'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; -import { Source } from '../../../containers/source'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration/view_source_configuration_button'; +import { Source } from '../../../containers/metrics_source'; import { useTrackPageview } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Layout } from './components/layout'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts index 1e315f95dbd7c..dbe45a387891c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts @@ -14,7 +14,6 @@ const options: InfraWaffleMapOptions = { container: 'container.id', pod: 'kubernetes.pod.uid', host: 'host.name', - message: ['@message'], timestamp: '@timestanp', tiebreaker: '@timestamp', }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index 6b9912346f396..2a436eac30b2c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/e import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; -import { ViewSourceConfigurationButton } from '../../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../../components/source_configuration/view_source_configuration_button'; import { useLinkProps } from '../../../../hooks/use_link_props'; interface InvalidNodeErrorProps { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index d174707d8b6c9..13fa5cf1f0667 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -17,7 +17,7 @@ import { Header } from '../../../components/header'; import { ColumnarPage, PageContent } from '../../../components/page'; import { withMetricPageProviders } from './page_providers'; import { useMetadata } from './hooks/use_metadata'; -import { Source } from '../../../containers/source'; +import { Source } from '../../../containers/metrics_source'; import { InfraLoadingPanel } from '../../../components/loading'; import { findInventoryModel } from '../../../../common/inventory_models'; import { NavItem } from './lib/side_nav_context'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx index ac90e488cea94..c4e1b6bf8ef16 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx @@ -7,7 +7,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; -import { Source } from '../../../containers/source'; +import { Source } from '../../../containers/metrics_source'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = (Component: React.ComponentType) => ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 442382010d78c..35265f0a462cf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/charts'; import { first, last } from 'lodash'; import moment from 'moment'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, @@ -47,7 +47,7 @@ interface Props { options: MetricsExplorerOptions; chartOptions: MetricsExplorerChartOptions; series: MetricsExplorerSeries; - source: InfraSourceConfiguration | undefined; + source: MetricsSourceConfigurationProperties | undefined; timeRange: MetricsExplorerTimeOptions; onTimeChange: (start: string, end: string) => void; } diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index f5970cffa157d..8f281bda0229d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import DateMath from '@elastic/datemath'; import { Capabilities } from 'src/core/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { AlertFlyout } from '../../../../alerting/metric_threshold/components/alert_flyout'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { @@ -33,14 +33,14 @@ export interface Props { options: MetricsExplorerOptions; onFilter?: (query: string) => void; series: MetricsExplorerSeries; - source?: InfraSourceConfiguration; + source?: MetricsSourceConfigurationProperties; timeRange: MetricsExplorerTimeOptions; uiCapabilities?: Capabilities; chartOptions: MetricsExplorerChartOptions; } const fieldToNodeType = ( - source: InfraSourceConfiguration, + source: MetricsSourceConfigurationProperties, groupBy: string | string[] ): InventoryItemType | undefined => { const fields = Array.isArray(groupBy) ? groupBy : [groupBy]; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx index e2e64a6758a29..68faaf1f45145 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiFlexGrid, EuiFlexItem, EuiText, EuiHorizontalRule } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerResponse } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, @@ -31,7 +31,7 @@ interface Props { onFilter: (filter: string) => void; onTimeChange: (start: string, end: string) => void; data: MetricsExplorerResponse | null; - source: InfraSourceConfiguration | undefined; + source: MetricsSourceConfigurationProperties | undefined; timeRange: MetricsExplorerTimeOptions; } export const MetricsExplorerCharts = ({ diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index d2eeada219fa4..1a549041823ec 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -8,7 +8,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from '@elastic/safer-lodash-set'; -import { InfraSourceConfiguration } from '../../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../../common/metrics_sources'; import { colorTransformer, Color } from '../../../../../../common/color_palette'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { @@ -143,7 +143,7 @@ const createTSVBIndexPattern = (alias: string) => { }; export const createTSVBLink = ( - source: InfraSourceConfiguration | undefined, + source: MetricsSourceConfigurationProperties | undefined, options: MetricsExplorerOptions, series: MetricsExplorerSeries, timeRange: MetricsExplorerTimeOptions, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index eb5a4633d4fa9..a304c81ca1298 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -7,7 +7,7 @@ import { useState, useCallback, useContext } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerMetric, MetricsExplorerAggregation, @@ -28,7 +28,7 @@ export interface MetricExplorerViewState { } export const useMetricsExplorerState = ( - source: InfraSourceConfiguration, + source: MetricsSourceConfigurationProperties, derivedIndexPattern: IIndexPattern, shouldLoadImmediately = true ) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index 3d09a907be12f..9a5e5fcf39ce4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -22,7 +22,7 @@ import { import { MetricsExplorerOptions, MetricsExplorerTimeOptions } from './use_metrics_explorer_options'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { HttpHandler } from 'kibana/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; const mockedFetch = jest.fn(); @@ -38,7 +38,7 @@ const renderUseMetricsExplorerDataHook = () => { return renderHook( (props: { options: MetricsExplorerOptions; - source: InfraSourceConfiguration | undefined; + source: MetricsSourceConfigurationProperties | undefined; derivedIndexPattern: IIndexPattern; timeRange: MetricsExplorerTimeOptions; afterKey: string | null | Record; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index b6620e963217d..6689aedcd7209 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -9,7 +9,7 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState, useCallback } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSourceConfiguration } from '../../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerResponse, metricsExplorerResponseRT, @@ -25,7 +25,7 @@ function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOpt export function useMetricsExplorerData( options: MetricsExplorerOptions, - source: InfraSourceConfiguration | undefined, + source: MetricsSourceConfigurationProperties | undefined, derivedIndexPattern: IIndexPattern, timerange: MetricsExplorerTimeOptions, afterKey: string | null | Record, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index 3eb9bbacddd2e..0d1ac47812577 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -9,7 +9,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { InfraSourceConfiguration } from '../../../../common/http_api/source_api'; +import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; import { useTrackPageview } from '../../../../../observability/public'; import { DocumentTitle } from '../../../components/document_title'; import { NoData } from '../../../components/empty_states'; @@ -19,7 +19,7 @@ import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; import { useSavedViewContext } from '../../../containers/saved_view/saved_view'; interface MetricsExplorerPageProps { - source: InfraSourceConfiguration; + source: MetricsSourceConfigurationProperties; derivedIndexPattern: IIndexPattern; } diff --git a/x-pack/plugins/infra/public/pages/metrics/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx index c9be4abcf9e5f..c54725ab39754 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings.tsx @@ -8,7 +8,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; +import { SourceConfigurationSettings } from './settings/source_configuration_settings'; export const MetricsSettingsPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; diff --git a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/fields_configuration_panel.tsx similarity index 98% rename from x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/fields_configuration_panel.tsx index 2a8abdbc04f8e..7026f372ec7ff 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/fields_configuration_panel.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { InputFieldProps } from './input_fields'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface FieldsConfigurationPanelProps { containerFieldProps: InputFieldProps; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts similarity index 91% rename from x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts rename to x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts index b4dede79d11f2..ad26c1b13b0e1 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_form_state.ts @@ -11,16 +11,14 @@ import { createInputFieldProps, createInputRangeFieldProps, validateInputFieldNotEmpty, -} from './input_fields'; +} from '../../../components/source_configuration/input_fields'; interface FormState { name: string; description: string; metricAlias: string; - logAlias: string; containerField: string; hostField: string; - messageField: string[]; podField: string; tiebreakerField: string; timestampField: string; @@ -56,16 +54,6 @@ export const useIndicesConfigurationFormState = ({ }), [formState.name] ); - const logAliasFieldProps = useMemo( - () => - createInputFieldProps({ - errors: validateInputFieldNotEmpty(formState.logAlias), - name: 'logAlias', - onChange: (logAlias) => setFormStateChanges((changes) => ({ ...changes, logAlias })), - value: formState.logAlias, - }), - [formState.logAlias] - ); const metricAliasFieldProps = useMemo( () => createInputFieldProps({ @@ -144,7 +132,6 @@ export const useIndicesConfigurationFormState = ({ const fieldProps = useMemo( () => ({ name: nameFieldProps, - logAlias: logAliasFieldProps, metricAlias: metricAliasFieldProps, containerField: containerFieldFieldProps, hostField: hostFieldFieldProps, @@ -155,7 +142,6 @@ export const useIndicesConfigurationFormState = ({ }), [ nameFieldProps, - logAliasFieldProps, metricAliasFieldProps, containerFieldFieldProps, hostFieldFieldProps, @@ -193,11 +179,9 @@ export const useIndicesConfigurationFormState = ({ const defaultFormState: FormState = { name: '', description: '', - logAlias: '', metricAlias: '', containerField: '', hostField: '', - messageField: [], podField: '', tiebreakerField: '', timestampField: '', diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_panel.tsx similarity index 93% rename from x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_panel.tsx index cff9b78777aa3..c64ab2b0e9df5 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/indices_configuration_panel.tsx @@ -17,8 +17,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { METRICS_INDEX_PATTERN } from '../../../common/constants'; -import { InputFieldProps } from './input_fields'; +import { METRICS_INDEX_PATTERN } from '../../../../common/constants'; +import { InputFieldProps } from '../../../components/source_configuration/input_fields'; interface IndicesConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/ml_configuration_panel.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/ml_configuration_panel.tsx index 3bd498d460391..abf25dde0ea99 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/ml_configuration_panel.tsx @@ -13,7 +13,7 @@ import { EuiDescribedFormGroup } from '@elastic/eui'; import { EuiForm } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { InputRangeFieldProps } from './input_fields'; +import { InputRangeFieldProps } from '../../../components/source_configuration/input_fields'; interface MLConfigurationPanelProps { isLoading: boolean; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx similarity index 57% rename from x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx index c80235137eea6..37da4bd1aa1bd 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_form_state.tsx @@ -6,12 +6,12 @@ */ import { useCallback, useMemo } from 'react'; -import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; - +import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; import { useIndicesConfigurationFormState } from './indices_configuration_form_state'; -import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state'; -export const useSourceConfigurationFormState = (configuration?: InfraSourceConfiguration) => { +export const useSourceConfigurationFormState = ( + configuration?: MetricsSourceConfigurationProperties +) => { const indicesConfigurationFormState = useIndicesConfigurationFormState({ initialFormState: useMemo( () => @@ -19,11 +19,9 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi ? { name: configuration.name, description: configuration.description, - logAlias: configuration.logAlias, metricAlias: configuration.metricAlias, containerField: configuration.fields.container, hostField: configuration.fields.host, - messageField: configuration.fields.message, podField: configuration.fields.pod, tiebreakerField: configuration.fields.tiebreaker, timestampField: configuration.fields.timestamp, @@ -34,43 +32,26 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi ), }); - const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({ - initialFormState: useMemo( - () => - configuration - ? { - logColumns: configuration.logColumns, - } - : undefined, - [configuration] - ), - }); - - const errors = useMemo( - () => [...indicesConfigurationFormState.errors, ...logColumnsConfigurationFormState.errors], - [indicesConfigurationFormState.errors, logColumnsConfigurationFormState.errors] - ); + const errors = useMemo(() => [...indicesConfigurationFormState.errors], [ + indicesConfigurationFormState.errors, + ]); const resetForm = useCallback(() => { indicesConfigurationFormState.resetForm(); - logColumnsConfigurationFormState.resetForm(); - }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); + }, [indicesConfigurationFormState]); - const isFormDirty = useMemo( - () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, - [indicesConfigurationFormState.isFormDirty, logColumnsConfigurationFormState.isFormDirty] - ); + const isFormDirty = useMemo(() => indicesConfigurationFormState.isFormDirty, [ + indicesConfigurationFormState.isFormDirty, + ]); - const isFormValid = useMemo( - () => indicesConfigurationFormState.isFormValid && logColumnsConfigurationFormState.isFormValid, - [indicesConfigurationFormState.isFormValid, logColumnsConfigurationFormState.isFormValid] - ); + const isFormValid = useMemo(() => indicesConfigurationFormState.isFormValid, [ + indicesConfigurationFormState.isFormValid, + ]); const formState = useMemo( () => ({ name: indicesConfigurationFormState.formState.name, description: indicesConfigurationFormState.formState.description, - logAlias: indicesConfigurationFormState.formState.logAlias, metricAlias: indicesConfigurationFormState.formState.metricAlias, fields: { container: indicesConfigurationFormState.formState.containerField, @@ -79,17 +60,15 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi tiebreaker: indicesConfigurationFormState.formState.tiebreakerField, timestamp: indicesConfigurationFormState.formState.timestampField, }, - logColumns: logColumnsConfigurationFormState.formState.logColumns, anomalyThreshold: indicesConfigurationFormState.formState.anomalyThreshold, }), - [indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState] + [indicesConfigurationFormState.formState] ); const formStateChanges = useMemo( () => ({ name: indicesConfigurationFormState.formStateChanges.name, description: indicesConfigurationFormState.formStateChanges.description, - logAlias: indicesConfigurationFormState.formStateChanges.logAlias, metricAlias: indicesConfigurationFormState.formStateChanges.metricAlias, fields: { container: indicesConfigurationFormState.formStateChanges.containerField, @@ -98,25 +77,18 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi tiebreaker: indicesConfigurationFormState.formStateChanges.tiebreakerField, timestamp: indicesConfigurationFormState.formStateChanges.timestampField, }, - logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns, anomalyThreshold: indicesConfigurationFormState.formStateChanges.anomalyThreshold, }), - [ - indicesConfigurationFormState.formStateChanges, - logColumnsConfigurationFormState.formStateChanges, - ] + [indicesConfigurationFormState.formStateChanges] ); return { - addLogColumn: logColumnsConfigurationFormState.addLogColumn, - moveLogColumn: logColumnsConfigurationFormState.moveLogColumn, errors, formState, formStateChanges, isFormDirty, isFormValid, indicesConfigurationProps: indicesConfigurationFormState.fieldProps, - logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps, resetForm, }; }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx similarity index 94% rename from x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx index e63f43470497d..71fa4e7600503 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx @@ -19,15 +19,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useContext, useMemo } from 'react'; -import { Source } from '../../containers/source'; +import { Source } from '../../../containers/metrics_source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; import { IndicesConfigurationPanel } from './indices_configuration_panel'; -import { NameConfigurationPanel } from './name_configuration_panel'; +import { NameConfigurationPanel } from '../../../components/source_configuration/name_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; -import { SourceLoadingPage } from '../source_loading_page'; -import { Prompt } from '../../utils/navigation_warning_prompt'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { Prompt } from '../../../utils/navigation_warning_prompt'; import { MLConfigurationPanel } from './ml_configuration_panel'; -import { useInfraMLCapabilitiesContext } from '../../containers/ml/infra_ml_capabilities'; +import { useInfraMLCapabilitiesContext } from '../../../containers/ml/infra_ml_capabilities'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; diff --git a/x-pack/plugins/infra/public/utils/source_configuration.ts b/x-pack/plugins/infra/public/utils/source_configuration.ts index b7b45d1927711..a3e1741c7590b 100644 --- a/x-pack/plugins/infra/public/utils/source_configuration.ts +++ b/x-pack/plugins/infra/public/utils/source_configuration.ts @@ -6,14 +6,14 @@ */ import { - InfraSavedSourceConfigurationColumn, - InfraSavedSourceConfigurationFields, + InfraSourceConfigurationColumn, + InfraSourceConfigurationFieldColumn, InfraSourceConfigurationMessageColumn, InfraSourceConfigurationTimestampColumn, -} from '../../common/http_api/source_api'; +} from '../../common/source_configuration/source_configuration'; -export type LogColumnConfiguration = InfraSavedSourceConfigurationColumn; -export type FieldLogColumnConfiguration = InfraSavedSourceConfigurationFields; +export type LogColumnConfiguration = InfraSourceConfigurationColumn; +export type FieldLogColumnConfiguration = InfraSourceConfigurationFieldColumn; export type MessageLogColumnConfiguration = InfraSourceConfigurationMessageColumn; export type TimestampLogColumnConfiguration = InfraSourceConfigurationTimestampColumn; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 69595c90c7911..f42207e0ad142 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -32,7 +32,7 @@ import { } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; -import { initSourceRoute } from './routes/source'; +import { initMetricsSourceConfigurationRoutes } from './routes/metrics_sources'; import { initOverviewRoute } from './routes/overview'; import { initAlertPreviewRoute } from './routes/alerting'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; @@ -50,7 +50,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetHostsAnomaliesRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); - initSourceRoute(libs); + initMetricsSourceConfigurationRoutes(libs); initValidateLogAnalysisDatasetsRoute(libs); initValidateLogAnalysisIndicesRoute(libs); initGetLogEntryExamplesRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index e390d6525cd60..921634361f4a2 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -34,7 +34,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { options: InfraMetricsRequestOptions, rawRequest: KibanaRequest ): Promise { - const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; + const indexPattern = `${options.sourceConfiguration.metricAlias}`; const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); const nodeField = fields.id; @@ -112,7 +112,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ); } - const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; + const indexPattern = `${options.sourceConfiguration.metricAlias}`; const timerange = { min: options.timerange.from, max: options.timerange.to, @@ -132,7 +132,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { const calculatedInterval = await calculateMetricInterval( client, { - indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, + indexPattern: `${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, timerange: options.timerange, }, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 439764f80186e..5244b8a81e75f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -23,6 +23,7 @@ import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_ap import { InfraSource } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { getNodes } from '../../../routes/snapshot/lib/get_nodes'; +import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; type ConditionResult = InventoryMetricConditions & { shouldFire: boolean[]; @@ -36,6 +37,7 @@ export const evaluateCondition = async ( condition: InventoryMetricConditions, nodeType: InventoryItemType, source: InfraSource, + logQueryFields: LogQueryFields, esClient: ElasticsearchClient, filterQuery?: string, lookbackSize?: number @@ -58,6 +60,7 @@ export const evaluateCondition = async ( metric, timerange, source, + logQueryFields, filterQuery, customMetric ); @@ -101,6 +104,7 @@ const getData = async ( metric: SnapshotMetricType, timerange: InfraTimerangeInput, source: InfraSource, + logQueryFields: LogQueryFields, filterQuery?: string, customMetric?: SnapshotCustomMetricInput ) => { @@ -124,7 +128,7 @@ const getData = async ( includeTimeseries: Boolean(timerange.lookbackSize), }; try { - const { nodes } = await getNodes(client, snapshotRequest, source); + const { nodes } = await getNodes(client, snapshotRequest, source, logQueryFields); if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 632ba9cd6f282..d775a503d1d32 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -68,12 +68,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = sourceId || 'default' ); + const logQueryFields = await libs.getLogQueryFields( + sourceId || 'default', + services.savedObjectsClient + ); + const results = await Promise.all( criteria.map((c) => evaluateCondition( c, nodeType, source, + logQueryFields, services.scopedClusterClient.asCurrentUser, filterQuery ) diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 472f9d408694c..f254f1e68ae46 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -14,10 +14,11 @@ import { isTooManyBucketsPreviewException, } from '../../../../common/alerting/metrics'; import { ElasticsearchClient } from '../../../../../../../src/core/server'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { InfraSource } from '../../../../common/source_configuration/source_configuration'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { evaluateCondition } from './evaluate_condition'; +import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; interface InventoryMetricThresholdParams { criteria: InventoryMetricConditions[]; @@ -30,6 +31,7 @@ interface PreviewInventoryMetricThresholdAlertParams { esClient: ElasticsearchClient; params: InventoryMetricThresholdParams; source: InfraSource; + logQueryFields: LogQueryFields; lookback: Unit; alertInterval: string; alertThrottle: string; @@ -43,6 +45,7 @@ export const previewInventoryMetricThresholdAlert: ( esClient, params, source, + logQueryFields, lookback, alertInterval, alertThrottle, @@ -68,7 +71,7 @@ export const previewInventoryMetricThresholdAlert: ( try { const results = await Promise.all( criteria.map((c) => - evaluateCondition(c, nodeType, source, esClient, filterQuery, lookbackSize) + evaluateCondition(c, nodeType, source, logQueryFields, esClient, filterQuery, lookbackSize) ) ); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index f6214edc5d0ab..87150aa134837 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -11,7 +11,7 @@ import { isTooManyBucketsPreviewException, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, } from '../../../../../common/alerting/metrics'; -import { InfraSource } from '../../../../../common/http_api/source_api'; +import { InfraSource } from '../../../../../common/source_configuration/source_configuration'; import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../../utils/get_all_composite_data'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 064804b661b74..a4c207f4006d5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -12,7 +12,7 @@ import { isTooManyBucketsPreviewException, } from '../../../../common/alerting/metrics'; import { ElasticsearchClient } from '../../../../../../../src/core/server'; -import { InfraSource } from '../../../../common/http_api/source_api'; +import { InfraSource } from '../../../../common/source_configuration/source_configuration'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { PreviewResult } from '../common/types'; import { MetricExpressionParams } from './types'; diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts index b653351a34760..d5ffa56987666 100644 --- a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -18,21 +18,16 @@ export class InfraFieldsDomain { public async getFields( requestContext: InfraPluginRequestHandlerContext, sourceId: string, - indexType: 'LOGS' | 'METRICS' | 'ANY' + indexType: 'LOGS' | 'METRICS' ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId ); - const includeMetricIndices = ['ANY', 'METRICS'].includes(indexType); - const includeLogIndices = ['ANY', 'LOGS'].includes(indexType); const fields = await this.adapter.getIndexFields( requestContext, - [ - ...(includeMetricIndices ? [configuration.metricAlias] : []), - ...(includeLogIndices ? [configuration.logAlias] : []), - ].join(',') + indexType === 'LOGS' ? configuration.logAlias : configuration.metricAlias ); return fields; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index e3c42c4dceede..278ae0e086cfc 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -17,7 +17,7 @@ import { LogColumn, LogEntryCursor, LogEntry } from '../../../../common/log_entr import { InfraSourceConfiguration, InfraSources, - SavedSourceConfigurationFieldColumnRuntimeType, + SourceConfigurationFieldColumnRuntimeType, } from '../../sources'; import { getBuiltinRules } from '../../../services/log_entries/message/builtin_rules'; import { @@ -349,7 +349,7 @@ const getRequiredFields = ( ): string[] => { const fieldsFromCustomColumns = configuration.logColumns.reduce( (accumulatedFields, logColumn) => { - if (SavedSourceConfigurationFieldColumnRuntimeType.is(logColumn)) { + if (SourceConfigurationFieldColumnRuntimeType.is(logColumn)) { return [...accumulatedFields, logColumn.fieldColumn.field]; } return accumulatedFields; diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 65bb5f878b275..08e42279e4939 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { InfraSourceConfiguration } from '../../common/http_api/source_api'; +import { InfraSourceConfiguration } from '../../common/source_configuration/source_configuration'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; @@ -13,6 +13,7 @@ import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; import { InfraConfig } from '../plugin'; import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; +import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -25,6 +26,7 @@ export interface InfraBackendLibs extends InfraDomainLibs { framework: KibanaFramework; sources: InfraSources; sourceStatus: InfraSourceStatus; + getLogQueryFields: GetLogQueryFields; } export interface InfraConfiguration { diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index cb89c5a6b1bd3..e436ad2ba0b05 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -120,5 +120,5 @@ export const query = async ( ThrowReporter.report(HistogramResponseRT.decode(response.aggregations)); } - throw new Error('Elasticsearch responsed with an unrecoginzed format.'); + throw new Error('Elasticsearch responded with an unrecognized format.'); }; diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index 1b924619a905c..ff6d6a4f5514b 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -10,7 +10,7 @@ import { LOGS_INDEX_PATTERN, TIMESTAMP_FIELD, } from '../../../common/constants'; -import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; +import { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', diff --git a/x-pack/plugins/infra/server/lib/sources/index.ts b/x-pack/plugins/infra/server/lib/sources/index.ts index 57852f7f3e4e6..27ad665be31a9 100644 --- a/x-pack/plugins/infra/server/lib/sources/index.ts +++ b/x-pack/plugins/infra/server/lib/sources/index.ts @@ -8,4 +8,4 @@ export * from './defaults'; export { infraSourceConfigurationSavedObjectType } from './saved_object_type'; export * from './sources'; -export * from '../../../common/http_api/source_api'; +export * from '../../../common/source_configuration/source_configuration'; diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts index dbfe0f81c187a..e71994fe11517 100644 --- a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts @@ -6,7 +6,7 @@ */ import { SavedObjectMigrationFn } from 'src/core/server'; -import { InfraSourceConfiguration } from '../../../../common/http_api/source_api'; +import { InfraSourceConfiguration } from '../../../../common/source_configuration/source_configuration'; export const addNewIndexingStrategyIndexNames: SavedObjectMigrationFn< InfraSourceConfiguration, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index fe005b04978da..7abbed0a9fbdd 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -23,7 +23,7 @@ import { SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, InfraSource, -} from '../../../common/http_api/source_api'; +} from '../../../common/source_configuration/source_configuration'; import { InfraConfig } from '../../../server'; interface Libs { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index c80e012844c1e..50fec38b9f2df 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -9,7 +9,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; -import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; +import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; @@ -30,6 +30,7 @@ import { InfraSourceStatus } from './lib/source_status'; import { LogEntriesService } from './services/log_entries'; import { InfraPluginRequestHandlerContext } from './types'; import { UsageCollector } from './usage/usage_collector'; +import { createGetLogQueryFields } from './services/log_queries/get_log_query_fields'; export const config = { schema: schema.object({ @@ -123,6 +124,7 @@ export class InfraServerPlugin implements Plugin { sources, sourceStatus, ...domainLibs, + getLogQueryFields: createGetLogQueryFields(sources), }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 6622df1a8333a..4d980834d3a70 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -25,7 +25,11 @@ import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/pre import { InfraBackendLibs } from '../../lib/infra_types'; import { assertHasInfraMlPlugins } from '../../utils/request_context'; -export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { +export const initAlertPreviewRoute = ({ + framework, + sources, + getLogQueryFields, +}: InfraBackendLibs) => { framework.registerRoute( { method: 'post', @@ -77,6 +81,10 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { + const logQueryFields = await getLogQueryFields( + sourceId || 'default', + requestContext.core.savedObjects.client + ); const { nodeType, criteria, @@ -87,6 +95,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) params: { criteria, filterQuery, nodeType }, lookback, source, + logQueryFields, alertInterval, alertThrottle, alertNotifyWhen, diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/metrics_sources/index.ts similarity index 69% rename from x-pack/plugins/infra/server/routes/source/index.ts rename to x-pack/plugins/infra/server/routes/metrics_sources/index.ts index 5ab3275f9ea9e..0123e4678697c 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_sources/index.ts @@ -8,63 +8,49 @@ import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; import { createValidationFunction } from '../../../common/runtime_types'; -import { - InfraSourceStatus, - SavedSourceConfigurationRuntimeType, - SourceResponseRuntimeType, -} from '../../../common/http_api/source_api'; import { InfraBackendLibs } from '../../lib/infra_types'; import { hasData } from '../../lib/sources/has_data'; import { createSearchClient } from '../../lib/create_search_client'; import { AnomalyThresholdRangeError } from '../../lib/sources/errors'; +import { + partialMetricsSourceConfigurationPropertiesRT, + metricsSourceConfigurationResponseRT, + MetricsSourceStatus, +} from '../../../common/metrics_sources'; -const typeToInfraIndexType = (value: string | undefined) => { - switch (value) { - case 'metrics': - return 'METRICS'; - case 'logs': - return 'LOGS'; - default: - return 'ANY'; - } -}; - -export const initSourceRoute = (libs: InfraBackendLibs) => { +export const initMetricsSourceConfigurationRoutes = (libs: InfraBackendLibs) => { const { framework } = libs; framework.registerRoute( { method: 'get', - path: '/api/metrics/source/{sourceId}/{type?}', + path: '/api/metrics/source/{sourceId}', validate: { params: schema.object({ sourceId: schema.string(), - type: schema.string(), }), }, }, async (requestContext, request, response) => { - const { type, sourceId } = request.params; + const { sourceId } = request.params; - const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ + const [source, metricIndicesExist, indexFields] = await Promise.all([ libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), + libs.fields.getFields(requestContext, sourceId, 'METRICS'), ]); if (!source) { return response.notFound(); } - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', + const status: MetricsSourceStatus = { metricIndicesExist, indexFields, }; return response.ok({ - body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), + body: metricsSourceConfigurationResponseRT.encode({ source: { ...source, status } }), }); } ); @@ -77,7 +63,7 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { params: schema.object({ sourceId: schema.string(), }), - body: createValidationFunction(SavedSourceConfigurationRuntimeType), + body: createValidationFunction(partialMetricsSourceConfigurationPropertiesRT), }, }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { @@ -110,20 +96,18 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { patchedSourceConfigurationProperties )); - const [logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), + const [metricIndicesExist, indexFields] = await Promise.all([ libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType('metrics')), + libs.fields.getFields(requestContext, sourceId, 'METRICS'), ]); - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', + const status: MetricsSourceStatus = { metricIndicesExist, indexFields, }; return response.ok({ - body: SourceResponseRuntimeType.encode({ + body: metricsSourceConfigurationResponseRT.encode({ source: { ...patchedSourceConfiguration, status }, }), }); @@ -154,25 +138,23 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { framework.registerRoute( { method: 'get', - path: '/api/metrics/source/{sourceId}/{type}/hasData', + path: '/api/metrics/source/{sourceId}/hasData', validate: { params: schema.object({ sourceId: schema.string(), - type: schema.string(), }), }, }, async (requestContext, request, response) => { - const { type, sourceId } = request.params; + const { sourceId } = request.params; const client = createSearchClient(requestContext, framework); const source = await libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId ); - const indexPattern = - type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; - const results = await hasData(indexPattern, client); + + const results = await hasData(source.configuration.metricAlias, client); return response.ok({ body: { hasData: results }, diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index aaf23085d0d60..cbadd26ccd4bf 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -41,9 +41,15 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { snapshotRequest.sourceId ); + const logQueryFields = await libs.getLogQueryFields( + snapshotRequest.sourceId, + requestContext.core.savedObjects.client + ); + UsageCollector.countNode(snapshotRequest.nodeType); const client = createSearchClient(requestContext, framework); - const snapshotResponse = await getNodes(client, snapshotRequest, source); + + const snapshotResponse = await getNodes(client, snapshotRequest, source, logQueryFields); return response.ok({ body: SnapshotNodeResponseRT.encode(snapshotResponse), diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts deleted file mode 100644 index 85c1ece1ca042..0000000000000 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SnapshotRequest } from '../../../../common/http_api'; -import { InfraSource } from '../../../lib/sources'; - -export const calculateIndexPatterBasedOnMetrics = ( - options: SnapshotRequest, - source: InfraSource -) => { - const { metrics } = options; - if (metrics.every((m) => m.type === 'logRate')) { - return source.configuration.logAlias; - } - if (metrics.some((m) => m.type === 'logRate')) { - return `${source.configuration.logAlias},${source.configuration.metricAlias}`; - } - return source.configuration.metricAlias; -}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index 9dec21d3ab1c7..ff3cf048b99de 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -12,16 +12,24 @@ import { transformRequestToMetricsAPIRequest } from './transform_request_to_metr import { queryAllData } from './query_all_data'; import { transformMetricsApiResponseToSnapshotResponse } from './trasform_metrics_ui_response'; import { copyMissingMetrics } from './copy_missing_metrics'; +import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; -export const getNodes = async ( +export interface SourceOverrides { + indexPattern: string; + timestamp: string; +} + +const transformAndQueryData = async ( client: ESSearchClient, snapshotRequest: SnapshotRequest, - source: InfraSource + source: InfraSource, + sourceOverrides?: SourceOverrides ) => { const metricsApiRequest = await transformRequestToMetricsAPIRequest( client, source, - snapshotRequest + snapshotRequest, + sourceOverrides ); const metricsApiResponse = await queryAllData(client, metricsApiRequest); const snapshotResponse = transformMetricsApiResponseToSnapshotResponse( @@ -32,3 +40,59 @@ export const getNodes = async ( ); return copyMissingMetrics(snapshotResponse); }; + +export const getNodes = async ( + client: ESSearchClient, + snapshotRequest: SnapshotRequest, + source: InfraSource, + logQueryFields: LogQueryFields +) => { + let nodes; + + if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) { + // *Only* the log rate metric has been requested + if (snapshotRequest.metrics.length === 1) { + nodes = await transformAndQueryData(client, snapshotRequest, source, logQueryFields); + } else { + // A scenario whereby a single host might be shipping metrics and logs. + const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter( + (metric) => metric.type !== 'logRate' + ); + const nodesWithoutLogsMetrics = await transformAndQueryData( + client, + { ...snapshotRequest, metrics: metricsWithoutLogsMetrics }, + source + ); + const logRateNodes = await transformAndQueryData( + client, + { ...snapshotRequest, metrics: [{ type: 'logRate' }] }, + source, + logQueryFields + ); + // Merge nodes where possible - e.g. a single host is shipping metrics and logs + const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => { + const logRateNode = logRateNodes.nodes.find( + (_logRateNode) => node.name === _logRateNode.name + ); + if (logRateNode) { + // Remove this from the "leftovers" + logRateNodes.nodes.filter((_node) => _node.name !== logRateNode.name); + } + return logRateNode + ? { + ...node, + metrics: [...node.metrics, ...logRateNode.metrics], + } + : node; + }); + nodes = { + ...nodesWithoutLogsMetrics, + nodes: [...mergedNodes, ...logRateNodes.nodes], + }; + } + } else { + nodes = await transformAndQueryData(client, snapshotRequest, source); + } + + return nodes; +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 8804121fc4167..128137efa272e 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -12,13 +12,14 @@ import { InfraSource } from '../../../lib/sources'; import { createTimeRangeWithInterval } from './create_timerange_with_interval'; import { parseFilterQuery } from '../../../utils/serialized_query'; import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapshot_metrics_to_metrics_api_metrics'; -import { calculateIndexPatterBasedOnMetrics } from './calculate_index_pattern_based_on_metrics'; import { META_KEY } from './constants'; +import { SourceOverrides } from './get_nodes'; export const transformRequestToMetricsAPIRequest = async ( client: ESSearchClient, source: InfraSource, - snapshotRequest: SnapshotRequest + snapshotRequest: SnapshotRequest, + sourceOverrides?: SourceOverrides ): Promise => { const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, { ...snapshotRequest, @@ -27,9 +28,9 @@ export const transformRequestToMetricsAPIRequest = async ( }); const metricsApiRequest: MetricsAPIRequest = { - indexPattern: calculateIndexPatterBasedOnMetrics(snapshotRequest, source), + indexPattern: sourceOverrides?.indexPattern ?? source.configuration.metricAlias, timerange: { - field: source.configuration.fields.timestamp, + field: sourceOverrides?.timestamp ?? source.configuration.fields.timestamp, from: timeRangeWithIntervalApplied.from, to: timeRangeWithIntervalApplied.to, interval: timeRangeWithIntervalApplied.interval, @@ -74,7 +75,7 @@ export const transformRequestToMetricsAPIRequest = async ( top_hits: { size: 1, _source: [inventoryFields.name], - sort: [{ [source.configuration.fields.timestamp]: 'desc' }], + sort: [{ [sourceOverrides?.timestamp ?? source.configuration.fields.timestamp]: 'desc' }], }, }, }, diff --git a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts new file mode 100644 index 0000000000000..9497a8b442768 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { InfraSources } from '../../lib/sources'; + +// NOTE: TEMPORARY: This will become a subset of the new resolved KIP compatible log source configuration. +export interface LogQueryFields { + indexPattern: string; + timestamp: string; +} + +// NOTE: TEMPORARY: This will become a subset of the new resolved KIP compatible log source configuration. +export const createGetLogQueryFields = (sources: InfraSources) => { + return async ( + sourceId: string, + savedObjectsClient: SavedObjectsClientContract + ): Promise => { + const source = await sources.getSourceConfiguration(savedObjectsClient, sourceId); + + return { + indexPattern: source.configuration.logAlias, + timestamp: source.configuration.fields.timestamp, + }; + }; +}; + +export type GetLogQueryFields = ReturnType; diff --git a/x-pack/test/api_integration/apis/metrics_ui/http_source.ts b/x-pack/test/api_integration/apis/metrics_ui/http_source.ts index aecff3eaa5cb8..912266bf87e42 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/http_source.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/http_source.ts @@ -15,16 +15,14 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const fetchSource = async (): Promise => { const response = await supertest - .get('/api/metrics/source/default/metrics') + .get('/api/metrics/source/default') .set('kbn-xsrf', 'xxx') .expect(200); return response.body; }; - const fetchHasData = async ( - type: 'logs' | 'metrics' - ): Promise<{ hasData: boolean } | undefined> => { + const fetchHasData = async (): Promise<{ hasData: boolean } | undefined> => { const response = await supertest - .get(`/api/metrics/source/default/${type}/hasData`) + .get(`/api/metrics/source/default/hasData`) .set('kbn-xsrf', 'xxx') .expect(200); return response.body; @@ -34,41 +32,27 @@ export default function ({ getService }: FtrProviderContext) { describe('8.0.0', () => { before(() => esArchiver.load('infra/8.0.0/logs_and_metrics')); after(() => esArchiver.unload('infra/8.0.0/logs_and_metrics')); - describe('/api/metrics/source/default/metrics', () => { + describe('/api/metrics/source/default', () => { it('should just work', async () => { const resp = fetchSource(); return resp.then((data) => { expect(data).to.have.property('source'); expect(data?.source.configuration.metricAlias).to.equal('metrics-*,metricbeat-*'); - expect(data?.source.configuration.logAlias).to.equal( - 'logs-*,filebeat-*,kibana_sample_data_logs*' - ); expect(data?.source.configuration.fields).to.eql({ container: 'container.id', host: 'host.name', - message: ['message', '@message'], pod: 'kubernetes.pod.uid', tiebreaker: '_doc', timestamp: '@timestamp', }); expect(data?.source).to.have.property('status'); expect(data?.source.status?.metricIndicesExist).to.equal(true); - expect(data?.source.status?.logIndicesExist).to.equal(true); }); }); }); - describe('/api/metrics/source/default/metrics/hasData', () => { + describe('/api/metrics/source/default/hasData', () => { it('should just work', async () => { - const resp = fetchHasData('metrics'); - return resp.then((data) => { - expect(data).to.have.property('hasData'); - expect(data?.hasData).to.be(true); - }); - }); - }); - describe('/api/metrics/source/default/logs/hasData', () => { - it('should just work', async () => { - const resp = fetchHasData('logs'); + const resp = fetchHasData(); return resp.then((data) => { expect(data).to.have.property('hasData'); expect(data?.hasData).to.be(true); diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index a5bab8de92f38..d55530a501366 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -8,10 +8,10 @@ import expect from '@kbn/expect'; import { - SourceResponse, - InfraSavedSourceConfiguration, - SourceResponseRuntimeType, -} from '../../../../plugins/infra/common/http_api/source_api'; + MetricsSourceConfigurationResponse, + PartialMetricsSourceConfigurationProperties, + metricsSourceConfigurationResponseRT, +} from '../../../../plugins/infra/common/metrics_sources'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -19,8 +19,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const SOURCE_API_URL = '/api/metrics/source/default'; const patchRequest = async ( - body: InfraSavedSourceConfiguration - ): Promise => { + body: PartialMetricsSourceConfigurationProperties + ): Promise => { const response = await supertest .patch(SOURCE_API_URL) .set('kbn-xsrf', 'xxx') @@ -51,10 +51,9 @@ export default function ({ getService }: FtrProviderContext) { name: 'UPDATED_NAME', description: 'UPDATED_DESCRIPTION', metricAlias: 'metricbeat-**', - logAlias: 'filebeat-**', }); - expect(SourceResponseRuntimeType.is(updateResponse)).to.be(true); + expect(metricsSourceConfigurationResponseRT.is(updateResponse)).to.be(true); const version = updateResponse?.source.version; const updatedAt = updateResponse?.source.updatedAt; @@ -67,15 +66,12 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration?.name).to.be('UPDATED_NAME'); expect(configuration?.description).to.be('UPDATED_DESCRIPTION'); expect(configuration?.metricAlias).to.be('metricbeat-**'); - expect(configuration?.logAlias).to.be('filebeat-**'); expect(configuration?.fields.host).to.be('host.name'); expect(configuration?.fields.pod).to.be('kubernetes.pod.uid'); expect(configuration?.fields.tiebreaker).to.be('_doc'); expect(configuration?.fields.timestamp).to.be('@timestamp'); expect(configuration?.fields.container).to.be('container.id'); - expect(configuration?.logColumns).to.have.length(3); expect(configuration?.anomalyThreshold).to.be(50); - expect(status?.logIndicesExist).to.be(true); expect(status?.metricIndicesExist).to.be(true); }); @@ -105,8 +101,6 @@ export default function ({ getService }: FtrProviderContext) { expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt || 0); expect(configuration?.metricAlias).to.be('metricbeat-**'); - expect(configuration?.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); - expect(status?.logIndicesExist).to.be(true); expect(status?.metricIndicesExist).to.be(true); }); @@ -144,37 +138,6 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration?.fields.timestamp).to.be('@timestamp'); }); - it('applies a log column update to an existing source', async () => { - const creationResponse = await patchRequest({ - name: 'NAME', - }); - - const initialVersion = creationResponse?.source.version; - const createdAt = creationResponse?.source.updatedAt; - - const updateResponse = await patchRequest({ - logColumns: [ - { - fieldColumn: { - id: 'ADDED_COLUMN_ID', - field: 'ADDED_COLUMN_FIELD', - }, - }, - ], - }); - - const version = updateResponse?.source.version; - const updatedAt = updateResponse?.source.updatedAt; - const configuration = updateResponse?.source.configuration; - expect(version).to.be.a('string'); - expect(version).to.not.be(initialVersion); - expect(updatedAt).to.be.greaterThan(createdAt || 0); - expect(configuration?.logColumns).to.have.length(1); - expect(configuration?.logColumns[0]).to.have.key('fieldColumn'); - const fieldColumn = (configuration?.logColumns[0] as any).fieldColumn; - expect(fieldColumn).to.have.property('id', 'ADDED_COLUMN_ID'); - expect(fieldColumn).to.have.property('field', 'ADDED_COLUMN_FIELD'); - }); it('validates anomalyThreshold is between range 1-100', async () => { // create config with bad request await supertest diff --git a/x-pack/test/api_integration/services/infraops_source_configuration.ts b/x-pack/test/api_integration/services/infraops_source_configuration.ts index 5c1566827b701..f78cc880a1d17 100644 --- a/x-pack/test/api_integration/services/infraops_source_configuration.ts +++ b/x-pack/test/api_integration/services/infraops_source_configuration.ts @@ -6,17 +6,17 @@ */ import { - InfraSavedSourceConfiguration, - SourceResponse, -} from '../../../plugins/infra/common/http_api/source_api'; + PartialMetricsSourceConfiguration, + MetricsSourceConfigurationResponse, +} from '../../../plugins/infra/common/metrics_sources'; import { FtrProviderContext } from '../ftr_provider_context'; export function InfraOpsSourceConfigurationProvider({ getService }: FtrProviderContext) { const log = getService('log'); const supertest = getService('supertest'); const patchRequest = async ( - body: InfraSavedSourceConfiguration - ): Promise => { + body: PartialMetricsSourceConfiguration + ): Promise => { const response = await supertest .patch('/api/metrics/source/default') .set('kbn-xsrf', 'xxx') @@ -26,7 +26,10 @@ export function InfraOpsSourceConfigurationProvider({ getService }: FtrProviderC }; return { - async createConfiguration(sourceId: string, sourceProperties: InfraSavedSourceConfiguration) { + async createConfiguration( + sourceId: string, + sourceProperties: PartialMetricsSourceConfiguration + ) { log.debug( `Creating Infra UI source configuration "${sourceId}" with properties ${JSON.stringify( sourceProperties From c5e3e78de8469abde694b5644137868ae39ae1c7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Mar 2021 13:00:16 -0600 Subject: [PATCH 08/98] [Maps] do not track total hits for elasticsearch search requests (#91754) * [Maps] do not track total hits for elasticsearch search requests * set track_total_hits for es_search_source tooltip fetch * tslint * searchSource doc updates, set track_total_hits in MVT requests * revert changes made to searchsourcefields docs * tslint * review feedback * tslint * remove Hits (Total) from functional tests * remove sleep in functional test * tslint * fix method name Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../maps/common/elasticsearch_util/index.ts | 1 + .../elasticsearch_util/total_hits.test.ts | 48 +++++++++++++++ .../common/elasticsearch_util/total_hits.ts | 33 +++++++++++ .../blended_vector_layer.ts | 10 +++- .../es_geo_grid_source/es_geo_grid_source.tsx | 1 + .../es_geo_line_source/es_geo_line_source.tsx | 2 + .../es_pew_pew_source/es_pew_pew_source.js | 7 ++- .../es_search_source/es_search_source.tsx | 11 ++-- .../classes/sources/es_source/es_source.ts | 8 ++- .../sources/es_term_source/es_term_source.ts | 1 + x-pack/plugins/maps/server/mvt/get_tile.ts | 24 +++++++- .../apps/maps/blended_vector_layer.js | 19 ++---- .../maps/documents_source/docvalue_fields.js | 18 +----- .../apps/maps/embeddable/dashboard.js | 33 ++++------- .../apps/maps/es_geo_grid_source.js | 58 ++++--------------- .../functional/apps/maps/es_pew_pew_source.js | 12 +--- x-pack/test/functional/apps/maps/joins.js | 22 ++----- .../test/functional/page_objects/gis_page.ts | 27 +++++++++ 18 files changed, 198 insertions(+), 137 deletions(-) create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts create mode 100644 x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts diff --git a/x-pack/plugins/maps/common/elasticsearch_util/index.ts b/x-pack/plugins/maps/common/elasticsearch_util/index.ts index 0b6eaa435264c..24dd56b217401 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/index.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/index.ts @@ -8,3 +8,4 @@ export * from './es_agg_utils'; export * from './convert_to_geojson'; export * from './elasticsearch_geo_utils'; +export { isTotalHitsGreaterThan, TotalHits } from './total_hits'; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts new file mode 100644 index 0000000000000..211cb2d302f2c --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isTotalHitsGreaterThan, TotalHits } from './total_hits'; + +describe('total.relation: eq', () => { + const totalHits = { + value: 100, + relation: 'eq' as TotalHits['relation'], + }; + + test('total.value: 100 should be more than 90', () => { + expect(isTotalHitsGreaterThan(totalHits, 90)).toBe(true); + }); + + test('total.value: 100 should not be more than 100', () => { + expect(isTotalHitsGreaterThan(totalHits, 100)).toBe(false); + }); + + test('total.value: 100 should not be more than 110', () => { + expect(isTotalHitsGreaterThan(totalHits, 110)).toBe(false); + }); +}); + +describe('total.relation: gte', () => { + const totalHits = { + value: 100, + relation: 'gte' as TotalHits['relation'], + }; + + test('total.value: 100 should be more than 90', () => { + expect(isTotalHitsGreaterThan(totalHits, 90)).toBe(true); + }); + + test('total.value: 100 should be more than 100', () => { + expect(isTotalHitsGreaterThan(totalHits, 100)).toBe(true); + }); + + test('total.value: 100 should throw error when value is more than 100', () => { + expect(() => { + isTotalHitsGreaterThan(totalHits, 110); + }).toThrow(); + }); +}); diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts new file mode 100644 index 0000000000000..5de38d3f28851 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export interface TotalHits { + value: number; + relation: 'eq' | 'gte'; +} + +export function isTotalHitsGreaterThan(totalHits: TotalHits, value: number) { + if (totalHits.relation === 'eq') { + return totalHits.value > value; + } + + if (value > totalHits.value) { + throw new Error( + i18n.translate('xpack.maps.totalHits.lowerBoundPrecisionExceeded', { + defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower then value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, + values: { + totalHitsString: JSON.stringify(totalHits, null, ''), + value, + }, + }) + ); + } + + return true; +} diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index d795315acbf50..6dd454137be7d 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -22,6 +22,7 @@ import { LAYER_STYLE_TYPE, FIELD_ORIGIN, } from '../../../../common/constants'; +import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { IESSource } from '../../sources/es_source'; @@ -323,13 +324,18 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { syncContext.startLoading(dataRequestId, requestToken, searchFilters); const abortController = new AbortController(); syncContext.registerCancelCallback(requestToken, () => abortController.abort()); + const maxResultWindow = await this._documentSource.getMaxResultWindow(); const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', maxResultWindow + 1); const resp = await searchSource.fetch({ abortSignal: abortController.signal, sessionId: syncContext.dataFilters.searchSessionId, + legacyHitsTotal: false, }); - const maxResultWindow = await this._documentSource.getMaxResultWindow(); - isSyncClustered = resp.hits.total > maxResultWindow; + isSyncClustered = isTotalHitsGreaterThan( + (resp.hits.total as unknown) as TotalHits, + maxResultWindow + ); const countData = { isSyncClustered } as CountData; syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); } catch (error) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 4715398dab97b..7910e931e60e6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -368,6 +368,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle ): Promise { const indexPattern: IndexPattern = await this.getIndexPattern(); const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); let bucketsPerGrid = 1; this.getMetricFields().forEach((metricField) => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index c652935d7188a..9a1f23e055af1 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -190,6 +190,7 @@ export class ESGeoLineSource extends AbstractESAggSource { // Fetch entities // const entitySearchSource = await this.makeSearchSource(searchFilters, 0); + entitySearchSource.setField('trackTotalHits', false); const splitField = getField(indexPattern, this._descriptor.splitField); const cardinalityAgg = { precision_threshold: 1 }; const termsAgg = { size: MAX_TRACKS }; @@ -250,6 +251,7 @@ export class ESGeoLineSource extends AbstractESAggSource { const tracksSearchFilters = { ...searchFilters }; delete tracksSearchFilters.buffer; const tracksSearchSource = await this.makeSearchSource(tracksSearchFilters, 0); + tracksSearchSource.setField('trackTotalHits', false); tracksSearchSource.setField('aggs', { tracks: { filters: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index e3ee9599d86a9..781cc7f8c36b0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -109,6 +109,7 @@ export class ESPewPewSource extends AbstractESAggSource { async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { destSplit: { terms: { @@ -168,6 +169,7 @@ export class ESPewPewSource extends AbstractESAggSource { async getBoundsForFilters(boundsFilters, registerCancelCallback) { const searchSource = await this.makeSearchSource(boundsFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { destFitToBounds: { geo_bounds: { @@ -185,7 +187,10 @@ export class ESPewPewSource extends AbstractESAggSource { try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); + const esResp = await searchSource.fetch({ + abortSignal: abortController.signal, + legacyHitsTotal: false, + }); if (esResp.aggregations.destFitToBounds.bounds) { corners.push([ esResp.aggregations.destFitToBounds.bounds.top_left.lon, diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 3b6a7202691b6..eae00710c4c25 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -18,6 +18,7 @@ import { addFieldToDSL, getField, hitsToGeoJson, + isTotalHitsGreaterThan, PreIndexedShape, } from '../../../../common/elasticsearch_util'; // @ts-expect-error @@ -313,6 +314,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }; const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { totalEntities: { cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), @@ -343,11 +345,10 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye const areEntitiesTrimmed = entityBuckets.length >= DEFAULT_MAX_BUCKETS_LIMIT; let areTopHitsTrimmed = false; entityBuckets.forEach((entityBucket: any) => { - const total = _.get(entityBucket, 'entityHits.hits.total', 0); const hits = _.get(entityBucket, 'entityHits.hits.hits', []); // Reverse hits list so top documents by sort are drawn on top allHits.push(...hits.reverse()); - if (total > hits.length) { + if (isTotalHitsGreaterThan(entityBucket.entityHits.hits.total, hits.length)) { areTopHitsTrimmed = true; } }); @@ -385,6 +386,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye maxResultWindow, initialSearchContext ); + searchSource.setField('trackTotalHits', maxResultWindow + 1); searchSource.setField('fieldsFromSource', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields if (sourceOnlyFields.length === 0) { searchSource.setField('source', false); // do not need anything from _source @@ -408,7 +410,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top meta: { resultsCount: resp.hits.hits.length, - areResultsTrimmed: resp.hits.total > resp.hits.hits.length, + areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length), }, }; } @@ -508,6 +510,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source const searchService = getSearchService(); const searchSource = await searchService.searchSource.create(initialSearchContext as object); + searchSource.setField('trackTotalHits', false); searchSource.setField('index', indexPattern); searchSource.setField('size', 1); @@ -520,7 +523,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSource.setField('query', query); searchSource.setField('fieldsFromSource', this._getTooltipPropertyNames()); - const resp = await searchSource.fetch(); + const resp = await searchSource.fetch({ legacyHitsTotal: false }); const hit = _.get(resp, 'hits.hits[0]'); if (!hit) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index c55a564951c4e..222c49abfa16a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -195,6 +195,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource resp = await searchSource.fetch({ abortSignal: abortController.signal, sessionId: searchSessionId, + legacyHitsTotal: false, }); if (inspectorRequest) { const responseStats = search.getResponseInspectorStats(resp, searchSource); @@ -247,6 +248,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource } } const searchService = getSearchService(); + const searchSource = await searchService.searchSource.create(initialSearchContext); searchSource.setField('index', indexPattern); @@ -272,6 +274,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource registerCancelCallback: (callback: () => void) => void ): Promise { const searchSource = await this.makeSearchSource(boundsFilters, 0); + searchSource.setField('trackTotalHits', false); searchSource.setField('aggs', { fitToBounds: { geo_bounds: { @@ -284,7 +287,10 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource try { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); + const esResp = await searchSource.fetch({ + abortSignal: abortController.signal, + legacyHitsTotal: false, + }); if (!esResp.aggregations) { return null; diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 5c41971fb629c..caae4385aeec6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -127,6 +127,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource const indexPattern = await this.getIndexPattern(); const searchSource: ISearchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', false); const termsField = getField(indexPattern, this._termField.getName()); const termsAgg = { size: this._descriptor.size !== undefined ? this._descriptor.size : DEFAULT_MAX_BUCKETS_LIMIT, diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index d6ebf2fb216b2..95b8e043e0ce4 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -23,7 +23,12 @@ import { SUPER_FINE_ZOOM_DELTA, } from '../../common/constants'; -import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; +import { + convertRegularRespToGeoJson, + hitsToGeoJson, + isTotalHitsGreaterThan, + TotalHits, +} from '../../common/elasticsearch_util'; import { flattenHit } from './util'; import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; @@ -67,6 +72,7 @@ export async function getGridTile({ MAX_ZOOM ); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; + requestBody.track_total_hits = false; const response = await context .search!.search( @@ -78,6 +84,7 @@ export async function getGridTile({ }, { sessionId: searchSessionId, + legacyHitsTotal: false, abortSignal, } ) @@ -130,6 +137,7 @@ export async function getTile({ const searchOptions = { sessionId: searchSessionId, + legacyHitsTotal: false, abortSignal, }; @@ -141,6 +149,7 @@ export async function getTile({ body: { size: 0, query: requestBody.query, + track_total_hits: requestBody.size + 1, }, }, }, @@ -148,7 +157,12 @@ export async function getTile({ ) .toPromise(); - if (countResponse.rawResponse.hits.total > requestBody.size) { + if ( + isTotalHitsGreaterThan( + (countResponse.rawResponse.hits.total as unknown) as TotalHits, + requestBody.size + ) + ) { // Generate "too many features"-bounds const bboxResponse = await context .search!.search( @@ -165,6 +179,7 @@ export async function getTile({ }, }, }, + track_total_hits: false, }, }, }, @@ -191,7 +206,10 @@ export async function getTile({ { params: { index, - body: requestBody, + body: { + ...requestBody, + track_total_hits: false, + }, }, }, searchOptions diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js index e12ffefb71080..6d4fca1b0b7c0 100644 --- a/x-pack/test/functional/apps/maps/blended_vector_layer.js +++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js @@ -27,28 +27,21 @@ export default function ({ getPageObjects, getService }) { }); it('should request documents when zoomed to smaller regions showing less data', async () => { - const hits = await PageObjects.maps.getHits(); + const response = await PageObjects.maps.getResponse(); // Allow a range of hits to account for variances in browser window size. - expect(parseInt(hits)).to.be.within(30, 40); + expect(response.hits.hits.length).to.be.within(30, 40); }); it('should request clusters when zoomed to larger regions showing lots of data', async () => { await PageObjects.maps.setView(20, -90, 2); - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - - expect(hits).to.equal('0'); - expect(totalHits).to.equal('14000'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(17); }); it('should request documents when query narrows data', async () => { await PageObjects.maps.setAndSubmitQuery('bytes > 19000'); - const hits = await PageObjects.maps.getHits(); - expect(hits).to.equal('75'); + const response = await PageObjects.maps.getResponse(); + expect(response.hits.hits.length).to.equal(75); }); }); } diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index a49ab7d7dd980..1d6477b243cdf 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -9,9 +9,6 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); - const inspector = getService('inspector'); - const monacoEditor = getService('monacoEditor'); - const testSubjects = getService('testSubjects'); const security = getService('security'); describe('docvalue_fields', () => { @@ -24,18 +21,9 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - async function getResponse() { - await inspector.open(); - await inspector.openInspectorRequestsView(); - await testSubjects.click('inspectorRequestDetailResponse'); - const responseBody = await monacoEditor.getCodeEditorValue(); - await inspector.close(); - return JSON.parse(responseBody); - } - it('should only fetch geo_point field and nothing else when source does not have data driven styling', async () => { await PageObjects.maps.loadSavedMap('document example'); - const response = await getResponse(); + const response = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); expect(firstHit.fields).to.only.have.keys(['geo.coordinates']); @@ -43,7 +31,7 @@ export default function ({ getPageObjects, getService }) { it('should only fetch geo_point field and data driven styling fields', async () => { await PageObjects.maps.loadSavedMap('document example with data driven styles'); - const response = await getResponse(); + const response = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); expect(firstHit.fields).to.only.have.keys(['bytes', 'geo.coordinates', 'hour_of_day']); @@ -51,7 +39,7 @@ export default function ({ getPageObjects, getService }) { it('should format date fields as epoch_millis when data driven styling is applied to a date field', async () => { await PageObjects.maps.loadSavedMap('document example with data driven styles on date field'); - const response = await getResponse(); + const response = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); expect(firstHit.fields).to.only.have.keys(['@timestamp', 'bytes', 'geo.coordinates']); diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 0f331e7763a76..89c1cbded9a26 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -14,7 +14,6 @@ export default function ({ getPageObjects, getService }) { const filterBar = getService('filterBar'); const dashboardPanelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); const security = getService('security'); @@ -81,11 +80,10 @@ export default function ({ getPageObjects, getService }) { }); it('should apply container state (time, query, filters) to embeddable when loaded', async () => { - await dashboardPanelActions.openInspectorByTitle('geo grid vector grid example'); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - expect(totalHits).to.equal('6'); + const response = await PageObjects.maps.getResponseFromDashboardPanel( + 'geo grid vector grid example' + ); + expect(response.aggregations.gridSplit.buckets.length).to.equal(6); }); it('should apply new container state (time, query, filters) to embeddable', async () => { @@ -94,25 +92,16 @@ export default function ({ getPageObjects, getService }) { await filterBar.selectIndexPattern('meta_for_geo_shapes*'); await filterBar.addFilter('shape_name', 'is', 'alpha'); - await dashboardPanelActions.openInspectorByTitle('geo grid vector grid example'); - const geoGridRequestStats = await inspector.getTableData(); - const geoGridTotalHits = PageObjects.maps.getInspectorStatRowHit( - geoGridRequestStats, - 'Hits (total)' + const gridResponse = await PageObjects.maps.getResponseFromDashboardPanel( + 'geo grid vector grid example' ); - await inspector.close(); - expect(geoGridTotalHits).to.equal('1'); + expect(gridResponse.aggregations.gridSplit.buckets.length).to.equal(1); - await dashboardPanelActions.openInspectorByTitle('join example'); - await testSubjects.click('inspectorRequestChooser'); - await testSubjects.click('inspectorRequestChoosermeta_for_geo_shapes*.shape_name'); - const joinRequestStats = await inspector.getTableData(); - const joinTotalHits = PageObjects.maps.getInspectorStatRowHit( - joinRequestStats, - 'Hits (total)' + const joinResponse = await PageObjects.maps.getResponseFromDashboardPanel( + 'join example', + 'meta_for_geo_shapes*.shape_name' ); - await inspector.close(); - expect(joinTotalHits).to.equal('3'); + expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); it('should re-fetch query when "refresh" is clicked', async () => { diff --git a/x-pack/test/functional/apps/maps/es_geo_grid_source.js b/x-pack/test/functional/apps/maps/es_geo_grid_source.js index 5a9e62b94f2a2..6dee4b87bceea 100644 --- a/x-pack/test/functional/apps/maps/es_geo_grid_source.js +++ b/x-pack/test/functional/apps/maps/es_geo_grid_source.js @@ -141,12 +141,8 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to geotile_grid aggregation request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - expect(hits).to.equal('1'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(1); }); }); @@ -156,18 +152,8 @@ export default function ({ getPageObjects, getService }) { }); it('should contain geotile_grid aggregation elasticsearch request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('6'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('logstash-*'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(4); }); it('should not contain any elasticsearch request after layer is deleted', async () => { @@ -218,12 +204,8 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to geotile_grid aggregation request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - expect(hits).to.equal('1'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(1); }); }); @@ -233,18 +215,8 @@ export default function ({ getPageObjects, getService }) { }); it('should contain geotile_grid aggregation elasticsearch request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('6'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('logstash-*'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(4); }); it('should not contain any elasticsearch request after layer is deleted', async () => { @@ -272,18 +244,8 @@ export default function ({ getPageObjects, getService }) { }); it('should contain geotile_grid aggregation elasticsearch request', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('4'); //4 geometries result in 13 cells due to way they overlap geotile_grid cells - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('geo_shapes*'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.gridSplit.buckets.length).to.equal(13); }); }); }); diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/es_pew_pew_source.js index 41dadff1b6f93..66406cd6d8f91 100644 --- a/x-pack/test/functional/apps/maps/es_pew_pew_source.js +++ b/x-pack/test/functional/apps/maps/es_pew_pew_source.js @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); - const inspector = getService('inspector'); const security = getService('security'); const VECTOR_SOURCE_ID = '67c1de2c-2fc5-4425-8983-094b589afe61'; @@ -25,15 +24,8 @@ export default function ({ getPageObjects, getService }) { }); it('should request source clusters for destination locations', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - await inspector.close(); - - expect(hits).to.equal('0'); - expect(totalHits).to.equal('4'); + const response = await PageObjects.maps.getResponse(); + expect(response.aggregations.destSplit.buckets.length).to.equal(2); }); it('should render lines', async () => { diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 49717016f9c60..8b40651ea5674 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -121,17 +121,8 @@ export default function ({ getPageObjects, getService }) { }); it('should not apply query to source and apply query to join', async () => { - await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name'); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('3'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - const indexPatternName = PageObjects.maps.getInspectorStatRowHit( - requestStats, - 'Index pattern' - ); - expect(indexPatternName).to.equal('meta_for_geo_shapes*'); + const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + expect(joinResponse.aggregations.join.buckets.length).to.equal(2); }); }); @@ -145,13 +136,8 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to join request', async () => { - await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name'); - const requestStats = await inspector.getTableData(); - const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits (total)'); - expect(totalHits).to.equal('2'); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - await inspector.close(); + const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); it('should update dynamic data range in legend with new results', async () => { diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 3f6b5691314bb..3d9572dcac24e 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -23,6 +23,8 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const browser = getService('browser'); const MenuToggle = getService('MenuToggle'); const listingTable = getService('listingTable'); + const monacoEditor = getService('monacoEditor'); + const dashboardPanelActions = getService('dashboardPanelActions'); const setViewPopoverToggle = new MenuToggle({ name: 'SetView Popover', @@ -614,6 +616,31 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte return mapboxStyle; } + async getResponse(requestName: string) { + await inspector.open(); + const response = await this._getResponse(requestName); + await inspector.close(); + return response; + } + + async _getResponse(requestName: string) { + if (requestName) { + await testSubjects.click('inspectorRequestChooser'); + await testSubjects.click(`inspectorRequestChooser${requestName}`); + } + await inspector.openInspectorRequestsView(); + await testSubjects.click('inspectorRequestDetailResponse'); + const responseBody = await monacoEditor.getCodeEditorValue(); + return JSON.parse(responseBody); + } + + async getResponseFromDashboardPanel(panelTitle: string, requestName: string) { + await dashboardPanelActions.openInspectorByTitle(panelTitle); + const response = await this._getResponse(requestName); + await inspector.close(); + return response; + } + getInspectorStatRowHit(stats: string[][], rowName: string) { const STATS_ROW_NAME_INDEX = 0; const STATS_ROW_VALUE_INDEX = 1; From d70d02ee83b18128f07d3d6fd92c50b539a25571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Thu, 25 Mar 2021 15:05:23 -0400 Subject: [PATCH 09/98] Add a11y test coverage to Rule Creation Flow for Detections tab (#94377) [Security Solution] Add a11y test coverage to Detections rule creation flow (#80060) --- test/functional/page_objects/common_page.ts | 15 ++ test/functional/services/common/find.ts | 9 +- .../services/common/test_subjects.ts | 13 +- .../web_element_wrapper.ts | 8 +- .../accessibility/apps/security_solution.ts | 142 +++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + x-pack/test/functional/config.js | 3 + x-pack/test/functional/page_objects/index.ts | 2 + .../page_objects/detections/index.ts | 149 ++++++++++++++++++ 9 files changed, 330 insertions(+), 12 deletions(-) create mode 100644 x-pack/test/accessibility/apps/security_solution.ts create mode 100644 x-pack/test/security_solution_ftr/page_objects/detections/index.ts diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index c6412f55dffbf..6d9641a1a920e 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -463,6 +463,21 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async getWelcomeText() { return await testSubjects.getVisibleText('global-banner-item'); } + + /** + * Clicks on an element, and validates that the desired effect has taken place + * by confirming the existence of a validator + */ + async clickAndValidate( + clickTarget: string, + validator: string, + isValidatorCssString: boolean = false, + topOffset?: number + ) { + await testSubjects.click(clickTarget, undefined, topOffset); + const validate = isValidatorCssString ? find.byCssSelector : testSubjects.exists; + await validate(validator); + } } return new CommonPage(); diff --git a/test/functional/services/common/find.ts b/test/functional/services/common/find.ts index 2a86efad1ea9d..0cd4c14683f6e 100644 --- a/test/functional/services/common/find.ts +++ b/test/functional/services/common/find.ts @@ -79,11 +79,11 @@ export async function FindProvider({ getService }: FtrProviderContext) { return wrap(await driver.switchTo().activeElement()); } - public async setValue(selector: string, text: string): Promise { + public async setValue(selector: string, text: string, topOffset?: number): Promise { log.debug(`Find.setValue('${selector}', '${text}')`); return await retry.try(async () => { const element = await this.byCssSelector(selector); - await element.click(); + await element.click(topOffset); // in case the input element is actually a child of the testSubject, we // call clearValue() and type() on the element that is focused after @@ -413,14 +413,15 @@ export async function FindProvider({ getService }: FtrProviderContext) { public async clickByCssSelector( selector: string, - timeout: number = defaultFindTimeout + timeout: number = defaultFindTimeout, + topOffset?: number ): Promise { log.debug(`Find.clickByCssSelector('${selector}') with timeout=${timeout}`); await retry.try(async () => { const element = await this.byCssSelector(selector, timeout); if (element) { // await element.moveMouseTo(); - await element.click(); + await element.click(topOffset); } else { throw new Error(`Element with css='${selector}' is not found`); } diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index 28b37d9576e8c..111206ec9eafe 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -100,9 +100,13 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { await find.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { timeout }); } - public async click(selector: string, timeout: number = FIND_TIME): Promise { + public async click( + selector: string, + timeout: number = FIND_TIME, + topOffset?: number + ): Promise { log.debug(`TestSubjects.click(${selector})`); - await find.clickByCssSelector(testSubjSelector(selector), timeout); + await find.clickByCssSelector(testSubjSelector(selector), timeout, topOffset); } public async doubleClick(selector: string, timeout: number = FIND_TIME): Promise { @@ -187,12 +191,13 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { public async setValue( selector: string, text: string, - options: SetValueOptions = {} + options: SetValueOptions = {}, + topOffset?: number ): Promise { return await retry.try(async () => { const { clearWithKeyboard = false, typeCharByChar = false } = options; log.debug(`TestSubjects.setValue(${selector}, ${text})`); - await this.click(selector); + await this.click(selector, undefined, topOffset); // in case the input element is actually a child of the testSubject, we // call clearValue() and type() on the element that is focused after // clicking on the testSubject diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 1a45aee877e1f..b1561b29342da 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -182,9 +182,9 @@ export class WebElementWrapper { * * @return {Promise} */ - public async click() { + public async click(topOffset?: number) { await this.retryCall(async function click(wrapper) { - await wrapper.scrollIntoViewIfNecessary(); + await wrapper.scrollIntoViewIfNecessary(topOffset); await wrapper._webElement.click(); }); } @@ -693,11 +693,11 @@ export class WebElementWrapper { * @nonstandard * @return {Promise} */ - public async scrollIntoViewIfNecessary(): Promise { + public async scrollIntoViewIfNecessary(topOffset?: number): Promise { await this.driver.executeScript( scrollIntoViewIfNecessary, this._webElement, - this.fixedHeaderHeight + topOffset || this.fixedHeaderHeight ); } diff --git a/x-pack/test/accessibility/apps/security_solution.ts b/x-pack/test/accessibility/apps/security_solution.ts new file mode 100644 index 0000000000000..0ee4e88d712c8 --- /dev/null +++ b/x-pack/test/accessibility/apps/security_solution.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const a11y = getService('a11y'); + const { common, detections } = getPageObjects(['common', 'detections']); + const security = getService('security'); + const toasts = getService('toasts'); + const testSubjects = getService('testSubjects'); + + describe('Security Solution', () => { + before(async () => { + await security.testUser.setRoles(['superuser'], false); + await common.navigateToApp('security'); + }); + + after(async () => { + await security.testUser.restoreDefaults(false); + }); + + describe('Detections', () => { + describe('Create Rule Flow', () => { + beforeEach(async () => { + await detections.navigateToCreateRule(); + }); + + describe('Custom Query Rule', () => { + describe('Define Step', () => { + it('default view meets a11y requirements', async () => { + await toasts.dismissAllToasts(); + await testSubjects.click('customRuleType'); + await a11y.testAppSnapshot(); + }); + + describe('import query modal', () => { + it('contents of the default tab meets a11y requirements', async () => { + await detections.openImportQueryModal(); + await a11y.testAppSnapshot(); + }); + + it('contents of the templates tab meets a11y requirements', async () => { + await common.scrollKibanaBodyTop(); + await detections.openImportQueryModal(); + await detections.viewTemplatesInImportQueryModal(); + await a11y.testAppSnapshot(); + }); + }); + + it('preview section meets a11y requirements', async () => { + await detections.addCustomQuery('_id'); + await detections.preview(); + await a11y.testAppSnapshot(); + }); + + describe('About Step', () => { + beforeEach(async () => { + await detections.addCustomQuery('_id'); + await detections.continue('define'); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + + it('advanced settings view meets a11y requirements', async () => { + await detections.revealAdvancedSettings(); + await a11y.testAppSnapshot(); + }); + + describe('Schedule Step', () => { + beforeEach(async () => { + await detections.addNameAndDescription(); + await detections.continue('about'); + }); + + it('meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + + describe('Actions Step', () => { + it('meets a11y requirements', async () => { + await detections.continue('schedule'); + await a11y.testAppSnapshot(); + }); + }); + }); + }); + }); + }); + + describe('Machine Learning Rule First Step', () => { + it('default view meets a11y requirements', async () => { + await detections.selectMLRule(); + await a11y.testAppSnapshot(); + }); + }); + + describe('Threshold Rule Rule First Step', () => { + beforeEach(async () => { + await detections.selectThresholdRule(); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + + it('preview section meets a11y requirements', async () => { + await detections.addCustomQuery('_id'); + await detections.preview(); + await a11y.testAppSnapshot(); + }); + }); + + describe('Event Correlation Rule First Step', () => { + beforeEach(async () => { + await detections.selectEQLRule(); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + }); + + describe('Indicator Match Rule First Step', () => { + beforeEach(async () => { + await detections.selectIndicatorMatchRule(); + }); + + it('default view meets a11y requirements', async () => { + await a11y.testAppSnapshot(); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index c6d85c8755a6b..2a8840c364927 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -34,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/lens'), require.resolve('./apps/upgrade_assistant'), require.resolve('./apps/canvas'), + require.resolve('./apps/security_solution'), ], pageObjects, diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 9b1df72aa78c8..c0323d96026ef 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -197,6 +197,9 @@ export default async function ({ readConfigFile }) { reporting: { pathname: '/app/management/insightsAndAlerting/reporting', }, + securitySolution: { + pathname: '/app/security', + }, }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 804f49e5ea075..cf92191075fba 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -40,6 +40,7 @@ import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; import { TagManagementPageProvider } from './tag_management_page'; import { NavigationalSearchProvider } from './navigational_search'; import { SearchSessionsPageProvider } from './search_sessions_management_page'; +import { DetectionsPageProvider } from '../../security_solution_ftr/page_objects/detections'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -77,4 +78,5 @@ export const pageObjects = { roleMappings: RoleMappingsPageProvider, ingestPipelines: IngestPipelinesPageProvider, navigationalSearch: NavigationalSearchProvider, + detections: DetectionsPageProvider, }; diff --git a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts new file mode 100644 index 0000000000000..dd17548df6e3f --- /dev/null +++ b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +export function DetectionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const { common } = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + class DetectionsPage { + async navigateHome(): Promise { + await this.navigateToDetectionsPage(); + } + + async navigateToRules(): Promise { + await this.navigateToDetectionsPage('rules'); + } + + async navigateToRuleMonitoring(): Promise { + await common.clickAndValidate('allRulesTableTab-monitoring', 'monitoring-table'); + } + + async navigateToExceptionList(): Promise { + await common.clickAndValidate('allRulesTableTab-exceptions', 'exceptions-table'); + } + + async navigateToCreateRule(): Promise { + await this.navigateToDetectionsPage('rules/create'); + } + + async replaceIndexPattern(): Promise { + const buttons = await find.allByCssSelector('[data-test-subj="comboBoxInput"] button'); + await buttons.map(async (button: WebElementWrapper) => await button.click()); + await testSubjects.setValue('comboBoxSearchInput', '*'); + } + + async openImportQueryModal(): Promise { + const element = await testSubjects.find('importQueryFromSavedTimeline'); + await element.click(500); + await testSubjects.exists('open-timeline-modal-body-filter-default'); + } + + async viewTemplatesInImportQueryModal(): Promise { + await common.clickAndValidate('open-timeline-modal-body-filter-template', 'timelines-table'); + } + + async closeImportQueryModal(): Promise { + await find.clickByCssSelector('.euiButtonIcon.euiButtonIcon--text.euiModal__closeIcon'); + } + + async selectMachineLearningJob(): Promise { + await find.clickByCssSelector('[data-test-subj="mlJobSelect"] button'); + await find.clickByCssSelector('#high_distinct_count_error_message'); + } + + async openAddFilterPopover(): Promise { + const addButtons = await testSubjects.findAll('addFilter'); + await addButtons[1].click(); + await testSubjects.exists('saveFilter'); + } + + async closeAddFilterPopover(): Promise { + await testSubjects.click('cancelSaveFilter'); + } + + async toggleFilterActions(): Promise { + const filterActions = await testSubjects.findAll('addFilter'); + await filterActions[1].click(); + } + + async toggleSavedQueries(): Promise { + const filterActions = await find.allByCssSelector( + '[data-test-subj="saved-query-management-popover-button"]' + ); + await filterActions[1].click(); + } + + async addNameAndDescription( + name: string = 'test rule name', + description: string = 'test rule description' + ): Promise { + await find.setValue(`[aria-describedby="detectionEngineStepAboutRuleName"]`, name, 500); + await find.setValue( + `[aria-describedby="detectionEngineStepAboutRuleDescription"]`, + description, + 500 + ); + } + + async goBackToAllRules(): Promise { + await common.clickAndValidate('ruleDetailsBackToAllRules', 'create-new-rule'); + } + + async revealAdvancedSettings(): Promise { + await common.clickAndValidate( + 'advancedSettings', + 'detectionEngineStepAboutRuleReferenceUrls' + ); + } + + async preview(): Promise { + await common.clickAndValidate( + 'queryPreviewButton', + 'queryPreviewCustomHistogram', + undefined, + 500 + ); + } + + async continue(prefix: string): Promise { + await testSubjects.click(`${prefix}-continue`); + } + + async addCustomQuery(query: string): Promise { + await testSubjects.setValue('queryInput', query, undefined, 500); + } + + async selectMLRule(): Promise { + await common.clickAndValidate('machineLearningRuleType', 'mlJobSelect'); + } + + async selectEQLRule(): Promise { + await common.clickAndValidate('eqlRuleType', 'eqlQueryBarTextInput'); + } + + async selectIndicatorMatchRule(): Promise { + await common.clickAndValidate('threatMatchRuleType', 'comboBoxInput'); + } + + async selectThresholdRule(): Promise { + await common.clickAndValidate('thresholdRuleType', 'input'); + } + + private async navigateToDetectionsPage(path: string = ''): Promise { + const subUrl = `detections${path ? `/${path}` : ''}`; + await common.navigateToUrl('securitySolution', subUrl, { + shouldUseHashForSubUrl: false, + }); + } + } + + return new DetectionsPage(); +} From 6eb31178ed172fbf452aeb78f60e681e74f386ef Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 25 Mar 2021 20:20:01 +0100 Subject: [PATCH 10/98] blank_issues_enabled: false. it seems contact_links not supported otherwise (#95423) --- .github/ISSUE_TEMPLATE/Question.md | 15 --------------- .github/ISSUE_TEMPLATE/config.yml | 4 ++++ 2 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/Question.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md deleted file mode 100644 index 38fcb7af30b47..0000000000000 --- a/.github/ISSUE_TEMPLATE/Question.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Question -about: Who, what, when, where, and how? - ---- - -Hey, stop right there! - -We use GitHub to track feature requests and bug reports. Please do not submit issues for questions about how to use features of Kibana, how to set Kibana up, best practices, or development related help. - -However, we do want to help! Head on over to our official Kibana forums and ask your questions there. In additional to awesome, knowledgeable community contributors, core Kibana developers are on the forums every single day to help you out. - -The forums are here: https://discuss.elastic.co/c/kibana - -We can't stop you from opening an issue here, but it will likely linger without a response for days or weeks before it is closed and we ask you to join us on the forums instead. Save yourself the time, and ask on the forums today. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..e8050c846b254 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +blank_issues_enabled: false +contact_links: + - name: Question + url: https://discuss.elastic.co/c/kibana From a10c4188b76044cbcc1c7480e97f8a6c62cc2c05 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 25 Mar 2021 12:24:36 -0700 Subject: [PATCH 11/98] Re-enable skipped test (discover with async scripted fields) (#94653) * Re-enable skipped test * remove comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test/functional/apps/discover/async_scripted_fields.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 9696eb0460142..7364f2883bd1a 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -19,8 +19,7 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const security = getService('security'); - // Failing: See https://github.com/elastic/kibana/issues/78553 - describe.skip('async search with scripted fields', function () { + describe('async search with scripted fields', function () { this.tags(['skipFirefox']); before(async function () { @@ -42,7 +41,7 @@ export default function ({ getService, getPageObjects }) { await security.testUser.restoreDefaults(); }); - it.skip('query should show failed shards pop up', async function () { + it('query should show failed shards pop up', async function () { if (false) { /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' */ @@ -74,7 +73,7 @@ export default function ({ getService, getPageObjects }) { }); }); - it.skip('query return results with valid scripted field', async function () { + it('query return results with valid scripted field', async function () { if (false) { /* the commented-out steps below were used to create the scripted fields in the logstash-* index pattern which are now saved in the esArchive. From 3bb9220db90a5976388b21e23ce7f3b6327e22e6 Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 25 Mar 2021 20:29:21 +0100 Subject: [PATCH 12/98] add about section for link in github issue template --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e8050c846b254..aa68db29974a8 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,3 +2,4 @@ blank_issues_enabled: false contact_links: - name: Question url: https://discuss.elastic.co/c/kibana + about: Please ask and answer questions here. From dd10c8b5f2197c936a2efbc45feed7fbb88ebe08 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 25 Mar 2021 12:57:32 -0700 Subject: [PATCH 13/98] [kbn/test] switch to @elastic/elasticsearch (#95443) Co-authored-by: spalger --- packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index 43b6c90452b81..d472f27395ffb 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -12,9 +12,9 @@ import { get, toPath } from 'lodash'; import { Cluster } from '@kbn/es'; import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; import { esTestConfig } from './es_test_config'; +import { Client } from '@elastic/elasticsearch'; import { KIBANA_ROOT } from '../'; -import * as legacyElasticsearch from 'elasticsearch'; const path = require('path'); const del = require('del'); @@ -102,8 +102,8 @@ export function createLegacyEsTestCluster(options = {}) { * Returns an ES Client to the configured cluster */ getClient() { - return new legacyElasticsearch.Client({ - host: this.getUrl(), + return new Client({ + node: this.getUrl(), }); } From 306a42c03c00aa640ad2ceb89d085ad0ce85af67 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 25 Mar 2021 14:59:14 -0500 Subject: [PATCH 14/98] Index pattern management - use new es client instead of legacy (#95293) * use new es client instead of legacy * use resolve api on client --- .../server/routes/resolve_index.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/plugins/index_pattern_management/server/routes/resolve_index.ts b/src/plugins/index_pattern_management/server/routes/resolve_index.ts index 851a2578231aa..22c214f2adee2 100644 --- a/src/plugins/index_pattern_management/server/routes/resolve_index.ts +++ b/src/plugins/index_pattern_management/server/routes/resolve_index.ts @@ -31,19 +31,11 @@ export function registerResolveIndexRoute(router: IRouter): void { }, }, async (context, req, res) => { - const queryString = req.query.expand_wildcards - ? { expand_wildcards: req.query.expand_wildcards } - : null; - const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ - queryString ? '?' + new URLSearchParams(queryString).toString() : '' - }`, - } - ); - return res.ok({ body: result }); + const { body } = await context.core.elasticsearch.client.asCurrentUser.indices.resolveIndex({ + name: req.params.query, + expand_wildcards: req.query.expand_wildcards || 'open', + }); + return res.ok({ body }); } ); } From 724e21e0070b9679c7bdf9fa4da08f8bfd05a18e Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 25 Mar 2021 15:11:40 -0500 Subject: [PATCH 15/98] skip flaky test. #95345 --- .../management/users/edit_user/create_user_page.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx index 2c40fec2ec31d..4c0bb6a67f2e4 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx @@ -53,7 +53,8 @@ describe('CreateUserPage', () => { }); }); - it('validates form', async () => { + // flaky https://github.com/elastic/kibana/issues/95345 + it.skip('validates form', async () => { const coreStart = coreMock.createStart(); const history = createMemoryHistory({ initialEntries: ['/create'] }); const authc = securityMock.createSetup().authc; From bd3f5d4863fccac61f093e03c7900c303a4a71c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 25 Mar 2021 21:19:48 +0100 Subject: [PATCH 16/98] Update APM readme (#95383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update APM readme * Update readme.md * Update x-pack/plugins/apm/readme.md Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> --- x-pack/plugins/apm/readme.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index b35024844a892..b125407a160aa 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -31,19 +31,19 @@ _Docker Compose is required_ ## Testing -### E2E (Cypress) tests +### Cypress tests ```sh -x-pack/plugins/apm/e2e/run-e2e.sh +node x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js ``` _Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sample data into Elasticsearch via APM Server and runs the Cypress tests_ -### Unit testing +### Jest tests Note: Run the following commands from `kibana/x-pack/plugins/apm`. -#### Run unit tests +#### Run ``` npx jest --watch @@ -82,8 +82,11 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### API integration tests -Our tests are separated in two suites: one suite runs with a basic license, and the other -with a trial license (the equivalent of gold+). This requires separate test servers and test runners. +API tests are separated in two suites: + - a basic license test suite + - a trial license test suite (the equivalent of gold+) + +This requires separate test servers and test runners. **Basic** @@ -109,7 +112,10 @@ node scripts/functional_test_runner --config x-pack/test/apm_api_integration/tri The API tests for "trial" are located in `x-pack/test/apm_api_integration/trial/tests`. -For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + +**API Test tips** + - For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + - To update snapshots append `--updateSnapshots` to the functional_test_runner command ## Linting From e01f317d9cd94461e1c11c17bde0cf3b70047012 Mon Sep 17 00:00:00 2001 From: Greg Back <1045796+gtback@users.noreply.github.com> Date: Thu, 25 Mar 2021 17:57:25 -0400 Subject: [PATCH 17/98] Add Vega help link to DocLinksService (#87721) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/core/public/doc_links/doc_links_service.ts | 1 + .../vis_type_vega/public/components/vega_help_menu.tsx | 4 +++- src/plugins/vis_type_vega/public/plugin.ts | 2 ++ src/plugins/vis_type_vega/public/services.ts | 4 +++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 9711d546fc947..0bb5ddd29609e 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -191,6 +191,7 @@ export class DocLinksService { lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, maps: `${ELASTIC_WEBSITE_URL}maps`, + vega: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/vega.html`, }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, diff --git a/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx index efb41c470024b..f5b0f614458fd 100644 --- a/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx @@ -11,6 +11,8 @@ import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } fr import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { getDocLinks } from '../services'; + function VegaHelpMenu() { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); @@ -30,7 +32,7 @@ function VegaHelpMenu() { const items = [ diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 0204c2c90b71b..f935362d21604 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -19,6 +19,7 @@ import { setUISettings, setInjectedMetadata, setMapServiceSettings, + setDocLinks, } from './services'; import { createVegaFn } from './vega_fn'; @@ -96,5 +97,6 @@ export class VegaPlugin implements Plugin { setNotifications(core.notifications); setData(data); setInjectedMetadata(core.injectedMetadata); + setDocLinks(core.docLinks); } } diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index c47378282932b..f67fe4794e783 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/public'; +import { CoreStart, NotificationsStart, IUiSettingsClient, DocLinksStart } from 'src/core/public'; import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../kibana_utils/public'; @@ -35,3 +35,5 @@ export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ }>('InjectedVars'); export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; + +export const [getDocLinks, setDocLinks] = createGetterSetter('docLinks'); From ba029aa95eee81d0c6a102e5d5d16f7a10fede66 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Mar 2021 16:22:02 -0600 Subject: [PATCH 18/98] [Maps] split out DrawFilterControl and DrawControl (#95255) * [Maps] split out DrawFilterControl and DrawControl * clean up * update i18n id * give 'global_index_pattern_management_all' permission to functional test because new check blocks access without it * revert last change Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../mb_map/draw_control/draw_control.tsx | 114 ++-------------- .../draw_filter_control.tsx | 126 ++++++++++++++++++ .../draw_control/draw_filter_control/index.ts | 32 +++++ .../mb_map/draw_control/draw_tooltip.tsx | 9 +- .../mb_map/draw_control/index.ts | 26 +--- .../connected_components/mb_map/mb_map.tsx | 4 +- 6 files changed, 179 insertions(+), 132 deletions(-) create mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx create mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index f68875dc81394..a1bea4a8e93dc 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -12,20 +12,10 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw'; // @ts-expect-error import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { Map as MbMap } from 'mapbox-gl'; -import { i18n } from '@kbn/i18n'; -import { Filter } from 'src/plugins/data/public'; -import { Feature, Polygon } from 'geojson'; -import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants'; -import { DrawState } from '../../../../common/descriptor_types'; -import { DrawCircle, DrawCircleProperties } from './draw_circle'; -import { - createDistanceFilterWithMeta, - createSpatialFilterWithGeometry, - getBoundingBoxGeometry, - roundCoordinates, -} from '../../../../common/elasticsearch_util'; +import { Feature } from 'geojson'; +import { DRAW_TYPE } from '../../../../common/constants'; +import { DrawCircle } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; -import { getToasts } from '../../../kibana_services'; const DRAW_RECTANGLE = 'draw_rectangle'; const DRAW_CIRCLE = 'draw_circle'; @@ -35,10 +25,8 @@ mbDrawModes[DRAW_RECTANGLE] = DrawRectangle; mbDrawModes[DRAW_CIRCLE] = DrawCircle; export interface Props { - addFilters: (filters: Filter[], actionId: string) => Promise; - disableDrawState: () => void; - drawState?: DrawState; - isDrawingFilter: boolean; + drawType?: DRAW_TYPE; + onDraw: (event: { features: Feature[] }) => void; mbMap: MbMap; } @@ -70,100 +58,26 @@ export class DrawControl extends Component { return; } - if (this.props.isDrawingFilter) { + if (this.props.drawType) { this._updateDrawControl(); } else { this._removeDrawControl(); } }, 0); - _onDraw = async (e: { features: Feature[] }) => { - if ( - !e.features.length || - !this.props.drawState || - !this.props.drawState.geoFieldName || - !this.props.drawState.indexPatternId - ) { - return; - } - - let filter: Filter | undefined; - if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { - const circle = e.features[0] as Feature & { properties: DrawCircleProperties }; - const distanceKm = _.round( - circle.properties.radiusKm, - circle.properties.radiusKm > 10 ? 0 : 2 - ); - // Only include as much precision as needed for distance - let precision = 2; - if (distanceKm <= 1) { - precision = 5; - } else if (distanceKm <= 10) { - precision = 4; - } else if (distanceKm <= 100) { - precision = 3; - } - filter = createDistanceFilterWithMeta({ - alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '', - distanceKm, - geoFieldName: this.props.drawState.geoFieldName, - indexPatternId: this.props.drawState.indexPatternId, - point: [ - _.round(circle.properties.center[0], precision), - _.round(circle.properties.center[1], precision), - ], - }); - } else { - const geometry = e.features[0].geometry as Polygon; - // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number - roundCoordinates(geometry.coordinates); - - filter = createSpatialFilterWithGeometry({ - geometry: - this.props.drawState.drawType === DRAW_TYPE.BOUNDS - ? getBoundingBoxGeometry(geometry) - : geometry, - indexPatternId: this.props.drawState.indexPatternId, - geoFieldName: this.props.drawState.geoFieldName, - geoFieldType: this.props.drawState.geoFieldType - ? this.props.drawState.geoFieldType - : ES_GEO_FIELD_TYPE.GEO_POINT, - geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '', - relation: this.props.drawState.relation - ? this.props.drawState.relation - : ES_SPATIAL_RELATIONS.INTERSECTS, - }); - } - - try { - await this.props.addFilters([filter!], this.props.drawState.actionId); - } catch (error) { - getToasts().addWarning( - i18n.translate('xpack.maps.drawControl.unableToCreatFilter', { - defaultMessage: `Unable to create filter, error: '{errorMsg}'.`, - values: { - errorMsg: error.message, - }, - }) - ); - } finally { - this.props.disableDrawState(); - } - }; - _removeDrawControl() { if (!this._mbDrawControlAdded) { return; } this.props.mbMap.getCanvas().style.cursor = ''; - this.props.mbMap.off('draw.create', this._onDraw); + this.props.mbMap.off('draw.create', this.props.onDraw); this.props.mbMap.removeControl(this._mbDrawControl); this._mbDrawControlAdded = false; } _updateDrawControl() { - if (!this.props.drawState) { + if (!this.props.drawType) { return; } @@ -171,27 +85,27 @@ export class DrawControl extends Component { this.props.mbMap.addControl(this._mbDrawControl); this._mbDrawControlAdded = true; this.props.mbMap.getCanvas().style.cursor = 'crosshair'; - this.props.mbMap.on('draw.create', this._onDraw); + this.props.mbMap.on('draw.create', this.props.onDraw); } const drawMode = this._mbDrawControl.getMode(); - if (drawMode !== DRAW_RECTANGLE && this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + if (drawMode !== DRAW_RECTANGLE && this.props.drawType === DRAW_TYPE.BOUNDS) { this._mbDrawControl.changeMode(DRAW_RECTANGLE); - } else if (drawMode !== DRAW_CIRCLE && this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + } else if (drawMode !== DRAW_CIRCLE && this.props.drawType === DRAW_TYPE.DISTANCE) { this._mbDrawControl.changeMode(DRAW_CIRCLE); } else if ( drawMode !== this._mbDrawControl.modes.DRAW_POLYGON && - this.props.drawState.drawType === DRAW_TYPE.POLYGON + this.props.drawType === DRAW_TYPE.POLYGON ) { this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON); } } render() { - if (!this.props.isDrawingFilter || !this.props.drawState) { + if (!this.props.drawType) { return null; } - return ; + return ; } } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx new file mode 100644 index 0000000000000..c0cbd3566ca8f --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; +import React, { Component } from 'react'; +import { Map as MbMap } from 'mapbox-gl'; +import { i18n } from '@kbn/i18n'; +import { Filter } from 'src/plugins/data/public'; +import { Feature, Polygon } from 'geojson'; +import { + DRAW_TYPE, + ES_GEO_FIELD_TYPE, + ES_SPATIAL_RELATIONS, +} from '../../../../../common/constants'; +import { DrawState } from '../../../../../common/descriptor_types'; +import { + createDistanceFilterWithMeta, + createSpatialFilterWithGeometry, + getBoundingBoxGeometry, + roundCoordinates, +} from '../../../../../common/elasticsearch_util'; +import { getToasts } from '../../../../kibana_services'; +import { DrawControl } from '../draw_control'; +import { DrawCircleProperties } from '../draw_circle'; + +export interface Props { + addFilters: (filters: Filter[], actionId: string) => Promise; + disableDrawState: () => void; + drawState?: DrawState; + isDrawingFilter: boolean; + mbMap: MbMap; +} + +export class DrawFilterControl extends Component { + _onDraw = async (e: { features: Feature[] }) => { + if ( + !e.features.length || + !this.props.drawState || + !this.props.drawState.geoFieldName || + !this.props.drawState.indexPatternId + ) { + return; + } + + let filter: Filter | undefined; + if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + const circle = e.features[0] as Feature & { properties: DrawCircleProperties }; + const distanceKm = _.round( + circle.properties.radiusKm, + circle.properties.radiusKm > 10 ? 0 : 2 + ); + // Only include as much precision as needed for distance + let precision = 2; + if (distanceKm <= 1) { + precision = 5; + } else if (distanceKm <= 10) { + precision = 4; + } else if (distanceKm <= 100) { + precision = 3; + } + filter = createDistanceFilterWithMeta({ + alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '', + distanceKm, + geoFieldName: this.props.drawState.geoFieldName, + indexPatternId: this.props.drawState.indexPatternId, + point: [ + _.round(circle.properties.center[0], precision), + _.round(circle.properties.center[1], precision), + ], + }); + } else { + const geometry = e.features[0].geometry as Polygon; + // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number + roundCoordinates(geometry.coordinates); + + filter = createSpatialFilterWithGeometry({ + geometry: + this.props.drawState.drawType === DRAW_TYPE.BOUNDS + ? getBoundingBoxGeometry(geometry) + : geometry, + indexPatternId: this.props.drawState.indexPatternId, + geoFieldName: this.props.drawState.geoFieldName, + geoFieldType: this.props.drawState.geoFieldType + ? this.props.drawState.geoFieldType + : ES_GEO_FIELD_TYPE.GEO_POINT, + geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '', + relation: this.props.drawState.relation + ? this.props.drawState.relation + : ES_SPATIAL_RELATIONS.INTERSECTS, + }); + } + + try { + await this.props.addFilters([filter!], this.props.drawState.actionId); + } catch (error) { + getToasts().addWarning( + i18n.translate('xpack.maps.drawFilterControl.unableToCreatFilter', { + defaultMessage: `Unable to create filter, error: '{errorMsg}'.`, + values: { + errorMsg: error.message, + }, + }) + ); + } finally { + this.props.disableDrawState(); + } + }; + + render() { + return ( + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts new file mode 100644 index 0000000000000..17f4d919fb7e0 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { connect } from 'react-redux'; +import { DrawFilterControl } from './draw_filter_control'; +import { updateDrawState } from '../../../../actions'; +import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors'; +import { MapStoreState } from '../../../../reducers/store'; + +function mapStateToProps(state: MapStoreState) { + return { + isDrawingFilter: isDrawingFilter(state), + drawState: getDrawState(state), + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch) { + return { + disableDrawState() { + dispatch(updateDrawState(null)); + }, + }; +} + +const connected = connect(mapStateToProps, mapDispatchToProps)(DrawFilterControl); +export { connected as DrawFilterControl }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx index 099f409c91c21..df650d5dfe410 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx @@ -11,13 +11,12 @@ import { EuiPopover, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Map as MbMap } from 'mapbox-gl'; import { DRAW_TYPE } from '../../../../common/constants'; -import { DrawState } from '../../../../common/descriptor_types'; const noop = () => {}; interface Props { mbMap: MbMap; - drawState: DrawState; + drawType: DRAW_TYPE; } interface State { @@ -58,16 +57,16 @@ export class DrawTooltip extends Component { } let instructions; - if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + if (this.props.drawType === DRAW_TYPE.BOUNDS) { instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { defaultMessage: 'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.', }); - } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + } else if (this.props.drawType === DRAW_TYPE.DISTANCE) { instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', { defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.', }); - } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + } else if (this.props.drawType === DRAW_TYPE.POLYGON) { instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.', }); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts index cc2f560c63d24..63f91a03a5d01 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts @@ -5,28 +5,4 @@ * 2.0. */ -import { AnyAction } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { connect } from 'react-redux'; -import { DrawControl } from './draw_control'; -import { updateDrawState } from '../../../actions'; -import { getDrawState, isDrawingFilter } from '../../../selectors/map_selectors'; -import { MapStoreState } from '../../../reducers/store'; - -function mapStateToProps(state: MapStoreState) { - return { - isDrawingFilter: isDrawingFilter(state), - drawState: getDrawState(state), - }; -} - -function mapDispatchToProps(dispatch: ThunkDispatch) { - return { - disableDrawState() { - dispatch(updateDrawState(null)); - }, - }; -} - -const connected = connect(mapStateToProps, mapDispatchToProps)(DrawControl); -export { connected as DrawControl }; +export { DrawFilterControl } from './draw_filter_control'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index fae89a0484f11..5e4c3c9b1981f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -17,7 +17,7 @@ import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { Adapters } from 'src/plugins/inspector/public'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; -import { DrawControl } from './draw_control'; +import { DrawFilterControl } from './draw_control'; import { ScaleControl } from './scale_control'; // @ts-expect-error import { TooltipControl } from './tooltip_control'; @@ -418,7 +418,7 @@ export class MBMap extends Component { let scaleControl; if (this.state.mbMap) { drawControl = this.props.addFilters ? ( - + ) : null; tooltipControl = !this.props.settings.disableTooltipControl ? ( Date: Thu, 25 Mar 2021 18:52:07 -0400 Subject: [PATCH 19/98] Add section on developer documentation into best practices docs (#95473) * add section on dev docs to best pratices. * Update best_practices.mdx * Update dev_docs/best_practices.mdx Co-authored-by: Brandon Kobel Co-authored-by: Brandon Kobel --- dev_docs/assets/api_doc_pick.png | Bin 0 -> 82547 bytes dev_docs/assets/dev_docs_nested_object.png | Bin 0 -> 133531 bytes dev_docs/best_practices.mdx | 126 +++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 dev_docs/assets/api_doc_pick.png create mode 100644 dev_docs/assets/dev_docs_nested_object.png diff --git a/dev_docs/assets/api_doc_pick.png b/dev_docs/assets/api_doc_pick.png new file mode 100644 index 0000000000000000000000000000000000000000..825fa47b266cb9ca39eed58d0bcbd939a3fbe981 GIT binary patch literal 82547 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 literal 0 HcmV?d00001 diff --git a/dev_docs/assets/dev_docs_nested_object.png b/dev_docs/assets/dev_docs_nested_object.png new file mode 100644 index 0000000000000000000000000000000000000000..a6b2f533b3858357d8c80d59f5a4d6d1056e1d84 GIT binary patch literal 133531 zcmeEuXIN9))-Fhsj)H0P8ZDbhh&AORu>K|y-&B27f9bO=RMdIu3g zl@eMKYLI@H`<#8geYa=(_x`wFo+nw0wbq<-%`wLqbCh@RQd?7rnu3{vfPjEnMftui z0Rbt3fPhHwJSlL+FPE-^fPhfZ0Swkw0fRZTJ>2XZoNWmRcw?A$QRb}V5NRHNp?`sRWbe1-* z_RPwy^?A?ZM#C@CVKOYVjS^C`bl%LJr^mzQ73y=AGt}y-JVq&Vf&2arP=f}*gt7=cvDlUZd zAv1%5yeH4jvfHmPeY<^y>D2t}>}($o{0I}J!6Sm^Tg3@BIn5@Ax)R<1bPcyPRI$_0 zAm9hi&l3<6G82#hXN164mhkF-&L0r+5)l1y{Tu;7m;(Xvzn{?rjz2#!!1rgHe;tY9 z5CmkvUl)O|@9T4aK23^vP4wqEksxr7;GP~>MFlwOS$o*px_a8XdBxn2%mprxyDJ-c z5)fSG{`nfV3w z@8-Z?GS}?Ayxb*0AP58^3=tJ}^LPTfDIp;N61fGsbxR0%Ldet4)yv9P$kmhUU!DBf z&wX1@YYzu^F9$bQj-UNnJ$CcknsqNrv>uh-6!3CfhFox{SJK`eJe>C{z*8dFoucrE*wjN+N7oeq=?Eg^x z-;Muz^Y0D+7}Mx~#=Lv;)?bJG%bkC>lm`8r`d>)#FFOBm6(F=Mg*50tizZ9)@uNRG zu#PMa_q84ZM}W(Ieh8C*AKrf*fpbE>{_FK+nFIv#1SMV5pW zCUhXa&Tx~EuT7pr(Yb=|KHX;p9dgBwmx=bOO>`J4+FoDyBu}`SfJK=3qEl^Y&|k6B zNQf2WAd-E5G(!UIKVZA(dDg2Q^}IltG-1x}d9CPN9N!(ksHu1-HI`8)^=GAWiRb?Vkr}9lG@5#;dMQDXQkc zO`pwb(d$gAd?6~UhMRr)wo-ujp zxOfhIr3|C%sh^*;X8))8{hYXa9-!+Ue zBECj)w{4%%Mdr+JzW=3(2!oS5J@(lB=$Q7C)Yn?g_|iH^2~Ft zH7JEGF8;zlts^kip!fVSqEGtkW*X#J!X=o(pYSpil_(!Ayi@q?%CzdxZIX$f_9#u_ z%IGl-P67p%yyig~P}q4z*opnRVuX^+KeG>=d@Fg~;Aa;l;|C_7AIH9X9K-8{QcU5u zoriX;UAUNTn8N1qB-tIm=txLy|h8_pm& zH$?G?d2Pkp^s4I>Otl%(iv+w%Z8PQPy6XB-hzkSh@V=tK3J>`DTdMWmpwlR)FN#9p zk{tYM#K?dy?EdKDmM;pk56UfCC>!|wa_JMlA@>bluXCw=g*S>;7Fi5ttsPC5a-Wm$ zlsSxEZ=1u!&_}gK^}5q4&E2dg{^zCdsFF7(N_y2w(Hu7Cg0$<;kU*+6F1W6;DqC@D zv}r>XE<&Q6Bw1TRLMT^MoP39+eaJ1&*ciJ=Q zvBpZ*nYPJ9C-pi{(i0LEN#skTOh{*)^G;Sv82#B$zm?=B;x{yo>U0dHbvq|LAtA&l zYh@TU$v%t6-pA^tSw?RW1{aY9H`9TwY&0TTOBJkL00mX2Jp=pNSNfSX3 zFG-CH>W1IWay6m0do-uZ_qW7gVPcPgxsTl99dvCm@m9Yj!+p?PFei1W>G?%=O<_~R zMK>8Rlq&x+6=vdtQut+|wHDgnN&_LW?uBm+-o=YO$pigD4EvUyd%G%2Z0BeiJ*RYO z8@w?$^UfTjIfHWu?%esuJ>{n^dPpM1_KZnLt%IinmBzvT{Ma(j^k9xXN%W#t!l6EdzWC z(E07lRFT(ND#Iup?&f`FKc{F6F?i_|o#Sp5W8R73Ve;4z{H=bdqO>s-S^St$H5GT3 zG3Pz)<4HAS%`5b7RPi6S)IVM&j^wy42G2aqrVtAyKiHRQc_D*1Zi_0;7`|243^y6y zvtXpcOg9>YUlK^t^<)A6t}nXt7r#~7MlANFkN1NTjDNCr7?>D*@3r%9m1C+thZ-?> zQcsyu261b>Sh#a5J7E}Fcj%feJbhTfLdDH`m-mQ1zE@g}Z0Xs}oS&A<7G0-9 z;5@oF73_gljUoP!_utJKdHiJ#rtWh%$UyUb6?MrN5^RFWePBlwj1k~0TgEAxuC~ut}y++Fx-efkh@XSniTD*R$4Gy4UEzSnXxz|4Jhk)*$7! zn3+g~9MHO1s`d=f88G}t?Dx+RWV3oTlV2r5eQ?v+C0Q@S?g5kHiji}}*7Zx|dFElX zq%D_U(ccv9uw^}uWeD=3NI46=bwMzu|^^%}X`-b0zw%Q7(fxu>E>IYW9Ls2R6nmgh|6pKBqJ9|^O%8(3+>yT1n z6ztma0|$a2!D})0%jZXDJSHg(+jEZnJZ)p8(06_jX$~n$Ix5@Y+D7aBW_9S3smtfi zvk06E-A2D$)!C!mLLS9$#CS=Hv62K?>&u5B#@ufOaizOzxWsxP?T zi(Oj}g!Bs0vfSl3#8rp@3VLwp*t)Iyg1kJ*V_*++%klT~7}9$YJ$h5|o@@L`^ zdJvoi`ki$oMOylozCQ`ZI6UX1erCEw26>Zm)6E0-qxoAz>eu;w*Vhnjf)I1POA} zIf+zx?g!qy0PiK#OHx>{yYPbJ^_kg#)hS;}_KfF+7vCa(*(#Nk<)7y?`R%AGC%CJG`K0Y(B`3B4hyd3OV6??^SVNS= zW7^(Ox=#6^MN4IM5e5`pb3SxLBd9H86_FYp0TsA?07@p!RwJm{ch6b86P{Nq0}k6Q!#@t%~FaqDek;=qYkEu!fV41 zDK7P~`|}oH42f9xVS-uznT$qycBd)tcu48!kA`ljUvrQcMD{0K8QT-Y8<01 zmC<9)FrP-z)K+?;T>X}tqiqLD1LkSSHX7%fDf5h{DF~0E@*@qO=j87^>Tv>pDNs$~ zv2b18`LU#1uI&Fj>>((l?ULQ`U4t(vY~a#|5;;v%UT|1#szZ{S%xR?HG0Zl6U$4%P zaCK+#Qe4k2KHT8tzt>19`^oBd;w{YhY`%8ex#82MuUpVhfgH30A@n1+-2KcWM zgN>Fno@W1jg>B$L);E}#4;R%ctrqO+PreH4)(<~@>i40?;C&q09IB?8$S)jlG{u~J z$BYY0sb8C^pX_NToJjlNcy9Xiz#yCRNtMhHK&b8Q*C3mvH{OI{{NsWG^`7XsOA+H; z%`!(wYi7f#y-$}WD#auAWBA^Gx=2;D5R-*d{(gELM^W#=)464*Tpa+TSA<3axs&EV#iN=i*kq=l(NN?Z2*huoEWYBZ#lLc?T5JtsD{ z)~5Cr6LsG|#HY`ypx3+Jd-72?9qc{Z#3GLR#gU6~?U*?^3(2a-18vDroWR$_KLu=8 z;yfY_MkO3Z2qnDX&ERs;b|Jls3a#ES; zCC|Hyw4wY4(weTgr%39s?+12MjPJG_cLs_mmuBUXdbUZNT(q8tk9EJm3;zfh_MfP< zYDB&YobXqT7<^`n!v@^iLHkuco|iOofW|I@QVjSX@^Rl@W98Xvr=qO=xQkCcoOVVn z7kHyw2I%)$Y@-BRmr3}scQD&kU-uS&OfGFS^n32Kv{^tq2Am;pG9Y7DW@<{E4Uzg; zn4^kXEi@2hUZ0q$F&Wyl$GY`6gK}NcA(IC$qKb{XrL9@{jIT-9dw8Fy?WJsEW-INM zZj5kQ7cX|jtEnal@sIWHWKF({V$Aq4@O0gxMM~et8D%S8oTYs)E#zuOZdx?)IvUlvsa~er-qmM)=An=>i7f zM}z(d&Hd6`PYP~sHZ1oNtB8QNX6Z$|D5flT-625hbVvOC1V3EU$g<>X+03=i>bUIi zG1Z=zg^xt-Y)|xE?(YA{#KDdn#oq~zPfhfrz9v4wo^-BDB#3erQu&`0JX@%6{=~de zH)~x{v(uBey1fUH>q@I$$1F_wnmuV0G<%mfDwBhFZ(419S;$D3oSb}k_tmCM0UoE7 zz&8T_;Qda&==!DZ+}uy@jCtP7gn<#=;sLLA{v|j!MNf;VCSdOEl)}~1unH){Tk7q62!RVr4#w$)*L~X-M z5q{pg0m%Ig+Df~F2X5JL#rBM1oV`GYz7S^CWgLFAG)s^!yr03t{NBOvqUoV5yo5n8 zV}R6rK{j)h)pexkWk!G%mz#-!otA|ld&}J!6vSg^a(Js?c?J(QGvE9A+~DJtAM1Jh zGWB-~Qd)NbSS7>&pX_b23ozaj*Lpj=riDVifvGG?!N$a8hyD7NEor^Dy@1*t;qj7LUM_Q}9v+M0sr5lVc*n-h^o8`HY*lQQ<;sU4Yn!>( zIW?he(XxUV&|UG@t7A79B=(!4d@N9&#$5~T?A7aPNy70otYuZ}wO+`36qopUwPa5G zTk9Kc`mb;}j0BL^7@3AxdiR^+U5sy3I5lcpM~-ldptD7n!WtkDSB2%8o}o%n zdi${c%^Oepv9}gOrzP0YN!U{C%>wZU+q1-4MOf;6GfTwcyE*-r_-1eOTvKb1mZXsI6@udQjf`%8{l;jFx!I|tYXD%J z-rB%Jh!7H<9mi_a-w&hezTm}xI4lj~xZ`;J)LcZWCawNK?`<=3aj!|TffG8q5FhqI zPL7mtZYEV6Z20J8b;oFXVP$rzM9I|Aw=g@}Uez5ZfiWw%=h&qBJ}Bx)=CYF%sA)Uu zSaOFJSD`7xEkX6(ps=9I-lt(>+DRd4=Hz$|6&kt;gJarsX4q+)3rJD5RNz z-%vXX?)dt|44_LmgYb1!4X(TpP?H)}9X*HC=^Hf&Fwx=kgoIBTaH!Il0UYd1+-laz;5uFl{Ts_gA93-y_{KlCc7_<>Rs&-;L46zYmu4n-^Xz= zZ9A3!8mCdxn@gcYFi6P&TtgMQ%l4CNvS!{OWe|ADWA26;y+$irhtaBnjj0-?=Cj4| zX9Qo^sT>Ag6k-pQYh2Y&-#i>BaYdv#`TMv`)IwU0MH~wptZT6~OWP=8R8F}U*Hd`h zd)d+FHZ$*SZ&_{3gS@ol3eA`YGxHWjPMQZONlDD-b>nxvi|_QjAbUU}Hdfi*r8`6H zD*1lu{mUJswItQ_3CMKaB@D1^4Bt9J1y;4aX5qKE=2*$XXsl;boe>e~3)SdHRfPjx zeU3)!-}5OOrwMxo%uhbkD6aTizS+K0UBAnKLfNJ{jIj5U>`xAP>QW%4q?Ok6E1fUY zTmM+X-r7DKEIYTH;(>fw6+B6^afmQmt7%?n%vQyoyha&+IfAkjAzQ@FRAms4{j}O- z=67W^-So8HeJ$cxKSeo%xsVV8tluHO&OkzU5E0puqA1y@vFKusK=RD60&}MRh|8Cz zSAY6s?ANO$5Lxg8q0}vu#TyxKbN^)>+(30!BZ?~h>j^%vfuFI;i7yl6;x>NL7K62S z|4u&K*g@^Xdvp4$12Jn0lRl&u+n?7F(ci`4t4lP3Ah_AoCI(&bp27b&M9Z zsP3~td{NIy@$r|Rh{vBC?f2BqM#gt!`+j8`8AKOuS)}IR{HK>F|fXZd^nh)2c z{A@Pe4fLB2*7S~=r>HI4MiWG;mY!EjRukQ0OaPPw6lIB zy+An4_)>WP{C4+i&{V;X=Q(rpbzYry=|^TOV`EuYCHG$*-7*Q&Xk0yA1eqXJ?QHvd z^X+&W*qh8IchIkDc%Z1*M|Q_zMshf3|hCf#USn_@7uee zu{ZS$zg3P+Sp+HgA?qQ*upHtr&D;m4*H3eH4w0jZ+m|}6Q4}%_{yg_>`H>t#v(z&*g&(?TQR0!;ti+98=NZ|0gIC4$O;oOWL{&5Gl4Qi! z+R%C4QvVKhu_AG!!8sB;m%Gag%`2mSw93?~xQwiKT*}N{*{h95bHn^MiF6CRRA8Ne z4%2$W6Shm4s*XnMRpJ6_nBI>IaA3;RrECO$!B1u+fv*zN=eY)mO)p$(?> zj*+`pQ>Hw>P@AEn22LP3_P%*C*;R!)j-S~9w2uKe<7PJ8zW9>>)8Tw0C{I1L9ei)C z>0YguS{9Kqj3M%LR|c1KY^ByhhK=n{Cc#fJ?{;b*?0#4@f2$?Cl~Uy}{V^+j#?pQ( zE`N9{uGsl1^NUp^gBWh;Qs&j7c5Z|20*$Vgu;vrHs#J0Q1JI=(xV>%xNtM~7!@7}; z${yBK@YuU zkc#N&AwL__`ib61Pk)SutEkOm7?3XH?8Z+>}Y=n3>k;?;A@4~IEV3JyNLugTPkf1FP-Q4{<;M*x<|<*ilp z-AsunEv!sVx-hDr*6V1nr)j~}b}QrTJ{r5Vzx}kst0?C=jZOh!%;n6EmA?5fA|`59 zll1a-3$dx4{lNxYy;D=G(mDwlcrqQ6CW|@N2r(NU2&NH#3+V43V3o`P6_oeq+4N4# z4rt^e)#vT3EZn|>`~VxZj^|R>(scK|oj1pvE@1jE-fJ_%Qfy7zJ1Mh7P}x=Va{qj0 zANraMp2m_#b2WqQAxnp$!PeNaNk@=!8@;=?-Q+&0Hcu7QeD@XmP=!NvAoOIIg{$VYwq0!+GpDC*_D3$t?0#;> zj%MASZ2xizF0xy&>Rr*f+pMg1Wp}xc(}MWRMXGuy9O{Dw;NDyp?PEL@&;aqkhh8g$ zQH;z8GMBC-?>q24vG}Gu>E-x6^Ucn&zmvkvGZDX;d$X!yZQ6O|z2mRUSbcZa%{wo+ zu6&p~JjIL#bC#^TS-=!lkCkJ)f#9-p`K_H~Kf?C&jsftpo7B{|rPbAjMDBxG3;?5> zwE>q`P(3xzklY5Ild<_&CGF?gHD@w=G4n`SZ4cVfwYN`7`n)Z&fSYMss$frMuVt&c zeBN&Jqz;p?OmXOa%yO`e=_-9^He>JX{iD@4`9EQ=dRF&wwp>tD)b+XJo_~ImbQ;AQNbAJ8BzXH~iwS z-?-7GA%op}w<_W}AC7ov?8TOMwUnCL2EaqOX3H@-!ZZaOtPtvy1{A5~OB>5wgb$^m zRA&A|ByZZ=NRpk(8b4occ3Cc4%QU&;JjGjry1vg(%~m7y<9H6UO9}>u$qBLhZ@zTF z0(L{v1L}1$aiP;1w}vgrdZYowq=TI*l69xz;k$=7=SzHuv@GTG27>)PA+1SzYvK;=usNadvV>Z;vo97hV|$|g5<2}YHx42mvzBUvxljsA&PMj5zvHSI1}r<- znG^U8Mrp_{`_snOEU;1?l!=b-jvj7?ZV?VlLq6Sp6Et4St{&et+mAQU@;;fAMXQz| z5}TkE#Nf7_lNA;N?w-q+PI?$IQEwNf?Y`L>1A^7eb27D3@4y!e_369gX&IcVd+V|^tA!h>Rtu)lx!&vI-7+U^AI4uUD=oyY^y^q@l z!6(>6%}mLV67(#PU3m)*!`~`DBRaLiKyQmVkk`Iwz+7-gZ5 zmtq~-WdPKOoWAgzU3;h$)bcbq)WZn9?wsl4V$!o|75&W{kngeN+`Px1DSTRvVkc$l zp$-@~WZ9aiXPm+D*&p3-a%w9o$`dZ{( z@z(zNp>9?;SraeivJg`VjI=lq5JEX|Wwi~`2gq(h(JPw}$uR9IZe*Kw9JzoPgS7oQ zkdXd*Lb3p~hP-~`Q`u<(PVRIJRo0$f)b)xT-JMc@aqnTI6_;D#d=4~@3&IYGPSN3m5THETmj&su8Ms8FOp1;)%`H@FkD0tC34dSIa784BS=%qe~zT zVLOYvH+Vub7G`zo-0yW|gZ#^`!I8@A6{LVuJq!Ao92-0iCW*+?Lpxz)F`>IZHkB~Y zJi0G%k2*_xbbqFY-H4}iwWPOWPZ+LcUXJ_o-l#%~^8W6}h_X(BvStTVpx@#Lk3)wr z7`%b8mMMm}Lrtv*+JU2IN|#(}{rk`c8VAly7)#GO}Ww zeR2CuX1A|Cj9bS_8dTxZ`fn~m@heRN`?9cV20@k|q3`kXM{cX65~Yp~Ws!Y$vN?s= z(?OK0<;UnAf6u0k$z~yGW2|4x3305Z%cNp~rtLF1rj_u|1JvS_9z z&3@pT&?7esY{2X` zpFw?I>hrhOo!Y^T5NzVK6ke5FR7uDE^U=FY|)AN{=n*QT^`TWR<>GR*?ZaE4?-n>DN5c>uG26Kk3Aq zla`%hY9iBSVY0*H>l2;>thJply`9M151!|Yq>06`nZ-Z92+#)c zc^W+*Hs%9~vSSj9AE`Co>05f&&v>Nu@(2nI&cVTd=wR#j@4-N)a~;!DZ*{VJxSofF zu|zX`tlROrBFr>=!U{qfUj~`KDZb^s{Uhegr_``sH8B04Q6_M^i#p$9X^|}ipjzOm z?VH_ZE{WY$KNIm)s3#+;=B(qx0P=cSOYCtYik>W82C=zrg3Io{F$edt9ha*)hwYK&M=X_NKNOlzp=2-g9mjrGYab&2;qHKXoBo zZJ&_!IwZ@Kv$oN;oyKpwl(y+)GCsG1EBOUYV!k43qS`uoJbrP2&+cw*GJJcDkvBoB zv^lrrVX4tqEXv^=GlsnSk91rnmol1SczZ^4wx?+}*{?%2P5wKOq*R><@AA|wIdhBf zzqPF-^(l)+&gXm4nDg{)fjeE4w1I1xg`Q1b?1Mx2yAv%S9a-33^0OI(>Z^~EoDQpY z)*2HzYmwI3O0~I}*1UH)_)IE7yaF~Ay3*yMF)F)2mi2zShyiJHV0-{s1FM=yUil-NB-Kmce(a z(Mc%>C9?j38JTjcHA(>wKsSJElUw2=k zuA2$`ezlg5N4!i3BdkRue>-ESzSxw!dA6#laLzAicihi(Y1 zcPfsHdnZ?WK0(?j+-3A#3;F<(>^ka&U7{^n$sQoyag+W}eDP3lHaeRZ7m&d6pjmeN z>ChaH=|qKR@{|u6jMugml}^E5?oJjjd*$!yuI#=_i`WfOVq4=%6kuf>+~CP=NvfEA zReMCb+X#Ngmd#?7Vd04Q(f^ zow0uW_zi;qGjj^5n1?-=Oxxgae$UePCF>qv7$3Lk z2(}HrRzh?aW^KGyf+HQWGtc%kcbL>H4mXPZ%tmoThVPsU|y z9g_F7itSofXudhzsbU;>U0N1q3eswx!bi9wX{njT+y~!y9yCdBdZKbWuy=RCI0>zp zDZ+apQh`5xyw@r)yuDV+9C~DZRrV}-9eSGC4yr{O?KVgzWSrH>7SaS1aHhgrvI;r7 zg=o$4iV3-8+F}B&4j0YXq3vDur0vMUxhz${#$vuBV~w!hp?z8~{zyQm?dbMO!+vUU zleTJm{dicQ?fM}+>&7epsA3qkNV&qfFlv@DuO%X^xZD+amA4Z#N|=tf48b z>Eh>s#;m4~ltR{iT8C)M#b-mD*-q3?cV);-)EVO!L!ux?@1VKxa6Y6Ul|N= z{SuWBQRUHNTOWOf-%PC9rF0PUnZC8`%%y=r^LkSoTT$#q23qbS5A)nPSA9nGb^Da_`&428pK2Sze{N7r18Q z^v_Bv_C3|&Ob&Nw7q;oF0(H8`rZnDP8FXn zpknpPP5>Ev0Rk_l06bH=owTOP%1-DxpE#(ZrHH@)_*QslX?XL+du*&c-?Hu8_SA>y9==E-v~9y)jg)P7!YU!!e|rm6(4kMQ!pAgB z@E&(^veby{Aj?>v9W+-aB9opAg+gChc7py(Z3v6gPh8t>WQPFLa0jZ!uh3uQv8nt3 zC#Gm{rI#51eEDm=K3yHdXg{8b0t-lL4UXe9sSgxP!q$cl0*oz&za3qa`e9m&L1a>5 zujK6Oj8ok!un0Mdz?Y}Uxa7PN-MJo0`JxK_ic~5VL5XrgyggbzDcT-*#Z71xfwz-r zwv&+9nWERwzHrwj!Q7%f+)XayvU?Qi)e^}&msyuxG2I$7E_lE16ny5Aeumd-J?+aN z$eeLtQCANn*$2>eQQVnso$RA;V`=6K+n4IgTg^beFK)5UXol7AFv81U?-%c5Ku7o+ z-HUFg7pcE^8Op(jp2$_}o55UIuL+w@RQaU8^7D)nG%3F^*L=J4opEDR)vA~3x53Tm zF3Lqc(Tp5{)zzDM+3`7cSe~;0xx1AStcZc?t4*%}FQI05Cu>S(<-{?iVCHn5aF5mC z{i8`dUZUFD%wSsh1P7Xv520qIDO=F!<6_Z3VGUu2e_$OJu+W{PlI`7{vaZUd;*C-T z63T8$En1H-q^+)QoENgBjM`a@ZrGCdCsvbru=f;gaj=GBCwk|om4g+x;Ii$xPTPLj zebBiVh@mzeV0n!`_yn|aQpdY)3>#@W(%q#U4OUdM?OXtTgRMJ4scuKU=@?W3fMn6s zejjV(09SH22YmF*DZgs#b4uCbb6TaYsrs`?M@fHFC}V~bY`e}}5}iH%2qAr^l}(E; z1O4%U&SS64nP)?eV#P~NSD*Jk)FJ9x~>KuD?fm-#m)K6FSg#J|~J z52sNq1P|x?Xu6RpS*~d*q&Zp&5^?GP<|cuVZXre zBtd%wScuMSO5 zhRunh{q3SsfdMA!5yKvRhU<>FIF0ohm6x{^owafdQ4x6m0MOO1&edS_YV~bppaVKx zjB2Vx-ZmR^oUjo3Np%AdtN--r`VQKmm^H2JVg2I*aqu$i@XzQ%u&vnOM3qBC0dEHt)N{jT)cY8-=I(vB($WOPv_dPv{f6r4ij*MPtRHS+FgT@+VD&g1%F-Y&y*G` zDE1UfCI&0%Wjk^Gz1;4Z#WTy!ve=hj|FB_(HAT_=dJs5o;=9CHwZ`hr;0u_8?ZcR| zb5P?c_t^t0*w7~^ZALV~IU~LQAaJYJA`hS${)Y$R_$tP1zlRj0)P(5CoqZ;cwiu}| z7lV8O{Pky(1pF^-!hr;|+iGIZ+zyalsxV)MKW|B}-5K}oTk)5dgv7v^1Y`^+cQ;oK z3=6qtM87oNd_*VDF+%<)CW)T<6`tb|hV`F#j7UG)jc~*8m6&93kDGa`d>SD~TpM=h zAR<1GxJ8h7<5zr~+0UUL_xyq>}$Y zmtT?s=FU$b#w<+V`!r|%IWGE}iL$^&DnHZrEQEk1$n~t3``1bG19#r0MEP?AzL9jP zH00~8E8>ob_a@a6uO+BMG+p$SjJ=%&{Zgrk<6KqW|>MOPi zgaMUIN+V?&@m*i4P5ScIt^pCY*kckfj|;@x=qGBjf>aW_Z}=Xwd4sN#1|ZSIywEn) z--Ay!w11erKa=UHyZAaGV!FGe!~nqC2Rj2I>yxXf@ubq;dC~Dq-a{k-OtCx=KqV~{ z{o)s3daBVMJ|pW-6Z$dGOB832AyK9o4BtaIqIlCH%QQ}a6Mge~NA5K`-k20GdHjZ2 zLI{gQFI^ZhI7rf&^Y5(O8Vi(iNDhD3F^{(7Rpouh`Ai-GFbfq%OFa%i`CsQOJOHYH zEf7z>MZB3;2bIHcfhv;j+S_UMCqRiv=k9(F_1}S`&;C^&l7|~V(KrI|<3EcBY&?T9 z3Z8(2?(RCfxAcgy41+$cN+k5j-IX?f_6*N3{Oln(2dGrgkWUksyLjJe#e%ySvDbMm#FNMWG{&=V ziG{xi-Cti{0eEW#whKj{}I#Q-q7na zLh99EH4{YNU806%WTxs_x_u|SBIgz#;P#nvu|Gq+%j}mu_D>yVL;i;ekoKi-`g(LA z^4rXGK2LI@xsZTjK;iQi^J{Xw)AsAMP2~+4X2o^VXCo8#jJ z34ua=A^vvZqASri9Ekp^V*hG&aYO=kivzKg?igdqtk7+?3>%Pv7+iSb*h{S|mTRvV zCyG>~z#r;}X%ALnUw+8{p@bgz(z)4?4FY2OA&JXcvM z*L0e}ZVd#Z7RMJ+Gl?;_rBq(JI1kRHC(}%XpQ1PqJkSZ#1GV^mxj2D(H`XU|$r|q% zVl8lT7-jP`r+=g(%SB-S_{2`KZ`(ZceE^dA7uO^5e@yx)88iR(^I`Ybne;H4?|hUe$fv+p0*EPxC#x}=C5DHNZ@~r6(%M_+@z=4 zm0=mXtjIyO#?|)}!<9ZS%Q1q}btVsv>l98+>&+gec)m;M1x4~-x{RstdK{|2iEg^N zB==Jcrsa;~(rOIU+_{lu>}S)?5Qq!Z^c&|^pqNXO>*TOUX8j{n12+a*B^TG5p~`+% z^GNp0I#bZFQ4P<{ny{9?qedy3VLonmDPMagq+BENheN2kMWsG}+RR;W-w9~8ssj&H zF!w)5b(@s<0M&7@eLpGA%UTgxP8~KOF+_EQZ&4|>!KP(aJ~<dnT)jKK(>~uf&@_7I4gyr z?C$4lMvrB|w|f|3PVco}A|1H7JKoK@oLLg9LRs61U_HurEb{koMe2Ht77X&sUBCkk@}k5JM&<7zm0V6VcgE*Chw0F*L`Or6-4T0o_tn2r`j2avFmk3go`(| zkgJmp-{Poh0FUpyB3iGzTgmKd$Y}j$=&bFAMHhHZqoMX?hW^PWGVCGEWQ85d+~KZ$ z+Iv(pXhnIu0H2lSZ$U3ntvVF5AS?o?qcUXK2gJbnqFgD*L?MorJt>*#dF{gFvjnXt zC6p+ibV=^_CLPIfXaoh@xxwjNc;IVu_I`R+M-g3zM;}tu>l$A|zRiBwZ7x(k`4f^^YC8>df{^S>XQqnY08^7nI`y48rwCVZkgy-h+C)kux0D3BQ+U7x-8q<-25 zNFhLLf%=@LQ%LL6+8oZAAF`3{%^-s??q8rqe;TESfB+4neLnT8ipf~*_V{3|-q;>_ zmV89eGlwT2j~Gn~ZQ79bR%AV>@zseL~R&_; z@2gpy#u8{|552o5)$-2C{CJ>fe&Fl$OT1xwc&IJ9`TBqsB#AFWX_;D5@7Po4vnZ2C zKX#cvx$_CGV>%u?4tagg^m%v=a&*e?*C_j+>SkCN@Tq-1a0Px5!u7`*@RU}L2kaa| zhnHSaaPGfGO6>N)g!Z!y^x4+6xn66k0tJA#hG!m>`VW@`*_XfGp-=7oLUXAQJn5jE zZ+n~>nLkDz7WPJ94EglTSa zwytK@%0oSVwzbT>ds#H0Z>^G(=({f^NEASHt2lFS&1btJmA=Io-lDhSVp^r9+jz?s}pYYNyZm0n}_YdzLeLWQhJ+pLL#RZ^llqTa5olgOj>#DXNSE+JVdk5=yK22gFc-g@Z zsU}lD-#cj8&qlS=9%={WP1Dv|xAhY7ib(ZbU2rAQJEGR zB!BS7@c&+LrAdsfaL>U0TE6h{Jg48Oj}6V$NoZyMs7@eko0lOQnjq_THGsNHN8@) zE}6QMi=`%XTDXir4=Px8{*kWBhxE2qOe2W1m zJg+S6&S;*}Y!?2GSg@mJx)zT~}Zb!zwL&ZV4|oWl-Qp$CH$ z3ziQYO@n&Q(FDix!Tpne%zoN__Yi6hlki^{C8hXF?g&rhcC|+y5%BQyIfUul1b=I) z(7_>a>tD;1d2(wkKj+)ySI~URiywB8?sjD8{O~zAZqn5|5zV}y$uiWLHGV?COt?TK zC2`ef;CK_^Zv&?BPr%4Ie^$`0HLm#7Y~$BrW?PYsd@Ua{rFA`Tq?KhwCvgUcURZ!Y z0Vk%A1_`|*N=@tI1aC#_4T1HBJ;gk9ZaSfFuPV^44yU5IlDxOPPs6rTYHyx_+~&Kq>$8 znW8&0KJqc#mW`$Ngh>A+C4@!}(97hkFn_z3WT}z!oULu$P3QeKZ~{#n@XTVW3}%0e zFSEyUxo*AN6oCpz^sV61c2B(b%Y2PaJV7(Y|I;<(pB*BZ)t`p{ocBkq0*C$9kb2|r zvj0e-y+^;tI%zp$e$Faz&9K7{Jh14)S#gQMC?t(XEqf(fsWZ=Ya?Ng5IuOX7-TF2& zZx^Z-XE&-|!AsefueNvbo|e+o;*-kDDWBF>9YVO!&~ll08Xxt6t>W_RAz~Mku)?K_ zE1|Qy&1Xwu;x7ta983fRsOBupM}RxsMHyPY*!Mnr8Uc1M$l~p3Tb;EN2H<=%a0nZG zyC&N9Ht@V}o@NAH*&$O!WLm-knIg=l)0=>d}_BtDXB3ZJr#E$1d09 z6Uz>R$Y)MUI58Flb-JaMG>Z}^c7x%gGIWfOJ3>M(I__Vw&xYY@jMZ|n+s5q_nib5F*1}Tcc#Z< z6a2~Xrl;t&grA79Zv-8mDA3@P-SwVEm29C6woZqC>v2h;OTE?`+|x z)m8ZOOylo3W*e$&vI!qye;6i~hh|ZSatfxbYcnch-G{Dnz?bO{3Epy4-nLL6WwnKO8(A;Nc!+T{MVIWJ`%QhR|;eLeUvJh9%Na+PYeA0I<{Y-Tyr z^=NyY)BDzl^(EREJ`d&7@+U91UKs_Ffq3YLF%Q&1R z!7dw(m7rIw@?L!0xtZ<=jI=0>cJ;#n(s!7 zXQJNqj+wSq#K}rN6|4NZRIMg#B?Xpn07~`le#5hqvLb42H|<|ID4jro!0-+5TiXp&GaZy#8)+V1KS0psReS$Ve9qd9W)E;7`uVxbx_H_q);np@MHSBVsL}1^J^f_{88Fvg zUf~R9$=2~tPAo(@QQx@fUGqxU1c$c$85X|?a?io*HR`$3=`2>(s8Of3Qvn-<7Zme@ z@7)*7K305uL-a+hKJRF)mh05VgTvh)uVGeZTM{UDkP>H^rI*Tw#lVpthvdv1kXnsC;4Y{kGTXy==5u=*!qVURG^Lw&$)L$Kr zgFBb2nb^`5#(m+!|3U`*qhR9o5gcMw>+Z2_Yxcvp`G6N1 z8V3o#&UV?H`RLTGS2=fndJSh+mU%@|g`CUj?*?PtCIDWcyRIu)x4KvJ6U$m9dw;O6 z_?0&=onft$(wVV#t4PCQ>u1vQBn4F~BP`JVn0PDKqVvh{O{J!s^G=R|uRl22H2 z`dEMvZf3$Y$H9x%d)#>&qxYeu5D*5Kr&*U%>$LR+=sFJI)cA(;Hee0ge^~-ttVquF zB|)`kre2Gqy&(r=HAk-&}3jpbI;; zRvskc`0lfpa7c~4I0Ib_4SDwPs;_~&ncDB)J6hl?{RsnL zm(WN0oSD;}F7nYrtnboM@*Y0_153f%iOUS0D;G`woF_fzSP`SR;NSLmW0`vFq};<+ zGC+bP0CnL>6Ve3=nQUHG)7=PG)om&-zj)f!<4aPO=@^4t$_yY+8XaamJAt^?tPXYz zEJx$0oJI93-c`Ub{QaQ;AWAZL3*!R)LLSM>{XYzb^F^zg>6SkM5tZ5OF-byLX~vSi znk%450{tXw&0Q^elH+6b9Ap0nS)07ed~K`E_0EiL%)S{7zh|GpLKL6+GcLT180)pmixO?aQZD;pq zN4mnFXl?u+EgiVdwQ)G@>CG|9fRCVc3hvJfmp`mn5}fb2SxqzCwUY3+r7n8w;`FEZ zEB5&t{7;46KU##4IKW;kovd1!{jU3|)3NFEaGhta-W^wwx>VTaV8R~h$KW|F%WWRb zR`dE@w>b(|%bQplv(~(!8#1w04GIj8On$uaLKBi)S$X=n@`c|JAI_r>@dMOn(p4}7 z*CSZNV1N6k1v+Y!twdyr^ZA_n3Hz=)jkKR;;fKrK!atHmSZW7xBY3DjjC$Js&z+!u zw6?aThya_%V3WGT{{==bS(i;#PJgP&aVz9{?08Nix23UWh*hjT!!XG2f8*}{84zF_ z;P7w>pCEw|^vxt4io+XXwb6vv`Lu){hs!TIFU5|!Tm~Jkb#y>%C>yOqO-bq}`TMT& z1q>VD$JCA3w}+TV#kyXI^YuRXyqTW&mlgngA{v1J_Dr^Y_s>vQPsjY$I-b!m~ z{qXP`P3<`&{7Pohm-U z4?wA0HPhj7_`}DUJMHq13_#112f2R#mfTw8eoAit6e^AN){!2~) za5OBK!s@@Lnt$D>zaFQCFmSzbt$8?FSH=^38+CsJi2lW;w!kEWZAn9m?RWt0n>pgt zCP1I|si5Y6lJ5Vi9PQ%(a~pO7k50tBO3dSOlC7g4BZIZn<_y)P(`{~W=I1L1E~K}9 zfc@{$>jU71U<$qjiJ?;Ct$pvX{(C<4XDN_?0cw)0;;h*J_sM^Ym;ZlY{rTJf-KGDF zkpIh$|I3d5C<=d7{r{^BKxK&M@y{Wc^iFWK-+sz!Deyg8_78>*jdWvjKk*EY>?oG?){nP@xRKQK96 z?>32(`;l@~x3X+mbNW8VIE}-;!1XpTX|b9z>WcyAoWtap3o4@qNL5XcQR8QnhqFKS|`d$c9G`1YHANpyY zdcmtZiKZadazhWe4A*5I0P>UBy0s5WAl{hF7JSTD$Hm}tg2uW%DIQXn(5?nm%|Lii zb=$U6EmCnMNzJcj-75_9J047gD_X5Ht5q_}h61fL@xr@&VkPt)x4U8`q9CvSdF}tW z5x$3rifq`v-w-WNZ&tjY9jl(6?rmuL9v`n)laO0A0ANZ zL!k3by?*G?*-aG ze#+7)=YKhcJRVfEnq<1J(_KSMHQ-k37C0D3vvj{0(~u^<{_TPJUy(!jl0Jwc`@3P0 zC0!6Mv%3C!z{%^21=RDJO>%ZABkwaJwkR9OxM!Ga(adBxnV{KW$#I55%TlB3DE9p$ zqjn{$3p`44IE%RjXZcS%a8DWqh%6%Y=H&Zvue{vrH03Af*E!@cTkyVknVuufXX}II z%~s4!zkXMG3b%qR!kcW`CaHyP#4V%H_V+7`v?|KY0kfy& z27UIvSi6$G*eu8 z+Kyg%W}PZMfLvwpVgr%0ju?XxZqy0>N5=dseH5p`*@m>a?~Cy1wZd|GNnMAUAE!U7 zX&ue$nK*Y?lYF#Cy~3moFhulwg{UZOQPEPzbN^s3*8+dZt_!y|ZpF3qB~dfUJKO5D z^})Ka_&Ub-gYPpwt;LMYh%}hARv_eJe9<`r-|$l>N8Yx)nu7qS76)SKaTvJ>j?b=M6DQUg!%2;OfWd9w6hFVgXf#S@On#-Y`3>Hr*o4b3FrR~8m z=!z6OJ!8_QPk85!#noKxIr)0oE-B?~54%wRfW7E>|GDF#a|+p4lp#o!!S;sjnGW>C z5SBC5qEyl17GE9qqxE)sJC=bN<#BOu#mm&1$lKq2x@K-<`|) z@V3+IVGUuYnuK}ER<9$oaof)Tejmi0%QT+8p+%iQZCOFIw7aD$t=VuY$? z{8lcEv+r_yeqbwMhyZ=WP4$GQCkxquEv;Thf^_h0zG;l{f$W@5${2 zn^wn;ojU%}Qlm+q%7s^6k0M3AOcv7)2+rS$fH$)^G=1Y*a!{_W76@ob%=DD@`IC7gCs})q!3)>pEkifuqFxh*8&S`@(Tpv-X3PyEXC%ID}VRIuX?H!kznU8;Ii1u5u!0 zTN1zt`=!}FE<5Yu0J>v5I25l?kX)=>z#ZPX9ZdWsb;T4Re9>#-JeZT7vIw-xwwD9Iwg}tAq32l-gS3~iDD=_atu+mLd$nc z$=|Au6MT0Uk1?{#_2@A^EqL$n!^&nQZEMiG`ayZ>-J&($WS)j{uiBIQCDh|aeWK9( z0CIvj&$v(GaPv(v;7qWGxdvy9xe02rdOSqKFUb$yhrP^2vVv#kDfGV1AS{_FudE}q zaGo`noHPqQqGZzY$P{hE<60iT_w~Z!zaACx)@m&=N}XqGiDGi*>{gtwqDXSnV)gb- zxlsBRF3EF^n9N~4|4StJbpIw!!Qooah5~)W=9jaVY40&I4ny)FulCwHvdNRn_XXx2 z`*dT_7ScS;UAyCq#?00D@qDefb9H3IVLrVFa@a1{l8sft7Y z$<}z}a%FgC#cYRh!COJpt|)_l z>py^Mnq5jM+^r~%W^hUqAVGuJDHI^)^C@5E6!r`C)4pv6B=3+mDEmV@1-`?bhH3JV z^CCI6;V;Z?G4%l0G}6!Lf)_EBlJfXp2LsZyAA=-(Y}bEH$|I>XKiuwQZLi&vI>>}o z)*$-7s6EsTH)os+{Ow1m(l! zaEU2ZFV~{`&XTJ6budq9H>L^n@v7e?b5l~YJLcp=x-D{8`8P4ihV{d$Yu)W4Blb}_ z^=6kU$kZ8MfUUu&DkTYf&f4TtW`xed#^NkA5Q{bLB8_K*tB#u`)A22t~w6@0Q6LcxC0UFa+up}pe-B>%o zU4I)b#QV|9M4M~Y7S_I_8xPJ34teX9ydWLr$af3@!)5n-V%SW9HWk_GP!(Moau1d} zIq+E^1yNWos~^RsU$_>}){kd?G*ZfncUA&wB;(AIIW@~m)_o`Dv+d-*P2ZB8{=Oc0{j&voQK8cj3z~|&MdkqKvM6S-6I0?(k(a8AL|=aym~=GX z`sUVXK}iU9pn1d$U(2{GV;at<7ak4a+s5%a{O~r3H4O>l&qUWys&Dt)KOH zU5nh0>xHY~P4f|zJT9%Yd(2t&7VgUm#9#S+BWcNMmb#r-$MQp^m}o*}AT91~C1SUw zhdN?u3c`POW$HM8b+e7Qh9B9mUnyJR+!lZ-ALN{OEAHDQHXJL*6b`;*@7qXW2ECtdXS6>LGN>X&^PebIhQAK|1`pME=!z!T;$PpT z9A3UJ|DM zILA9ryD)OZb=#P_a{GocjTJ*~`)yR`cFwaO%JotAUKuRMo`T{$K5REque6VDrty@V z^b1#@=S|xhzsqP}GsWv0X_yWTuys@Nuk0xn74OS2g zSY7zo<*#i3nM6vGVuM3Hqe(7Q^fZk&mNXu*tA>s@|PNO{mEEqDrZvGE8KJ$rEAX`|#uCj-JW#B1A-RcQm_3!c9*4 zJ}WDUiJcN21H}d8O2(pYYG&L>qTm@Qky=HO<>t>Z|0-JGSWvBgJhe8Q@y_=B*z<_s zq`*uwozf1)E933%NSW&&!)>_dXb^6PIlWMI@!V8QW zw9fN`UR_g@(R3AT{paAd(uJbv^QHQgJ;q2X|MHf`ol+`=ANX!Z32JDJxgwRVeo7`P z-)Mt2A1i+cp22Lm3=o-^M9pRgYKBpKhkTkqrFvMw9QK~>q#CT zWleK%8jm+WhCf(~U0pI;p1jXD23Lj<3~HD`ZK4NncK$G9X|m8+%o1UB*|t`?onNPXwz z(m{b?cieasC7cZs>Gd7PNZf^A`BA2#zvM7<+6zBG`kSV-zS8|Q$d?U03|I6{i)PVM zXX%5z2*(#|b9vs&`1W;BqA4b7FIsGm;cNiItdt&iCVs{gqBw=!4UB{ z(+3lAwo4`L1O{|-JI>L`4QhxA+lJS~t++6_pbG}9 z%brUrd0o5WUcSgFs9~U0$;QXr%B}cB>(KO@Z@I$=O!YbvLi&XTow`(?`cYOEe1yIV?Wh4$W0NB>f|t0^3z0kGx+q0F z(N@D{i&#s%oFmuPgZCns)xzVTMwfIZQljWjN{n?KHbK;^Rov4kdMXvKPNM zWK-)hS)z9xS{>;Y+vGfM##irc?fY?(Ox1yx_OW$o3vKh_Norw>XxI;*l)%D)dItQ< zL_hU>Rrk&H#-)ZWy%+)J5g)ynoKfYE8)e5k;|g7dXlc~#?yI3{dYPYfy)QY`-rw#p zFglPxqHxUj(YE@X>SXb4I=`Y=-+&ZunLCEEy!YzD8x*}n322Tj*N&l?d{E@lkUH~* zvJ#AbJhhW2Nm7qpznEietsqU+Z`9mknCJ6(YK~YN8>kk->Tt-Hhch%26YA?XY9CPz zyzqDE@yf5VLtr}4*6o};h7z)W<56qI)+C1;#sw+F9dQX=U$7Gl82|DOJP~msDe2V_ zvYJ@g?8?2$48Vvp9B}#XCXxJ4lNcGoiWE&Q4l|yR?w|AN`K3G03Vf&9T=x%@oWMMA zt0>~ZE0B_Uj7`13h}Ik(7}p?}$z#{QQ`?%{V^2YLn1iw$8Fy6?KI8<%H@Z@59aQPl zQ~sFYY#u~K0P=pGy+k{M`$gf3DZKZP?8TZgW62@9AQs?U!uV~x} z7%{~4ya(o=j|eaMoU;8YVecmxSe&o#NVOqkELlISl!i0TUuthPV+x^r-TP4?{#=V1 zE-cQHw;MnT5W~-Rju10==}U;n+)Ng7)&^&F9b~rw%fQ|cP%@Lvi30Sjt4?$x zJOwY=3pan+({km2!7>X_GSUkzn>LO)y6tRzi3}=ooh$8o+0CpH!=fOS);jB%0`*67 zmL-~BbU^?{r*MFNici&_ouub7-tI}T8=x#du>BSG5+l=%4WZ`TP2d&YKVw2 z<}HT#LY9Q~X{O55otj+lhQxRZq$X#3Piv1}wkk8&ds}z;j{j^{EV_XW6Th~3SJc@n zV-+s{X-3a$%SENMZ#CA+yX556M7dE8YS2#@5SPcQGZ;IPsVrPHtFqtVCOgMBf7G=O zI#XNfqZ^!$9P%~5Z> zN-L?)-aY#RiT4qA67~+bQ_ieBz$WVJ7Ga{=Zx2+vsyD8Hi?N>DQ#bH(g?%H15;8vv zg}F`aMCUrzvo)E3pQmdB$<37iiRx^j`~l?lWdq*1eCSpLN#%nhmKJAyQO}%}2M@^e zNZA^s+Fw|tmbbWJ+b>YrJ(e6=2aSm_kUh94@N<-2=u9~WX1z)73(-SP=vY~`SNx@*c~b;DO?scHG(GtGP@S?e%GiA+HEB}R>!_L|>;MN{ZY=7-gN)G)D}kCf%; zj79pl^s0WaZkLV&Zof@;NOUNd0zJ>hd$*sOppJ7KbCBW=S0_>FyDTogdJ~0&DR{sm1SR! zccrYqm=;8W-*MSW5WIitNfupagt|kC`FekrzZFSN%YO7_d5mOM{EbffX%m_4iy*!0 zd!35cZ~2p9#k-yY*|VKUTlhGD~jiL z1fMbMhQ9dsfkxPz0x0{Sd37_Sk8D&@@lPr!4TPNYBk8Wkb2))v5e5=bpQedPJuxhj!WQzhje&?GXs}2fDy$x62qvVy@g-O)B1miPlH}o+*tyv@U zjdX7%;ObJ8A?j56B>Cq&@?WMf+b+SC6lzPM%-MlGh?^FGn^57!)Q%J$4)sO(10jJD zd3Z@cO0$6xP^<~RyvlPLDw6UzU9WEb=Zk9QU0U~$TM`Czwi$$^E=}4nTS4r2Su&=> zslD1UHu@~l!0>GOGk?WBkES$1(!jWr2$rI_X1l4Qp89}EWRhQH228wHb2fUWkDE=b zzwF6}gMmgpS3v8HCJ!$#y;~i++feZ0PSBmbj=uj9> zY)R0hpsT?+9WhT5xG}AK($Wkw7%R{1{0=Ly$A!lXo35>DOdg{>(D|>@-jcIT_G=p+ z3%tu|EYYh?5-$U)<87HqcklOB(vYTOUx8#%XoG%f;xX@x1zo-emsyUjeLNaAwtU{Z z+R`GR+4F8kSw#YmNOh{?NWA_oO3yg)n{a@BLk_E3v93y;_55Ca$!P=I%uPA64tc`V z)PS*6txXquh@e?rD*Y10E@=Kk!<_gE zFa{EDq{~~upk^36Q(L<0vLz-a=C7S+VM?gCqIemqTB8p8%N&)U3A^><+8CWCy0b|$ zi*(B{@(f&Dij$V-D93Zy5`6NNmLyy%5lp*=TE;MnbW*>J9x~7JIQ5VQ1f*w?Q(yl* zkyycj0ZM~J9?4fC#8{%)4)ZCd;>d&5#w655lY8sfVqlUbm!(U2#gfTY5; zj$h?siy1gF7?1mDmMNyNyGIEu9BJaAc?zM2Mdw27w^OTTBAlXSW!U1aX4+J3Z^KI8 zd8FHDA>f<4h-Xzyc4zL@w+bll&Cz6`kA(YDm<6d|FEw}mlq%(_ntQP`;$xdQdCb5G z8p`4t&A1Dj%wu2b2sUu>PbsX z1-@v5ep-d0)EV<0gLaB&fAJ{$fTI!SEo*h6j6VkZN}t|;wVa!n0Ko%eL?1K|WVlOn z@Wte&onnSqYie#ld$zz)Jd$9?YdeJ|>#Iy^rIam~;|Saig4dy`$8(Fo_dGKMd-aDR zR?lU)cwz4Lhct7BKqOvbkTT)iwP+>WJb;l8S54Q(D#njV00*SkP(wRC!tpWvr=o09 z(-sB#&!cK=c_=Tq1i^>W_v5NOnlYOUGY%vTGmh0eg6=7l(LD1Km^l%oLdVV=?(j!s*PwtPr z6CS(0k@sb;oYHgO)RZzKl~5DjHw^(_?#hSG^_+6-{9t6Q;5hNIS=}#(*VxIYiu}=s)iT&^hRiDq!lU6ies+s|? zfLom1Pez6yeF1oew=tPCh;?`Lrpn}{W89RZ-9q#Bt)-X6Lt5bNbM($Jk}3!&9!lWL zY=`vRrJ+^PeDTxuez>1t#X)}XJl$eZTUS9)J5*Krd~Ja1H0r;yp5#x~>zCn$sN?&R z^$ByM|1JYqXYqk4N|)`}pE>vwe8sg|soe1f9e(zBt&iJ;{yK3&jCbTAtsOyx4Pvnd zJON!f6?MU0{xQxI{sA*uHcP7>u;~&$>_`Fn1U|$KTGOTGOw2!c*Zc%6s7SkGa1(WY z9~&x{I9a*suef$uq_&V$=aKz^QU`9RsE^*M=5cazIx6+XUy0FNR zM7{<2Z-~T_l&k!ipcaQor#8wY#lYlp6i8F z#{<7ut6urFDf?b46d>C;<+OgIsDS+I>n$^eMeMpT}LN%^d4A8&(LEc65OEWnu zF*<)_Gqt8n)&sIVK-tFhJdC=#G(#OsJtJL5idkP4Ogh(jSpD33oO0M6JlG#~6~Rgy zM&@yibNxEY^6|~KMegS=7Yw*K#n>mwYx}>r-r1i)R4h(YJKgaCouazQ7A*+~-7%Rq z+Oyjf0JYD12I(#rLqJ95WQ_-n%r9j)=^WsR+k6`63HH*wMa!pK6nT@6#<@6ls9)w1 z2OhXSLt&Y-r(5X{K@abyJc9Vx9pAmh5`Q2gO=Ki}-#()MUszA>AFStP1#Z+ov7Yc4 z$_4WNkKMjwfE$)Il6G^3&1h@!Otx3g@#tk~Q*!yV)rWE_Kl40`sk+)Fv=)^r#tZ8m z4{R|_DX)vu-qfY*#qYCx>o5*?-1YLBir;nmMIcwVrdbfRcT29OJE;KD7jPyX%{z zyl0W;>*)iV#&opCM97|JM=W z`R9n}2LF&;>|E8&QuA9OJ2ZJ3q4&=A;aOev7u6{fIGx?-5Si)Cv~70Wt4os&mk`9g z&z3E37}7U2#$O|PiJ1O+erc4OMMNr~YxZ=p*c}gPq~VLm7eQ0hwI%Ms7ofJQByBs_ z9u4WUx9NkhYXZA5T^Q^O&<6je8NX2=#JGhJn~Fr9ZtGmxcSz_If2$6?SyTCM>m$jUKtDS`c&A zUEUnSMGHI>xjLUMFw4YGh&An<;)tPavs6xt=q=d%SRYwBTM4`ZCa-Y_(Om?3O;uWX z)@tq_q6yyGyb}>-a^_9Tete!SVLsfMG^o0doo+O2by4llnH29VPREnPqGt_%b-Y7% z78qiFpTW8q7nHxpa*Do`Ra+C>vw_N$6+4}E{0iB%2RuM|gnF%5AXa?*YQ7UtfWR<7 zVu31;)HbeL-sEJNeJF4lF^P5qN5zjjqqci@(wpS5;HF6;GN+eyk!r6J;;w$osMR=> zLFs?~IlxQPP^TN6{woej$qzm6$0>m?Gfzr8KV;X}kRt$?dCO0}-V|-%reI@LSF4m{ zdL6ME`tly`W9)@cDh=DR;m~i-^z;Rll3)Vn{GjHe##bV!#rLjM}v-L-UBg7s| z)wi2j-~EnSdN~2UwWW3gCMbrL9=`vKQAM}EAZrdBF=I)#JU9V^0nj5+u*$~R7ddeA zk|o^yisyX>N|0gx-S*VdBLcL=hNSgb#2F);ivK+D75GG#!$sXT#765t7aj&LhE{}Z zc6Yj|u((;?7?1gRRqVOolndl1sb`xct{8as2RpINhndl>oE-y+Ebn4c`ezimsW~Z; zTg3y{i2?)VA84d{IkHajVZhf#L1V^k77qQ4Z5)P+KltYkz&FVOmbRztWUJJUTTQ+b zraNT5s{GRM?I_G)t1{rcK>ztjyv&TVw#h|x=R1U#c&4BRug2G62E0>s)T|csD9)>v~Fk^dWyBK^x2CUfjZpI*bzZ1vrX-<$Ce{ z0oWJKx*}$PSL0Pq-e|is;~~0Y>cIA{| z`}F!qDd6gsHpqc^dlLGBvO!o(0Ep$f5e~e4b|fG$zdirajwsgnr;<7qsq~QU9$&WQ zT^8O402-MZQDS4hi;%8ifcxK%f3>>BQ*D~IaCB>HHYjQkMt&Ia{20BW6OVRS;FHH~ zvDUP?0?clzYgLd3hZ$*>Hp5KsI}PXPX7`H&;)_u88*Vu*g+a<2{G+E~&-BL`%Nd>Y z%b15A6`O~4o3e-=o1AFDo3RYN;1xxX(cuZx`$CViV#xJKlMa^tT$L7HVSFq<5UoXA zv^~Z~FpJ9_j~QK@cS+n2O*GhCK%Q7*cF4rYN3)ck>4Kd2^CIl%&%R=dF1j z_()Ww{}L|#HLrnv-~Odv#L2^ZP7!w4+nd@aqpvhV&u)|;Fvn|YLkMFX*A2l%b`+& z>LHX-0n?|O7O9WhD2=sn$jkLthjevx6itf#uwo}!h6IxQ>KA?$U2WV21`gMLT z?1_{sEBlfHcaEF9lj-$l)=KC5c4EPB{!z@($3JOch?rs)aYC4=!pAY!uetHb8CMJZ zQ~vR@Om?y;ktZkE%YvNM%fB+NS$eWyWC{uW;pwIeqs(|ZH#hjodv@ehLFnmMcBv>I zhxsA;ryw7a9qE>f2b2zoQqd^0Kof$WYF^6S9yZ|p?=1bbm*Hd$BXIQs+FHG*hq^!H zZ0=AY$JJecaJ!=hA~1nSm*Afz7=iOR&8L4OPb8*6%VN-WN`%R1J(E>YbIja!1VGZ2 znI$&2#K1Vo2J)aTgT6&~ghfGF>vFq1NV6j3a6lM%Zay?U&%{afyuHPH3DZi!6{?RL z8h2(HZ&>th-uQJlxbURzs-LTRMUF25(l0KyNJIEXy)UvvV3jT)}m2onQ}Wz z&nM0rCj_pJr< zZaoM+KUfIXCzjncw^fe{%|N8%Y8u`gc?)_x`?p^bPjpc0#1+U5y{~HvFh1@j^~4@R zIw9I*V4R4+xt-SfC>>FK5}40|X!Ne9Z@}sNMGs8O_OMDZzF+L;^xFApBf&pyeU||C~@tk^hpw>43if6WYZt#e4c#{*k15Aj7!@*7P2AaP{rW z0~1bx*y=H%%Jjs38>v%YNei=rsN;TFB-|fTDd>M^%Ae$72arL&S!X;m$gUPioKHyTzuNBq#RChEkL4<}vG zRU4qi-ki4a#AG0z{;H_$;^!~Df-|ZEh6(FGG$2J2cZjm>f55Iu0i=0T9*PL{M4tlgYrm|YP1a5o1%KCXE?a#Jz?)IgMNCj9}!w`xKmoQ7eQBF zmJ7`^L+EkDXcsG{9S>`rs$XiO1&!x0p>v(@jG!#`-*S6fv|VoFtc0T zip_$j0j*||W30XRS0=IY(;R%5j#RXjvinHw^RTc#6Yd92L2ZIK=l?=7JpLljJk5PM z0x-!mQ6+p-kUQ=^L=|PE#+9K|10)Px7Y8^DP}(iIT@LjZX2seG#xQg4;~sm&1Rqi* z?MSs1E}5gC;FnRw?11t0)p$VKf*niKneI@l_oyWMB>8}71s2gGGxbJ1=qt=xCHHLk zM@m@Vcxv*lVt>| z@1w3+bfBofw4RVxb2P>)sz4UkMd8 zgfkv?29;2FH0dS93NGO6D21@H9S)UR?rq&R#jUVawB3EtCl0hltcw!b6R-Al#Nhh6 zi-vPhLz6XY$pRv|S>*aKz3=r1qXj-GzNAw=5!|hCVi@Jlj)m+qu9Z~apS#=w;t@65p z51QUyS(N}_gOC*ypcYFT6hmH`u5bQ!6gm0%_MX(@w(484jw!<*sVRKO68b6| z&6mp2{MZp4X8|R~Orc>Zn!RViD}9fkz*3vYaPHlv)wkdyrij>+c=_w)1*nPo#sGVI zMvxqWE$mHCNk!9Y=HnfRSpYM}sZ>hY#zlzHmQ8iMBMc!w%b}0cktZb6er#%32ZX;Z0Cqe0 z%}g5GI%iqH8)jeO^hgM#Y%o?4ac?~qBknY(C*u0QLY4ooP_;78;Q}PeWC@0qPDfZy z%n{gdjcn+;ne9j|8Nw}&K zw)MCLQWNH9Exqzz8|1pKRci-cSdquuWI_u3sqtE!kB}|WQaLPAfys`AGn|&3wK7w{ zB2n4Q%J6g=SEGuc+$E$@*KiQVm%Os*OIblIvaHX_I;nTfKylWg{62@=<$f9-AI$(u zSC;qF-g4~-@4-zp)??7wS@%w+v)#Esp>P}Gu}gdPs;}c$e!Z8XJETmc9gjrVh8%}; z8jzDi7N>fyn-eZ~bi9#!9|pFSPN^hEmBl2JrlD(KQ`y;85PhEr=F)@K$~*KzVmjuz zkzMrbT877f7MA(gwKkVq>EOKcWvCz=vT%&52gHZ**`ph*o#vYzlFGK>UYPwuvn*A4 zm%6yE<_|Ip^oGaZWmie};XN_1@1JL@>Sd(|@&N3W}^9PtqA(+#LpCCp0hh$<@^ zN{sMt#~RK>w;p7(Fw}Z^cewsT#{Izcmsmct7@wY3nFc0P2_p#nM`}M(A%rcU=5!RV zextV^xP>By2}qg(-|I%gR>j>1RY{c3l1T{-H4e=Eu+}Mz*N7BK%X$xzA@&VK!A-4S zNww4@^8B24b_l4y?WgT&)4iGJiJBNlO|yMG)SQ`FxAo!;Y2P4`V){N~!zebk+zn9u z!Q7N9yjsR=f<+zlv%IX25%NE@k;AgDhfW|G(UlIzS|e-J*yh|Gr$G-1Vn-2Fx1Unn z&L}K(2?WhT?)Mm?snIL8@O(v23~nFY64U?voL+7Fs~IH%7^^uY)ST2=}9vU8W1Tm!sgSXC0G`{@j1uF*pS&&Ho^B_N=cIZ7- zv^}kRJZyY;rF1Le6P9z*s2RonwzQLAfE){8D+9$=OIX_8T`#-VE4TJ+%>%>N5E~7V z+M!1&@CKNE4|c0mp32*f?Nza~UgM941UO>dWCpzqDpsjL7a1Q8p9MZ*4mWl5o1L@i$xUMi660Dw}DSwC(d&cWK5)q zODsnHz$({2CZfNVCxo5;^o#8wz{*51epnGRd=f0E(%*ZXG8z&~VmsywDBlSa{FSOh zfZ`i(3RLm&i#6oeo_ z3QK|oOK_J2LU8vYxI=IY?(P&$aDoRZoWcvY!m6+-e$zc~|EFh7&-b@Kv1+k+9_rkC z_u2QzK1~L?si?|XwddYXI3xR`pTe>hJkqQ`!n=vs$9QV?-(~fX{7?YP0ots#MpG;k z@6O^p$C~#lEK6(+maI^*;<@|ZnA`2;J>%wmerO1lpDPLfdR+doa=l_N3Ch@Io&Oeo zWh=z}9mz~CEk|4oD9rq0Lgwob`eV55?+3YeM(RuwKt1&Vc-y}?jr_=m)e0HPL5p_p zLlr)t+U)-2_y5NagA@-ze>Ex!={o8#BSP$dZ3_F}T2Le&TIJZl4}la_XEg#;X>BLX z|HE5UY#zdcIlD#FIVNm={L~*G9`hfT=|6wST71ZNznfO&eTPbWTk<6TM}z%;U3>rG z84^wJQ9ClEB-aGB+PDxSp8xv`P|xsn2!F7by3D`Y{XabOKmA{uln($<5*5aT`l03D z`G2(NzkIhru7Au+Pa%!6auv4!zozy7(+f}!jMshGvqi$Mf>1yHdqMtBCL;M?+w#8` zV^47bS%p^Pqj@W`TcH47~Y3~ z`#KVx9vu~e)b@eg+$c7(V)B1Sn}3JG%!zP9Y1(+@s~3akNdzcP&_%!wgW2Rwy;)(sklxL z9%3KsxJUHYSwklu-}pH0d>}gbTB0$>RwzpCelD{Tm}DsEx(E-s9>2X<9;p+BP5F7B zZMXv806LI~LzPSK&C>`4w4h64HBzmI-ZG7z9}OxPCUT9AI7cE_7agLt&$>+npK!3) z$h8~q!VFVFmD^WxG93Ft3;HOh<5-45_%=%R2p*MY4J~BYrV14(8qIV+zFRR%Tvb)@ly}UoyA`%oRv3O?bLL^_-lUZ0D1fi4sJ0 zYH5D>)9m_PliOZy!Pfl8Z$>N`ql;heX5+`zTQEZq;VA=u8FHTlz) zecc5tC+%!;RGP%g+Ev0=05!*Yxg|64rf%xcDg}00Y-r1IXk{-Ozlh4cLCVMMGyxQ^ z%B*eZMCvvkt37`E5@EKq>@uI7-E=yEuaNzv^oZjbwcA-JEX@0v0K^E)!Y+dGi`lF{ zgjb%}m*w``PC{Su8+c321y_EkuC|-z)I77rocwqFco+T5@*5BRAB|tFttqAp20fZ; zS4Uy_!M|y3+stjM(q2na8M+nBna&jCU!J}`E9kgGF4TRW={53?9@nv~us{`m8TCao ze1mA265?@_X_j?YGZVK^w!@=~Pt1=ehm2aHOX&jI$JZ@y!-BdMw3~G9nWbLvGL7`4hnlgxRYg2cw)v zs>b$VO@!M%g>;q|P*+#*iXO^~ZKu9L2ZeOKHId4qE>|Oworq49m(*xw8M&z_^H#7} z)1X|YsK1caX^7RK%GaMU=kU=s)O}xHTw`!()d+XrdGK9NAjdSXtv1dNaRihb>J7BN z>popOMTxEKsc~}~a<4mmIC>YJiK=bcr_d_ew7ja?SIQ1*cl=K#H z$hDUI$@P&{jer8X|I^536O|#K>(XVB=Sw|t`nr3A>Lb9^qtpIg@swAP;mWcDhLBq~ zAgB5x(Di|9VIAk$TQz%sQI{r8U8G}^fMlf3M*N3O3Mt+IhptN3e0hq8EVx4MY$#Nf zt@AO3bSM)*wkPlWLoW$e&;-xo+wuyhKh=M>ke1<;92Te7fWl3>lMcP>uds!fcvtC^|DcxgPw!C^V0Y;r}j?_HI7HXbQbe!?MGjlryn{R!gfDbT3@WtwdwKOuAcLm^)R0|d+SvraF`&6a63I{Yu7 z<8CyebXDMdzQK(F_ZNFYd`1C~i@i-v7&&wkPyVD~dk#)}3e^2gCJu(+q zW&>mPuB2{Uv@q5bly#(GpYOgyB7;lY3Nt)=DR8)RzS}gtug>XI94@^0m2X=nKBQd> zLrFSlL9J}YjJ|_bfM%uhx|HFSm`3ub<{;bD%Kc6+l*juv@|`0^Sa7d7Vra z8>Wu6U(G`fn*VX|A~sMS4(?`7he|lqCv$1UWkKMl%*8R}`CYNjS5wC@XINluEimOY z(DSktfjgv+OmKfDSXcgxl6V%g5b4g>b>E zB9q?7lzY~T+WKkMI$R0F9_~1USEHVcX_w-uYlFBQodcdCd;YHX-j}$&6c*mSB%@>Q zOKU+ujIH?yK5#@5WOM8^ue>Fo!$S zq=Yc>0xlUq19zl!?%w;Mu#rUs{;;lXrRXmP^Tz2?-G%%)%S*EgfZGUYMr|QVLI(Ve z;g|m=Myt;^VyI%~s-Ai_R}OTdwbeA&dSu!2(4@WI3%MMe*~}LUs&~r<{{3)KJqFRS zBCog8j;$JcAqoO5HY zEqA_k`wjBc;+Tqjy#~{O@7OxYYtN^5rhUU{{3Fg$NrdCY>zrXI*{R4U{BS>Vn8#VRE`u?=t8Ygq1Cg-Tfq^nTRjUIY4xEH5S z*{rVs{efei?$W9~W|Ga$cz4{Noeya~Prnp*u{NvTRHyT3&7_xpL$M|nNOMHVskhN5 zc;=cM-o!f}g$Kk_{;i`m_T=^=#%vjVIrC;TxI+0?hfxgT`lv(yzT=W%gkY-*2&uvt zN?6%i%a^lFed`j@C01ticPxAC{XL}_F94cxbi3(LD3}7?oDbyFL~=hsd{R$Gx51S}%)1WVIDTkkAc<%6Dz8y*3|K?)d5O-G>;o14bRBD~R0e=m7 z6E1MQwQ;%FjAM=FT<7dBToH8x$&`0K+yrK*{Ba8B97TQZ*OhB`Sk;#n zF{yKj|0JF2^Cf&}I@eoQ4wqE)1Y$${s2RsK)&?RE;+7p?Cnj9D5hV*JUuVoBN4Q0f zsD;~E5Uj0yN(^gY`?}D}<@+hGt3PV~j>E!wAXVPG5px?hs6kP+j%oT>>XRKqck3Ej z+p3^0ozj$gmEXvcJaXygkz7VeAHO>~Z|pC`$sy;e7;*&U zaodvD&v{#+H*cr&Gzet7c#H4S+PFdrL#)kZM6)8iIUL8aRd*-m&kc`;H1-CiIiIt_qk5GkH_}A zDi)rYOR1mt13l)?hXO;*GHnjR@$k;1a?Y{LwN~#vZS0v4yOw*2=G0s)lVdrD&Ubf2 z0;`N{mGgDN3k1|2*53q3bXIOC4YSsXPixS^*U8e&?4U|!D^g0D=1!MFu}5}PzdTd5C$|daOvMIFxXzjF zd3fn~ze}-nWE0u7%gp-epN?fcE@ zm~Dv{U*t>Yc0%T^>-i7 zt4GVmh+P5D-CQ9x+ep?1#VIe=5%k+jxT-bI4!7vh8xE%>^%^^}KD}KL+#zD12!NNyQh^Km3aIb6lv)rGwx8C1yNPs(mSx1_pj0R(nq%1Aw z;IrYC1TTd2W#qS7>B?qqfAQ*MvF6@RbkaaKWw|I<+WAQS^vcG@*&8W0jDzF8YH9zY zzX!}Ct(a2jhu*>FYzY6PS=wKgja~@btrXbHrWHM^U&*zKjpywHO}wvrgMNU-D%k|Y zV{8GT7bt?z+hOGuKB82Z;r1m+WG0xwv+twVse2M;K$4hn{iJsD^@-pTMIGqU@CTQQ!)GJPl%qJLNg*TK4TvjOtdxbN4|M8G(b7Lm?b%7V)- znZD!r0Zo&rCA?)*vUlnidv7Vnrt&xQ$D5<`U5DbQp563M9U0P9WkiyoosWV1J9mJv zj#}fbG3%I)Ok3cdhrZIi0oZ$(ucGy=D}qhs%c@>%Q4=r3t{cahj~d{R|HiT)meaKX zNZ?2oVc&8v!EzQ?8+ov8iM(vCnPqLVg0cX@$T;K^ykTlrgN-aI4kMiT+S3R-Knb#Ak2X~3RZe^&zdrc15gtz8pB_BA8j2n-?I1BH-g!^ zgR64%GVKLwjtB~UTKO^serW_fxhOX_RwD?^{^ZhyiaUuo$2787pc<~Cj8T@1I+ylA9}?eY_VuqejwV>#K)7EAAq z=?QPy^<}a8^vYtLRcc<8@BRX5WFl z3mio~)i!&08~gjwk=JdxYq;Wel{9`!ywMWVd5A5&K$T@5k-OO6aW6E>ib#R^-@`Td zANL1Ix}?x%U%p81=s+XAlQdXi;uPi$4SF#+CVrnGTrw|!{=2LqJ%*S}RmwC5yL z$-g%gw=mnw?^FVgc1&aCnw<;eH-0@)dp~XSGr?{nP`_vPAm8HZx6Npdbg;!o7CN`w z#ZL??Xc!l6J{4h1Q|zJf8-E>1t$G^vVY(2rgP6(fp!<{C&sr6$ZwFMdy#jAwK!?UY^ftDY|Adx{JKWo679BQs9PpA8sJ-3-$sQOdu4_l!tcuy#E zD_Wet?{S9YF$BCkj#Nc8^Z*@GT(j;E#lO_m*Zg9ybo4^Qr(bI41(_6bv1#^>U(uF3 zY@`$W-Prr{Vy_o5NE`7+AwpRZE&+1%R3%~qo<=d@geq7anDAId!~TaQZl$XNi(unaQETbb=7PxbE_;g=)@cWr(mG z>t&hjSMC7wq&nct!LKB@Mpo+wUN!*c96NGAB%*>(xLf1fw7%ua;`gWF77Oj)CZD*S zAuVxQ>}gK|d^&3_E9%XDD}TGL8jlxv;Vz{`&3ZGMB=Z95RY9_-KU)#b!PjotmCV$; zrEk4(IjjYibPqbwpByjk$C29bb-@hPf?6zNGSu8*-me?E3>%-{kPb3yE#dDVd^@$)FpONq^w(buJgp?>$P9?G1w_I|!P&jI| zjdg15`y=f{oAmlwq|w6UDVHc|UOFx1_p*!X4+~1oX74i5oW>_a?6c8k9{bBg5K=10(A!tN zMO-EL)y8%`dR(rwgo8u&IDSWV52(DzY#bxf`?Fzh=KZa?t5`Li@RyrUY@# z22A^)40+MrOD~S^c5ZI5ejZ1&_-36HF%vzew7VnXHEhblalhq^Qg)(grNa2c_9klz zT@p7qJ?6Cqd}#lOToUOxD#WW&_CwjGP9#G7#;s(CSSk+$IhDmd|J&|(|09~_s4=}Z zEXD=L7c!!;gXRj7YI+rPeCvN8IKZv4|M>e1^4IS-R4oTb-pBSZ5aE$TI`QEQyQxg7 z1_2Y%Jbpkja6uzqB)QZ=Q_bFQ39Uefu8EJ=q!2HhOThzfVVT$#%Lxzo zHRV}<6$aLk-?OWCZ398L`ytCC%?Bvi5a`R^cgs|+il-ySUTgTC#>SW&Xxa8I!F3Ua zxRc@ZxCsyO%6625HEeU&$E7%9c=7ugl++ATlO(-QowtyEuNCen^ zlPYgLCW+`KRVAm^Bs_f5$SFyKaFH_{xcDKudQo`)GeHBg&5;Dg^mfk@t9pfD%$N9sVZJt zOZ#;kb|^Jegs+;-7oPHTNT#Gb$GR$Wf(iLAi&{IZ))AXn(VW0MN*PAtA|tWVm>}T~3($G8j7HSx(cMztpDG3|@D z*#yR7+KAP^e4@qH^+}bj%saTeeqxmNq@Bfb;f-?+LbDz4T?sI@UQu&G^Wh13{3j2?Scf+c#|Zl3XI`Mh(-uRQg?#(G7OqeZO|hR>ai2o{ zEvMe$icGXzFl^Obqu1kPgvXug8YltG zBlH&%EgYUrf5<3lJbX7ySMDK*Yy4-cjCsB-akWZ-sA5WntVOntp9~xIYfKMNw@>aD z3T)SnGZ^;6nP;oDNcJ(UA?hMZU0$jRx8zD+E=U)@B8(zD z{!=WT7pvi%vqY8AfI9D)Z$0p7eHZc(#$svt@4?LNTq3zv(@m zdxp1htbV+r(;U((^Ig?i(#w6<*gv(DJl~T<>OqRQ#qrz49bZKI#Y9Bx0-kY6aH|xFvTD0x7 zV@WCCKKJtTvBl+(up4qf{^@V@yis^SD;787VZ~V63j~W*u$X~8Ds`8dWavHAHsWfJ ziN!oR+Wpp+zTDP*j=ns+!1d}n&Nb2Ve=?Y8 z09gtYC(5P^^m;%k0Uw8sH@}P_KUWwev;kweD10iBiU=|E>j~ z@&1`%2n#gIJkW-CfXvac5gQ#bNK5tYUUK|VF9R-|V%YVKg|RPgb{&>SGA(xp*A^dZ z=U{te{Vd5T)+K2L3=M>cYhe1rAt$cGu;qG{!2->J-F}PymIOSn2~(VtBwqfhzZz1A z5d5y!k1bVJ{3UXG>;%6v7nBlr2U&0FP(7NXG8iG5mFC&E#rSQ;fZHQ+M<<1X9_P-~ z0Ye-k$ERDbpPjY&Bb}TsuL`fl8EfA->ZSvb(_WBsNhWEVe|VQ>WX8%Ck!(Rf+8^xo zo}y}S<2jI%+Ix>P6lREDZT2eE!89FW)+>ztl?eXY&rS?`hZlo8o2e8H(F(&j5ND&V ztV*-$%zP$x6nf1{<=)%D=8ns-CvaFkDt_DkU#shE;$~k zdxzm|Is=ggoRK%aI{lq(7w8q`wYFy5)J9KTS7KkwoaXAk|PbhQ0D zg;1ym7*Qudh+&SKA?XqGgT7|LMjyc0w6Me0lF%QB?aOWW@^bi+HN;Nz@K2hD~2RcJEOD&1ez-gf&TA^=rdDW2f+yxBsGL^cZCzP>N`V%8!6C!_C zD~N6EB!J>AzbWpsLL|g);A0(-Tm5>1Y#Gwduu+a(;KIh!oil{wwzy5zp-#_Z%(40x z!DH*GNlyfa`I)*T?fkKZCHu9)KkNe^<@YXpC50=4Ptwct$pYdnWLd8U)(h;Vm#{JZ z03MF|3;FF3`p)A*Lccw9To_6l@G`K6ifIa3kiGGey(RUYqHp?H#4R|WE#$HmacddtRE?lYSKs^wmOh_Tr=px0vR z?e~Aq_fq}3Z7mLq&)Q0)BQw5b@0{aS8b3s@o52Xm^R2dv?2b+q9Nl>G;`{PRUOjd* zbW~B&-fJKK$U&8$X}I=g(wYo1FGQv++fLtszr=8+Ct$-J;ToPbSBWXwUBHN2$qUJW z{uUyna=41T68db3X{nee2Yw#_<71V4OZbAHP2M8N+>J`$PsqY+E<2_9pr6FDGwM@j zu_#udfaD4(4CHEGGyg+c+PJNj*LVzTk}XACF7heA#yJGP^b3E&Ib>vvJ(1@BxHq@g zAeje^)<#EvV!Ag-{dIi5lz%z@+p<$$aNi;+cVy>1SsxMJqff5O*F{SNSE&kX_+0!) zogUY-xkNE#d=+Bl_)7M8E_>(9e!SY3J|3v#xlJj2#8p4@1bvx!>EafI(`AKQFIhON zXeKj1ViuSsRhA;3vsgK+h`Gkwd31g5ifTX{stg-FE-|chsXD2tUn0N`cRpU?*Ra4Z z9pRZsj1wB8plr!%w4Fe_`y)*v13qa4>A|R+8Tkb{d$iJB3XimG{c(rN>>w=A!Ne~4#Vm=~~4|8^IdHkr#QCku)ErYy%9bh-nc z1UWVwvHjuYSZFyBEqUMyJ#AEJnpNnSX$#`%$iQ%#my@AG;bZa-)wA-HP|`nQktVoZ z_PB&y+X-|CUr z%&aWS$^u5~{^0_tLcJg3d9cFBpM3hgX+Z94&eMcxVM$RPB+=Nw)@`fkk))0a_2_?q z4k~wGUJ6%Gc3Jk4EaLw%(XV_Lqw&sSv${`bCVDwPB3-w>JYScbunq^;6#T0X&Dv@) z;5n5z4r3Mg?X}O#cQ2v|l%KPNeJo@6k|wI4W^IAafDOR6;5)dxAc4=Qgo z)~kIgc^z9hB;^KsxZ@IlGN(h?Kh&#KFV3s>5WYs)u5;}4yB!ic*z6g~jC=Wzv)aX` z{At^E3##&*2FuI)x$F5bXcFCeBMG?06|>#Mlz7!}@5a5?e;Ckzyqc%AB=a(fB)fJH8zP(s{+d_a6{Ey zxk9s?nrx*nRRl$A5{GWBu|sV*i6q1|=5FPwRA z0c#M3HiGzV^cjPzZLaTV^zap9aJHCa=sbSRp$Pto7-dgew~xjH$B);&R8XZ%y8{o{ zHX>td+r0Agdtyp6aDER;j(&=`W|a%K{3ZHs-mL+322r{lum>m04?&bYbr#3X5IxAt z-Q)|?Ki@**>Sm|p?MGV@Sl_A|pfnIN;==cPNnw?Nwj-JY(MdHtP0(D`pMx}5?7f5A z3W%y?SjCr`uw{Zo4g}4wu|)yJyd3*Watk~YTO8fEo|OR>NxiuK4s_=^OG65J2AU5= z>E${%a1T!!l#Wa?640;o$|)!d)tB4jeP+KWesf}OpC(CN^sH_X`Z)g#0el+s9JKA* zlNrCuVo%0&pd(%AyOOgiKsXG#-rpL%BoXfj$#qTJelvG?8|p4+^!zIcpK9ZiNgRVg z$ENS-Y+YyXn4^eSUOqjfk_gm5sev(H#!}qZ)ql>Wkk--8hK62qw*@ux>E6_YzKB7I zIe{ss1~KNr<9G@H`}$8ToEeO)Uq7y0C}&zbPOlvLDhQTSH(&YjjWD$EpBs|hWL#~) zZ3^))>Z-H=8T=3rLA9T9ezbG;#ayO20h)6)3a_;OBq}%jsm+^#z1^;Ltg~o_3dCq! zx(ofF)Cf_{3p_+xmDah*v?+JRT`-Xv>b;TtgNANNhuW%$gBhPB+(`b5!0jW)-X1{G zU3!2fe`Zf)B(ki9rr|*oNpL_h5~JX!^OzWKR)y@d_jwf~Pkm zY7gpvv|>O&$vr#A#_fIMc1530{VMb#Ybh@%;l(RXdFnNAnvf;t5+yz_t3F?uW>q+_ zY!OLE$5!*#);G3}DcultI<}OA^&74yGN9R_a_v|rzbmP<&`+oLVN0$ zMg+>Y#G`Jx_Oc#wA8g=5zS?u+JDl$vEo|TmVM5WDMm7awE~qn-E}+El+*;HoBDVDu z74tJGgJ>zZ$Li$+CSYB%;v@+`(*w(BT8Ue7Kc|JBt+#A0Ik@Kbs}Zk%y7(8vLt9!+qrIPlte4g`Bc9-!gZ*r%gHhrA;(fZ)v(PkL zCgn!4&K?7zt}`em6Vs?6NqPMt$!(*3N4MS=avHMM`|6h&g2c*RtaWV3$DOu~`_#RU z=s|^nu^-=%2+B(^Bi`w+pDMp*k0fLHz%{yd#quiB5Tw10Br|(LjXYTgbthd^p}*(m zVrg4OP$XzzkD$KGX_S@7-(!O|%nvQVskn881EZzkX58@S-XKb%?Y_6dS!`w3gbWnN zrfM0+t7LryE)|URe*B~A23icHiE35PPjV~fSjU9ZuNwOfTWR;fD-rx{j(o#yky~H6 zeoP<+#VEI625Nai6V92GwtvKTX=eIv->3}lVg5aLT&YR1=D;uOWq+idwI`#?5jem-_2RF~i-I|}ZYyleg9F)8@BjUiKpZ&=Y*12J$x9w>=I z)}8bRlujwg_KSBO?5nZgMqVq`3!{kL`!{B)*8E$JWVUAsZh@*S;r zL2~y^_bnvW@|W7i@LaFKa(4p=D`+kkv{$_Z-oRI2PK@vnOFc+q$rN%Ask2Y7%$uBf zX+zeci$Kyo!AhmwwM*BhC03RTb-Z{88)vpXT7JnFHy{lplrSRxi3)rod}1@mkc_iYZnicaSzgV+kRjhgzP?y&OR-S#;qdP*ZZsxE zFXHrK0mX)5uYJJ##*$fNsdp#Gns4tH_fJLEyZn0infnFKzz=?ra* z1*YwwBq|J@EdC;dQK976cFfHkzHJ)#(pgF)zsF3G20`aO!`7whBh5H1S^qQN3so)4 zOBxbx+v;93Rc-TtnJUm)b=*6^{QAu!bCwIx#&fNcShJg90e47@^J0SrQ@d_$-v{M4 zmGvzy#`5KI@S2Ww&HnB-7qWstF^iK6==yz><+?Dp3&)=nRa_ZHe9a6Yb9~XNzbF~J zZJp$rS17#SK8iERISFl6en`)EJz|V?{beGHH0^nRKegCAhtAkJZ94smw#Bi!t|hq! zl}jbB4Cj#tGk2o&JD*Go4vN@um0zyt&-IBMOHVba%QC+2`rN+4ZgCxw>becg@ho(y zhQQ`2RJp^LZf(4vhc5jhWr=%J#a{&$@=_h&m+7}B7MRFp{`J3Pa5!#PRsZ?KpnSKl zcM+Z2;54qOX6T2+QvhDh=&x~aMW#NM`+ZvX4n0QD{!@5zy*#$X-X@A)o&M&R6MyCS zvIkHKZ^?uDxjnmGx<06Fv9MUW1in7e#x!P^-Fki)jC^TLB`IWCQnH!OM!I;`+VE49 z=6soDb&XPIaWOPp$osCmf$g^_wT&Y%<&)EvuWfyVE9<7%lJbjcYIzZ`;(TRdJZh># zKb zzhdttDTB>72rG05tQ%ZnzGQ@UX%jL@#a`%#xHB*3&y;$5)(JX)pLu5|VX?=;evb_p zcKA#amR$$^wMpjJ|AP0QL&P&e_S$xUwfkqCy(U|?uoL4p=dW|R=iG#u2@evg3EazA z?z-9<1Utk%8+Nf`bteri43GV68*si_&RBP19(_>$Sy##e(CO=Zn7$!S&sr_CueBIB z;G28JRe}AiF%S*>Gaj4FTVnU5f3~|P`V}Zy=(M9s(Oi(X(Bxar!blJ@obN>xX}p}! zL^KU#!K(En2<6O$ZwIp57P)V#dM4Y`ZIHxG5V1jZ-n^G+bAxOea&9rnOX~(4K`S84 z7S>!@j-)pQs^R8bc0*r%*^Xx7Oq1zN&A(RU1-_W0d~EN&7y4;-L%UG_tULPR*?})N zZulgAxif{!%8#x`QSJ8|LjtZ_oSNO`hOBBJRnMARmTEH0ar^1DK8(dcy#>gANSz#%cSU^ zqp*tvpiV;iYi2?$}xbawAPtS=SQO6|&FApc63cTLkXWd3pD zCexZIYia4CO8$2_?NqNN89q$^%QZkwykt*wVNvdMS~I;pZ_CxqFxK|(W^APKw_ucO z<<)fBIT{`8v5lxU-UC>)47{{SYGV+oTWS6&TGH`i##2nD+chM-pjJt|Gh?Y5JY=(c z6Np`LE+N#t(ICR;w3lBbkcH43bJxe-&7=%N&AizG@n$xaD=`XEaS-R`zEvx$cz=;`$H_3qxm zg`R|#hi_f4N$f>AOmxmop<}XR=GJWX;rhR^z9xkbi5&ZrM=U*G^)pF)zTm zIU*CpWKpZXdP)KDcqXP{rqB<|_eZrd<@-BILO&&vOIHP@Am=n8o5KKzIs7ggT70W3 zbMTQsVK#d~0?fytv$^--HnR=>1Dq;*eT*wB)9LpK3pMIV1G4O?-2$q>@Dcr3P1<;u1P5!PYeN-XX3Kg9{4m zXd!WjP^#ud2N8cC593LNpqo$EfR*}|hU5UU>3qaIvElRXrZ;L8oWwNpRCmvUFyG84 zacm5V4@p}zX%kpT$tn^Q+YO+57F^hR2VO7YSEf4eQ}xl$b5SuhBN{x1eHc6zt!-PT zx3)s0@9o6MUes``;(u-L8t>SrYH%RT{rM(3WWo4e>~=qVt7Ol{CGDgVB4sQKAcQRd zKEz>%@q?$wW2hQMZ>Um$_s6~LUhWWnq3?_N+$oE47r^ph@w~xLsJV{fSIPCDJ(Jtt zuJ)Ap@+1c9&BPH5bAVJ@_FZE4C;aM!bk`RpnSvE23_=Zm#-UXAI)auhUz@<&i=f|}AVY$M;-A;v8muQM z2V!3AIIHYH=#_^j&FJ&`4?!(A1I1lyFY8t^GttLaVUGN;Ypu7abo=gJQvj1MdxKBj z67Bl<-mWO-i2BeWy8n^)P3BXd-x1 z5A$wN91he*jD-8Ns^{#@}81`4@1dHal!%AbsqFW0V($pj{fv|?|jum8&S zCdJYSB}LMIz8YF?p+Rk7T=AmtG3Y+&&D~K=sZ9h(mw49JwRS;i;ayAkQ({%tJDTYG zLz$OV9KAr=#5Hj9EUdZeXvf`n`0Z^v?QB(sPEx&A2UBkD_B?5jlV3KbM`{I!Q`qRH zW`T01v4{4m0YqvahS%$J1r%SRM@D+l2qh;84Cuh(ha9+0lY%D{Zn6AaJbI4dOz3 z8A;9OD}$AeJR8z*6^U7peq4KBOp^@U{oU{KL!(N!UhkaD_~)3OJ4a*tIe$K9A`j}q z;q+!N0Q7Zbb*d}Ivt2qoMwLDC(u;5&K{7qIupG~!HuifoXpC>`T))`M-&ED(KM0GxDZb)ICqEQcKVByZ)cV`gbHzL~)O*5&x3 z9QHpE0Z-ac*VX?u{G90zaU(LOy(YMSI~>;akk6 zT1DE-u5$LY@PfzV>0|%)8zINvMFVZpH^PL0O!S47PUrPD)MsnZ{;gHRy7S&&?Z4nm z2TUKmln%t+qz-+7(Ko!NDGLd%BCucG9kjH4JtlftBl1qej+BGnJepWIxWd(^GKAPJ z5P(*yrD=wV{Ix+mV)=BmvJSf3LXC(TrI4aR2c{dSRn%QdG?hmLAFs|G(egHecc!Y- zp-u{+2{comR6yC^3?46YugCi|Rh%6Ew3~p5xh##gu>@bMYif&H)VMOPb*VsHh8%4F zzP<5&3o_)q0A(hGR+?9QO%x8kWdR;P*-B?6*wHyO7B5nfy_QFI24_ZK3&mH^p$ZJo z&r<`kO|E6;+f?JC|uzVHv`gwr|4unme06jcRx{oLhmv+ z*01$PW_;QdRIvcozzYb#Qqg`H^_5j!VY|6JmKRo&wPSXdbQP!|te}dGnDicAzSw=M zkje(c@zBS$akjc@5C8u?O$IW>JP? z6``a)^eKr%@<#aLI!J5JpQ4;X>eKN|1(Zr>_NsBbM&G`j7;)Qsp}LuS**ph10C&YI zG=w25!w0U$TA1N~;xdZ65dz-L_PN++i*Z*~d*%mjrmz z);fUV-FHlMzw|64)t6e7>-hTR*_TtgJT|4(dE#0CH+K(u&7mLUsECdt_tQIq0 z)aFDrzepY|`4rq%PClH7rL(CM6%K*s=?684$}#;`N4sB&TW_@IS$>_|&DK1JASl)_=jwY|ZobQv@tX(VtO!;S)Ezys zOns`v%D{*=g3OAs{L!Z_v}sszQ!5&un8uU$9eBU?&T&Uw@Et!yRNJz$zQm&A#=(YU zebkd70-JA2nKgp`)8^{YspbBc-(!Ge0xfTKZx#3mTM_Z#3oYtc1C#DsxjjJpO>@&# zSVxn-aQ~=?%|Y2LWi4-}Rw+uP57AzN@Rzcrp(hV}%}sV)DSf>;l3AwvR;+B7Luk75 zlIy$;&on5NvU5edBAAB|NJs}1K)(8ZuCDodud*db+oG5IgGoUJT;*AakA6OY183e> zTzweD?=J^Q{5?2&EJA`d zi?hqL0u?6gTX6i_tm{TSN}AQ~T4ewvP5V-kcK&*~&SG+e?*fEuZ-aQtdKj}Mg2zNG zQub(wC;Q8NosJBB()pKebiZ?GSGik~`{?OWfAUIvKpIs8i>{v+6pkHZgYRH}NWYh$ zXhoNAwA=EkgzRD}&yAnD)bUnY*%3Bj$JLr8-(Q2H8g5)^6tj3`$d2?-?D{$S`Q!b7 zv#r{)aZxxN#YSGqTt9cat)Sq@V!`SlOEa1e9dzO)<&IjLw< z=f+y1)qP{jfJYRD&xLmCzDiJ@e|G&mym(qucWyYDchdXpZ(r$UW^aDH(H3Z(xoX8nD8}OQ8OSt^^5+WQRPK@f!@^0;)-_}mO-|?d6 zv9dY?ZLg&?XiJV+zb_2@w0 z^ookgI~GVlpnvjePtU97qD=|~$r;wQ%bE&Y1fiN&H(8e-mjGY$i-@NUyl=VSe%`jH z9c2AF=`qklPKYIlF^YhA9hW`59oK0&<1* z*VqR}^?@8&#<-H!T{yC_GnmCSsy8B-&Cn-u@6P-D1^(hR0vt{#nZso5N=_8Q|=kl+lI_@9j@cs(cs2<20EFMBy?Os8n~IsPv+}jw|_SDX>I%) zimYF2(n>noobqL}+$A*BQum=hd~c%XLgjKzEIu`JX04jZ7>eNOIyyysOGdPo2gM6@ z!w=`1LoW&I&PgTY;|mtS?Y}m0L&6}-Tb{T2jS^X(dhQj7w8z&94M{` zGvD}X+Ce&fto0BlON$-Dz59l2ih0YYPJ~AGK$>BZqj~NkHgxX*BvjncDgBh29M@00-f ziGjo11Ju(e6~|pdd^_hX(3mm&IXXzC+(w$?2ui`x``nrajv(#wuBAr5_TZ^miY)VI z4fSL6C-uJmwen7P62=WkSc&yBP?jQ*>DEC*E@O@_D&^xu6@4={x#A>S|9gJHnan|a z83rO85OPWvVa=bLzLy!doY8Y=$Eza#>hKfZBNLQ8jSY-11{;`X+bA0p?$7%Pjl5aC zohJSLW{XjVbc}t*t&TuBlu5?KV(GP>v{mQlqOP7+kxe@ zl)J@vjBm~BDhy`03>af8UOSEJ1YR(m<;Y}~PFY+Woqv=#bs^xy4Oz7($Sf*!TO>5A z*ktwb^+ux;oME@Q0S{Mxs^o_TibAa8Vf=0D@~^N_Yx7tb4NYz6XR9dwmW>8-yq@+O?aYsg z4_Nhmk9=0y7>^%{eQ{V--g#v<>-m)yxFq5&c#|X2I9f!=c5DKJy=|$7RiDUJC9z|f ztWp10q9&l6*poayCmRo6=&z3VKUmp;IYxcH^h6V8W3JPoNyr;(v}|11 zA8n0uC3ECo2QMAK&2wEQpE^(SFR849xER-QLlTTW&|I9086!f(&M?}FU%V<8TffOq z52i|N``n%>MKs2bzDJ?EAKAAYItNk#Y;+%0W6DKaIL6@xjz#xg1CKNIGwi?OfIK=A z*{yNz=<->*_R|K$9Qqb64iEd{#S~c10Sv7ggg*&MXhfzlulY44cwk#AKk}uknPx_?^qkk3SDWBX!0-LR2 zI;Bs9%RePTB`U%pQ^3ujxa&Tchyj;H89!oHz|&{5HPoblmeLj{i{b-0NFB zV{Kd}8I0bBkHE*Y1C}O8tu9|J(8j6cnN#^qP#m!4CEVf@W~}AG^>&_Bbjw*ne1iLY zCuz6n+I>PO5Iy2bNfdHaw#&?#z?NWmHDFfFiZ8?BcVKI))Bacd658*8PHGk2Dii^u z;bxzMn!Fa3i>o7D0{D>T{KgdgT*Lv=GS^+bQ+p)+c75eiCk=ac+VEtI^tBU)<{n|~ z_1WU2>$0zDR_mO)8?&nrP3w0TGzkG~&f$>ukvpTi2~pk{Ipotz);h%I?rPrZmEq^B za~3SZ3HUzgQeyyzJ(DHp3Ble?d^xp-6?GkMoCYq8XZR`d9^QMhROVshM@o!+C}iWk z%zO+{+nN;W0;g)rrkeKbonISM*{hH9KHJQ4Gh7`Bo+3kpP6bzD=M=csoY`j;B?V9L z3jwFw0+x5%7jXI$RY0ah@>#S&a6_!>ae^KnjKB4Qrmx&_XMWW}k^k8TKOD!`&93`a zdNawB>W};wnF5abS=uyrKIn`=XbOuJiGNM?2W%vK=8`rU|LhErl zX+MyXsMf_>RTbka_v*^IvG?sU6WL1xDYfN~R&b{(=8wD*RX&gl7(o;JXd2(@&WoDI zH5Fa0t*POf{9w{hcJ)NEc7ex5mH`B{SMOb-037e5X;(v8gbxYu$j z*St{IdwXig&pDnh(#2fE+_>FO^TVc0*39%qEX*{GiN3^=qzg39_L$VWXsW*4BZ@Z3 z%3Ay91gG1Kx$v+nR?hw&{AHu_5IC!{*3pcV5f6#&ewx6S>UqSQkTF7 z6R>r$1w!(8dM10#<1aS9T)Pi@Gi>j?1JYe&Kob+ryL(7G+<~=w`O`Q38R6ZS+^ON= zuhTCDMcBUujZT>ftk_x&Guq<}9ay5>M-_doMw5L%^^)9k!f%qo!*s{Xic()YkT{0z z8}YTXL`)d18Jo+%D0WSV3 z<-I_uly|;mw&V(s53xVud6Ki*$-l$csxbDX2BMBunV`8YVfEt-V4TD!flYCPnJ8P2P3sK4~vvcHbAgb>v1i@GtV zi?#!YG;8Wmc=uLV5XsXD1$%)rMeYaXT%|Z6A>BkD=|eG8_yUn1n{EOfzdXtYjx0WR1!istKhqzK zcV|->6!E-)1Pny4_xIpFz+nh@aHVa^sd>|ufLr*4!QwGB>u{atAa6cmZk<5mz6nF5 z=Hhqi$CIp%L&yN>vyE^@bmn9--dbo94k~f^{@C^!yu~LoP|?2eA19NJm%PkoUx*go z!Kt04gIyL+(0+JZVl{LBJ}L_oXE#__=U}Th33=LvXtx7IsSf= z{J?uil-060=YXd+D10XJhE6H-4Np#;ct63QQa9F~;xt>v0C|g@rQVFaxHampG)>~D z`i%a|sf>e%*uo|i2?4xhbLflSI7%KL7XM{kI|h`WWbo zM)`O{A>#N?58ZKsIPjd7Ri~QWU$6gvhxyl}?!Jv-2k7AYDVF??uf30|$pAcuZvQe2 z^FP1+_uBsF;77FYI1<=7$NsN{paaio=3vUyQ2ln8zc=okXgd;4$Puly_y4gF)g0hC zrn(h|7yq^C|8ru0kJyt8=+*XbvH(f%DD(S=fNz-+faii=kamUqhm5~{7=bHF)R#=O z{eLYa7?H!1S{RU`t-Is0_~198S)X52B=guO8#WIOXSqUA z$i1P8*ZV%T2J_~FA+QSbB)w8Mb+Dq7%hnLDV;ekBw?vFMZX{fO zIs@;F6kgi~U6)9$fy@>MV@+PdM^kI;?Z5#J*Y@4p-^fGLl=zIQ7%U22p6|M_^HBI- zrSzAYd_dm(@$i&O&~9G%pnPjE%MrLnpyvdIOz5Wdt3}BRh0P3Xl&2~`N?|wFUF{U< z=bpz;PzbW9`2T@Y*^dD>Pc+F(FpV{$SC(x|Av%XD9zRCj!~Z&(nTs~k-9 zt%S|7$UxTp`kqM)sEk{_G*Ny)cK;u?|0EXRFF(6Uz2fmF-I*)ShW3dLg~kVX8pChL zD?HLrDEztRfJ9|vdQ9E%m_o`y`Fuq*DCOy1e4^BFeDn)k8G-^;CKD^QH+!&Bkjr;# z;eph$D)kqJo0UGdw)k$Kw(-f?E%V%GQFX3s=#zUbjc68SkvZe@4unz+w8 z!L0O|$Nf_sDiQOnoArE4PKU&(9i9ts$BWfEnYMR;!#aX;q+(BwpK&X#brJF@rSh<~ z>^lv}`Bg|Q8qhlv*xWz8#vb1#l~?%OMWA0kw3EEiTv-cE`1%Paq%Zpk!-L<7v}gq$ zAW&}KPS%igkz=ar;4VV&U4=?0%ATWC4q1b8c`FMI@?#R)Lf^y0`Q?KMyg{2XcLha# z3Tg$a$6uRUy+bZ)S^Ndutt_RnwhfJf`_)oerrn}CVT)`*Io{Lxn!gtu%0Pf_4-%Xs z;y!01%#6HGFySYB8iTD~@PR&NH(q_ahMZWR#>OSDWucP}Lc>vB8M$#fCET(ceyL2~ z%}4N!WaG3Hkzp>t+&JFFC=IhV+~Z&=turkRyHub3fSC>WF1$(-Vc))u?BI7O9jW+I z>~h3~jU&rfV0JH!!~3JPeFeoB6u{|P><9efY`nZnX4#fEIM?Tw!r|;nJXMOm{Zk12 z#*TP0Po1_~pp-7{(B&Mg3*4?ILw=iuIIiv8gjE%#BD7=lN(bYlT_)@rdM`djnAJPM z$j+MWJm&lr1ktX!HVbr&JObkB0#;bWp#sb~L-1nZ=$V^eYdquS)*7{8tZVX%%}vKn z7%?;^R!3XLseH>`!>x#^OZTBv-9p>sA&b9C2Nku=^@TQ?6{!Ul6?p68e7=F?-pAH3 z@?YHj_4R6k{^y59n+3_s998W%NeEzf6b~}HpAFKxb&4Fk4Xx9!tN>0UBfDCeOri>? zKXDLrgpCDV>nB&-_gFCB%s2z_^w%4Hk02jqr;^%d7_O0mTWtRNaHgclf_%_+TjSDw4n#0b z7q0sgOT}4*hq8SPS+I-E`2$W7<;z~4YHh`xGfa{~{nP6`ipDhlj;o!@erl}6;gk95 zMOR~+ne3dlTOmil-POp0BoYkhP7T?0D0z%KAR6SlDsXiaNf@aER6Gx^qa)^;oYuNB z^u-6aC){TXFrljW3wFO(a}yeXLBJoW7SX?N`K6ju`opH23E*gf-$3o)UnEqT774RN zTIWbRK(;n5eGB>mP9_e1chZNfuvL5qeC8VsUG`~oJ128Qn6Ll2l>>pLg?6$IFfA-Qg zkVh{)7`bUH5AHtFat|N=j8W<8By+ude{QW44O@y-sMi5N{v_NDci}klt21sg=5sv*R_quF3pkguyhhk`+nI4VSr3GO3tT z`pk?9@R|%kyP0A%2V;gPE$hY{zk%RnL&x?D@4z0CVXIL4mNW_M1;NQgqZ)gfvqZez zYnJWI?3>Y+t(r{$YvFoEda`x6g&9Q{YyS_9$OHyJck`tP;%`C#h=^K&6kq2>f9X)zJG8+4T@C`so#u&2$rlbLY_w(7R!NzOQ7K$c5n9MG$ySqNgRFT~SDhSy zkvJ;|AUZp)W!J`INHVlwq&}X@YUN2K!rtUQ? zK`qtHXIpM%Eqil%Ttd$x%VkSZAEQB4Td|l_wuZtdXR~hLJJ@_^JTG9`31xMC;LYepQ4KqM` z_O_!C$j>8~Ar?>uEyUwDorNgrVhCS+dw+_#C>ydz7tYEKF7->QClGLowSuwoam$c5 z1k`IhotIv5+v7g-n+P9fihqB>35_ZG21Iqg@glQ9fOE(M*KRwSOB>)GZrhbF37o`> z79MbgHNU?s^}mQn86r#b=-n*CB@DTNFLy^8h%;JoXd2&m?2Ywqa85VR$B{617_SDk zCpb=mgNKJ$YX_vBGiPyMM1K7CbfZA*oV8+}4~x! zfJIu=m0{~x0PjD=oiVRDt8`=EdI*i7|N1B1Sks=IJ}JuLeGd41Vo#*z@FAf1>=lWZ z9L#*K^v9$>-K|x%y;y3G)j`&Ek6BgS7p+NLe)zY@T-K?N-vg_#c&M1Nw{qcz`nDnu zOdDEa*7k}{t?Aou8lD9l^6pmyzH=JYwU4$-ULpI1Or}R{nynO?1fL!l z+4~}ZJ%v8gd&qTXVyWi=6FE(3`kYU3_>_TE_Y6eC7eG}W67$)-ra3+5(Odd8EI8O1 zExeb@LkQI5&}HThXY&~?etm=+roPSQXM3FCd}TZ+O;Wx$7JMttOrrZ(js`=brd(l^ zuQfZznf4^Dx^<_2>5UnbNDxKKE;G9to7{Z_Bwg9%i~1-;l_;+I?drj`zBS6Ja98<4 zMYsryUdVJ0nlKi3ntf~gJbm~}v^;cytRWKYRKA^Zu`MZRH*a!O9AO6@ zZ7!+AFC#nlkxYUK)P0W=Y#-#C&6aoH3C;U%xU3xgZ>u9u~TcgB&#nG+#|@eSgLLwW+DVpGHlejUOKho0R&6 z{aYPz@4?AO!1a8_x9NusvT(SbZmSHs+^vb9^@%3KQy`%r#SYG)&-4eScF=)l$Svr^ zHUNv+QcN>#4!FmD{uj{OLj7C&%vx>s93RdO<0KStYmA#*Je^hrHuV0f+LZaL%NYYM zQ1si(r-KrC2rDqMt{ABsG=?F#*7QI^GLpg=A8a1}5J1k(mDnp2DO2Y8I*H*wWvZExSaO3rok6q7JlbcPjq zQmk93u0}(}0%kjR!D~|vi?@Om!__^!a|MJE#j6CYPl?1$emXJVlP6M(ar?a4D~}HT z)TX0V zHRWCP#W4Fk)u+3TTPO-?$%@d-eGzn239N^=f9DbYrxxE~1Og;3hgT5Xcm>Yj+M$&%bKy0aW>A8vs#)*;QDLL1keViY_}em|+&rM5DF@Er-}QyL61W?@&8lYl9wT zpCrN0iLIqlMx673(sJy4C4i~?UK`QCdmE1Ii@&NZqL#a?qZtL9==^<%58kHVezij~ zn$GGx>~RCZU@1)y0sxK>b*K>ScPkuyvHdpb=%0l9l}^G>L&g?L8Sw^-^7%cUHr&)^ z_-y>TUi2R85dSw(J|L!zKy)QmY6ey%_#XI#dO8(tn>JI6GZX~WeUx{^U?`VZATxGd zO~60N!K>iN|6zz+NCO9gS0i(Ga9Rm0)6{?{F0Gau zB`PdxydDfw)=!J%d&TEimd_CXnil3p7Uu3uKMe#E$>BlEYs%YO`y@<0KMPgRpzaDS znZ8cak!?KATbmWs3Zv2{v+?C^R|pA=!F-==mG0l`%6ktIm4QrJg#O5|e(rN4F8?O7 z)=Gf`l~=C1Nj*@>JNReSM&~hag>gXF4AT^zE=tV!(WDR!(=wiluw&-!;zrV#&Arps z+Tkz*K{6NhWgCZ|V@Jn_a;trJiN9#jjR?H&8($to%J<@JiUN_q@t3y2sQGJt#r-Lf ztfPWw9CqdU$qHO(j}SqsL}4w1=y0^HwkB=rMjzIPwHg&*AI5|92P&Ze)sK_|?vv}?DrJ7!DssYg^I*1jj$|k@2 z;kU`JV%Iy*bm9F9EkhnOJRYTw zU$`+(*6oWBwiHcxQAZ??7Xmu}sR5J`obs!$Un~x;py*^J)Aezf6f^)bmH@fq1M(O=-(rCkq|wXj`x{cq7-Za>(nH00W){mjen1EP!abRdBQZ>>9CpbRg==I?; zV{^>NbM{1U*!9QM_jxw7 z@k|&#VLAQI3j{QL@STQlG<&%`^p0C)$QmD~Sphvb=j{(|ctD-)^b3v1dG_Uod;*EX zc+lV70i!Dm94iaLxc9(8-^1A*XnrxslE%w!en76_TXvPGRd2+PKn`icU^(!v!ZBfR zJ!}{YWBx2=nX#jiY^|FNjX_WN4sd23oftKiq)RWUkFyAUwx=ztE{+_M){M6TjO(C! zhYc?Nug!bhus1T;ATtFVQNNr5dN}0NJBjVB8_1<#1$dphxmx{9a-IAiX`adp_0gMc zoUheo4G13iSydc>>ez^l!#5#g5cC=&c&;2!%Ri#va`s(+f4J(@S&lk60f~(#+MHM4 zhT=5LkIIl*H7vY|f9sKWQq8&{4Thm-036!0tYPEq@zSRu!*Xxprcx1ALMt@{^S9ws zpttm$A|N?FZQ18LTe$tmY*LBf=V6aAs0JKL31|D0ADt@AhQd)oJ%3fcS*pyH9`+df zpF~0qzIGdAGP`e^-38D5@Z_?_9BTHr*5V#E0IYSGfjt;4FZ44IQN4xf-Uja%pU%3_ z*PiVQY71&UoE28yg?;-3VC7;O%bUyw6IB?riJ0(V>c^4g@1=X_(*+ft4?n#1TM$AF zV6gT63M@*)7w|xhZ#hwYjT)T8adV3`J8jo^LNFPn6plKtCO#~sz01Jkne2{BQ3GYfnpWO_{T%~nEAxuE%22C(vtlpiR_L1&*y%9Ju z*0ZAe7Sxiu=rKD~5qb+foi&*kN$bR#6I?JfH*{gNR;CTc=*HqzU>og&Bp#nNh|e>$ z7Db3SR@WkzI4Ce063Y& zbPz7f>epeEq%y2SQ|KN~6d1=Fws*ECq|2>Q72N(|$jw~C-&FdP4@mV33OzXq)PEx1B04c^0sg-DAV9Scd54QArgyNMLM;bZFlr zhi8S7dwBibWVAZ9b}NoC==p5bdiEdc8KSg6I+Y1Ng%OOU+n7GH_I7+yZ=I!K=8eVX zKvo9t$l{r>W&r`<*nQoaSdxg$8n(P^H9$G&uuMFji!qO5Nq9Q-%HDh&$eAoQ-xgJ! znwwCKZj~wwDUte$0Qrl-P>O&}tXZ?XlL0`wV;Hb{1=cS}9KttE`D>@0ECwfaoVt78 zh$cM*9SdzU)ZR4PUMMwcX7GGuN!CM0*==l@wJ)+?@Md^vJ@gbf*RZoLt59E|;BfPj zq7d@)T-0CN`xNx^U4AosdWP)N#|Np&jsLHx|?^cB8~QP+Db-5VZ$C9{i@?!k)}Oz z*D}`(kD2DT{f6D)t5(W+Wo#fmcH06V+3eXqvX~ZaY`%d{Jv!cvFrRS<(|63^Rji7S zK5bd#8uA!kbmst^SeJytnHT7f+|B2TwrcM1tdU34VUnBumF=8i3b-Ml26)`IQJK$o zqn3joEUCEOB0yn#vcA+=)L8mU6Mx`cUdq=CWxO9W|M_1u^g9SQ^ zI2(BojVU8rt#NTzk$p@NNV$YSGtpVUYQ{1D3Q{kf-?pWDE zyr)c$w`V%8*W7LQj>@$S)9l)Nk*A!k$oM#_5XP-OJbMR@6~Wr|4}YQ;;M|GuOnWSy zRa4mJIbpNhaqn2Ua-GPXrzLyIgROL%lb3hHx}xbP4aZbMtTMaIlbuOTOW&o9O&)r0 zS2Gw`O7(SDD7<)dQnVj1*MpW=QSQV4>AuIp^*cPHIuSV@Enj;8C)G&kKbh6I+}u(h zr$2a-^LK|qu|t|;b0GiCPYJE`0jhpCG7AFqBG63`c_{yqzo0?L-sYOQTQS%jtSwUo zVAi}gA<552mChnCx*u~!lJK4OE`kB7FA~c47*3;Wrr9-B_UliNG2<)Z>Uc-4GERXyT>0{CCh`TD8)7p8iQWodl)?Z$nhfuLWPyKk)(|K04 z*`VAl44^uuPNq9tEOPEIjh9Qs22oJZG)Hut6T>gk4lasq6h2wiS`4yzMp0#%EI9Xe zcPFD>dHeeSA&-=le6+#DfncOdkwvTITi`nk`1ENwO=0@J}g-wJd}gRl$LyT z!JN2&I6+JMG3T6Q$l_iyz{ahe-F1OK`2Y52oV>BfCV~a;!$iR6H6v3KyE=-X1sy& zY_KTdo!3Br;Y&+?nwrk+M4i%B45YxrA%LwN$cw(M znuE~bk^=b`i;Q~hGH0ujAS!b`rm5i5NO1tEX=wigWygH`Idfbckf{R}6cyid7_Glw zNtfG$F1wFPEB?Y-eG<1T%W;3fYtvn@W3n?^giMzl;kQ#euiyU?tvZsJ_h?irsp#a{ zX3%_MdE!`ht#SpJyc zioAdZphXL%B<@4^_@B-j^?X{UT7ou!46dzBJ*S2QjR zkUI7FPl27&@j_P59wpMIm&vjT^DTO|c|Q3^11W_A)EhrqltN;FT%ygA-z|0WuOkc@ zI^|#cE-4_t5WHlIp>*@PIwNSEW25g5;qA@mw}{$|>Gk=N}Nsqet` zzA9OaZO9oN7v80Ayu9O>BOdi^{bQ0riq8mQil3H;?~y!u+Oi&cb-t!j8F2gJmT!D2 z%msYYL%cbdwzcro|F$!_lFadij-_ z=Z=TS449V#h-P?L%TM%6#HU~veLzJ%)9aMhWA3XqQha}V9S&iLK%M^3&*m%NKAoYi zf_b&fgfZp_HXWkUro}(U?hA$hhlaBvF>-^3> z@!zGvyS35!^2H|Z626%2HV)bO+4oJA)Y-lus5eA;q)1yZ-}u! zqHB=-xraWk?<#zo!PHMU0Kv=0&*w6?|1S($sx}&cXJf^EIX^Uel9SQWkD^PL5wM@F zIAwS9Szd3)$OS-9OS^EIJJo;W(3E3U)crjwgaH4q($3hw5fgjG3Ap~+yUp+2dZ)fa zTzux5T~o~$>kuPVXP+$UwVDx1Nq4V(@@t=jHxxK&q}(2B0qE0R$E~p0w!>i8ADliJ z$x}%|z&B-F;5~Xjf$|0aR{@<~oDk+`HD$ zXnDiuj|*~5Q2yy{#CLBC0d?R7+!cH?FDW>IYyju-Eyo~P`Te;lvbmH>LxB`w_u0yP zywNQk0d)XDbdvZ*sd35vP7v}gl^XGNI2Lc~Vy%JJiozWr^X?)YfL=rz5TpxKep8~T zpZvqCDOG&{oQqAgeZ_`Qz>ROR$3wvboPUC4{2iB@-Qk!LpKkxojrwm)BpQ4NLimN8 zfByFm|G^qiG6U-`&zJpu`1BuHU`huv0Kt5eU7`Q&Ex-Gy3s@I(S}B3wM!)}w(NtIN za8a%I8A1QFJO6%n4*)@9U}hZqM=|-=0t3AhJ`TCJs@nhn^=l{U<|5_9QZea7%P@E6~b!u20syN0I zP|=^a|5;!&&d0mw^ODmTG#xf{We1_wf8e**PpDN1-zb7)ZCiu@>|CAnr!lt2Pb^H* zYbBd{kpIMh)g!1A4XW{)HFHArP*vPRy@)c5z(TemweXKU#DDss>J*ZVM$;jFRfP-X zV=t&%b-B{l!WZz$hdk*I{`k9CBpZbw{4Ef_+sZ(1 znd}!q>FQfR%OT2Q<-`N!5_s7|q(8r^x(A1${zY;ML=e3M}9b#B+%h`WA{f1-@yx1N9Cl#-q5kj>>`imc-kO~HjROAa^>W& zs~9UEE5o_9_3Dqz@Aq7_oPnAEgxxWatdi4>fCCi5r8OX6>N#J#;xfvwJLfP=;xX}_ z`U6V=oZFKnN)SlL8X0}o?>l7F!)bDZDnS*ATTaL{0E{z6NZY=*)@lC#V;}~yK$bU3 zLXpd-{pAI{e11a4K{+pDpEqJWvA*K;FL^$YCDNfM2fdzqwp{5hN+y{g@oXnM^^e5u zOBbzqFUMF)$S&clzhyO&E#}J9vCN)F#;<)OmzDs)iEtAitf4=VyW(8ovKD5EJH6yF z@y&r143v##t~yJHl97g9(7PKXVv&Wb$UHP#8ad-1X^cjp4^H zxP)E+^yC(BEeX*?L5>{}gxsv{Bd&*QGk+9q7I`EvI*0YPuWysxOm$8zQQV~Jf^$z% z*(awdS8D|2OTx&-K0Pk6t|*b|l2{oHnTA3b-|A7F-Z*an~jB&)Qa0n2N*n`{VH@C(|tfwx=PQ zZRhUli9qVJc*4nmDmNw!xca3}A|NcDW1>{6OrY{n(eG;{f7b`I8_*Q9K#OP+xPdfB zLE_lZk%Qy5U%C7qH0c8=+}%L8dtt*ooy$$TZwhubnuMBcH`U_}iRijNs)Ih77zHBr zNE_TxajFuW2Lr%~y!Agmi%U|GWhxNG{i^?ltAJ!_g8wH)bT9Bi^hZ&aKNkSUbb6q= z@J9ATf(VS$dafQtAkl7zaFuo>TaP~Ul4Tha0Kf^I|5R}MYo4kdsK9A1xiN%+;>_7J zH4HR3g_vu#m8P5tiJI~P>^u3p$`O@vM5)3)4Z6;~w0P09e;n>!U>_}q{T$DDowWvE zBS6cMsGKrh_V$Fdt{8S5)m>tpYd~%uUN!HOi|9V4dOZ0t4(X5k6-G^I($f;BfES`! z!qcB!cV13eP1GmPM3q74itAn=jWE4ZN4sv8r21qG@CgP~l}XI~4?F^(!;{|y<$RAM zNecncfX#6ldt*pD@oKBs?B20u>Z}S)*)m&e(FgGZq1WFa&Ivo;O(iWf)}=k(1SA) zm^8z&qCZvH&JS}2b*b{YO8#+8JC6bZWW{6*%R7LGb#BIpw;j>_mY=#*L3Quza;otp ziz>g5htBIwXAy>M4y2gsH9BOZ5(@t9MEuS0|5LPz6(0cilIK~E<$p>uE(V%3c_T2y z4)=T+@A?>a&yI7d<7vY^FW&0IE+mNh#N+;K2K(;3@-g?5!9)WFq>{dshAL*Ls=OuT zpG~z2>bGDjErlcr&^OliHgl)l=7w+XIFrSYVfUN6*1V!T$#<&9S`yNCJwX4c9I}mY zdvu6!gH%b;r!npW&JQZDk0QhFM1qw190!(3G2Q#!KdPt+Y``IA-22&uyLbA$55NmA z2no4t4shB7uzG&*X`Ua@i^Ry-7Ck;zwIsrSjeK!C?ky0UarbB((bvHk7ji+tm21sz2+t zs!5EGumM5s|8i0P%_?9Z=^>v3a1ZP$*9p7#DZ9(yf)^ZS+*LgKTq0Yo6S>fOO*&lB zuY}>B_D=QA!GG$0B1LpGC3EJTBGV>s?qnqa8c!?PkA&l(wp4)Ur@z`r=ZDWAWKmL6@7eaobjt4mFCS0fCm zCJ1XrWj=`zEh!J#)IZdm?)7C9M?y{D4@5orOL_ZJQs>2e%osk$X3x>YU(>P@f+Ovoc_Gs8@c`bCeqEVCt*mVi ze!!}=%-$%VPX0TZsM_3(FZm>7r;!=*tkz%7+AEhz6@jGp!qz~OmS+UCHSIM-n_)2i zgi>`MX`NVeRW-hgWL3Lzfh7FsHz8*A2oTN|@Q6XsC6=1$3uY#B$H$m~j`4n}-Z!Ww z#dHLLCBrGB7&B&uT3_cIK&?UGnuWia*?ZO6&cE!}bcuR@C@5mq?t+NVA)NiwS z#q^uFNwgPDmdFNBXx4xSlTqvS^(k)nEF+WeFDL(bc0b~d<3j}7HNuTNQH*MUd+!Rg z_-1OZpl{%3jZ(77T!(>md=TCzx-jFB>L2X*aUFWj9ah!T6oKkw6Vd ze}c9c(VM88gi0~jK%@{dmx`22*aMT|L2$*uwkb9f13iEAl|>)+W=mCJ%Cm9*7vX%? zW^?XVYc~Xr3&q(&uz9gAtO)KeMjhVY8kd!M**myK?ZAg279|Bc2W=Vt`W+Xn^}i{3 zWuF2?>4Iu0qSB5Jp*GGU?>>)RNlrfi!V3vUp|e=8A4yw-D$B#=+E8RnlFxgmiz9Y5 z{Q~Y#@FToJErq&|l0ezSQbOf=U(jXR@GNyJ(`QsM^$L>0X&SOU(%z_>AUUb~~0O7Y(#xykgc zKVA8?&Deix3!HTt4k(( zUBzkLrlh+2#gh?b#_~p}f8jDqCSp@)-gJ6TDSUGlDe_|KW#fW(D8J>zO#0-UXCCIj z=TU0Kv=@_$ET_AZ(?X}`8vExm6z$V2ZO7{T@FPKyi=TM42gX<@X`Agn4t!*S4bcrS zXY(}T=a?VHLFxbl43>E3+e@o~X!BP}aY%5MbvULY5>>Na%b_TjyIx3^rm-lTuu zz_h!Fl0Ko?(tU`MWWL>Y7WMAk1A?$eK4M)+s4SLXncbzD~21_Vvtv^MJo-Bw&iJa!|uHqqmhr z)8k;NZRk;Sv$3U&wR;HL+-ib=IHHFJq2YnV94eWN2VbKwy zw=+?hlF{7{9vU-eUM~>4jh9dCQ>Y?uG}9GHLZ5fvk17yw!=wB)aw0)%uhP7zj_4~W z=;lr~YncqVF=BNDotjHtk8Q}yo(SNZ!$bDK7Ko6KE*UKiv3NuFBu=}pnG%?ccq;c( zJ17nt@~~^(Y}jm%-&$XKWqP$%X;x}dRay$-u={UseC0CxssAL}2yBK{qE}zaqF48F z)pN2`YkkopM7AIdpE_#UYubqSCXUe%wv^?X+cY%*yZ^k5K$!6yL%5%DgVbmRCqH@PI>wH`LgQjaZlL{ zX8fDQgVFg_tfTl*3a;yaJf7D2a|%PDdms0f!Y5|Jpy;9-l zOH7l*ZO8rCaI@L>^Dx7a{YR_7q=}+ z@=1&i%Z+ZA(qrZJ!ppWNUPb=5EiN|$%mT8`^5Cl*`16$8c`n_Cw-qOOZd4_zkcjkv z@x$CamxZ+NH7ZS&6dYZdYqosW7wrZ-ud6uSMeVlOktbtHE98`^}8 z!4jDZ%(mZf*^Vu{XB`J!xC*o-M%?-qUhi64`xGeWF^%QBk@1(Cnr_Wm2_Ci zp=c2HarOJv^ES(6mFew;oeGoiR6W^qcOnbt8{rHPS+(ElmK!&lJxV;fZjy5k7!sz+y+am{n>arZi~U##Ih;b zR9-!>as{#bxH+T+jjdI^7dcH6e{J5eeixV&02fo zVY0u_vHpoZD-o0A#awdM{0YnU$O;3a9*=I6$Uc!rJaCB4#Mz5(>@UIpk01m8{c``g zD91WH7d7Nf7L#LlR7S2wUl$&hmqd<{uw%{p!c~wX<5uV(_D6n4sCWz3loIdzNs?vSf8_JqCqV0(k+tn3F@<`K`!Sx2)R zx3=ZNuMw{gQE%5SlZ{f4)>yuUpaeC>?2KhJqToWI?#U)gJ`>!5-HTsikS!Uz;?38ZE1tO3Wv?imb+gCdYJ0wEh)o)$=wNvv z1=)?#;Hs}0@2xOu2#|-+RoKk?1qA$fCFsjVNT|%j;xm4xf^QwwXr*J!*18d-7#N>9 zyzMh)%lrAl?&r7Svk5qHh@sPTNv^}Exm8Tpf#qn=dC0~0OtqE-vq#S)P(f#|=0OZ+y5ovbM2He$u3o>I zrM1(vDgw;(TzlZY?!;siY(mV?)_Je3bUH(25w$wZ+U;AqRvxRS0pn^(Hb{0j>G+|n z%m2sTdqy?6HGiNML`8~-igXd_0xCUJML{|O(tC$c1VSeua0Eo@AiYWNQX`>>g7g|l zXaSW@NTRd|q1*@0@m=q+{O|pI*ZYOF_>leVJ$vT2XMTHTb`QaQrm1z|v-07fv}qez zo%A6jFM(TY5_XW?CTH~pEVs5{+~^q_wu_~Tm~I<73~lA0lG?K>uwQo2Z$rk2k}B0X zJYvHQ=O7fTp^cYX3u`6o+)b>@_PnZgrmAGT78@J>clZ5_W;tE)$96$hvl zRcONP%*R}ncUhro8z*1kI%|ARo@=cadhurK(?*^{`F;h+VJT9f^-E*BxJkc(!cx7$ zfO`*oWBB9O+kQR4;g-W>px~u~(}UfR9tLvjlGper(gZoVN_RCpnwTXywIMZOQI4Uh zUGeae^vRJT>l$@GEw`9FTNpJXe)wT71A+Lk$DKQK&Hu&DsT0eBMX#)9 zdL>2I6ckBx3*XPke7+>LA~E8XIEGwI=5WJpby`2-n_8OtM+NAh;*7A>ffWL5|aJ6p`hFXU+(Zugnq#RJ08F>*Q~31#bE%&ca?A_0x~74J&PwKU7#8 z*SAgQqJ>Dkch)^j-kM@rf5$6_b#rs0_HqAsY$<(q0OLZQ6ja&boF#qORI|`_+IQzs zUG!j9s|Np@WhFmT1rpUvp%2A)%21(?x7b|L$%vk*LeAIUhf!Q^o^cFli#fm>netB4 z?QeM%IE)Ue%LH`NpVFQcPeNlto5VFH>!ggN_m;JMHmhEE6r{^f>y(^W-~C;=wul$3v-N@t@NFlJlV>TM0>HIS-YHjihPx!p15%)8j@* zsX@A1lEyrGSrYZQxQA>Uy^P@9n>8L{b@}N56T_Yl70M*N28r97E{p!}7lre&uBz5v ztIRtGxu*9N7L&1`r_>VUq8-g>5#?Z_Y1*w4S*nY51x}5-I7EJgaN?BAhVJvwNA`Xr z{rPT$jU>ehvREz1sZ(+`^AYtqQ20ji^Njj}$es6OcH4)jebD;SU+z=mx&9oH2YeQA zQK$*jxyC1im7)ohF{~6}pf%o~DQS9;+!A;%#w0f=fm8)DMMsCRLH_sqpY&Gf;=AJJ@Pu z@)84fWo?=*$yeI1IH6R#hxkpLr;}kh`ESQBYm8nFgq|LPNxvrUY+6F|loleWp}4J& z$A--m3w`|N+g?yVz?nVZg@HEDH_RcyW!z^33miwymJh-*s}wFLa+#=P2Cm)#!y8`U zKZtM-8!8Q%NkEwlo@u0%C`;UI zsSK0qvE5kYTi*_kJ@RQi8GN0`V{|i~onpZonxcLy8snVsrR>w_9 zTh_}q7;%8AevJT9-am*Zg=gFgaH07sSsV1L_x9JLCuUts{pZOSB(G?W=)KEnqI|l! z(?=B!&q6qjjRY=?otAdxm)V`a{0h94tQvBO5T5O;TB_fs;YZv|w|_n#qT88TvYp_H zB1rXeyG%7{Wwv09ZFj4D!?cqyzFt*>7+-0Y)`O6)K)Nxisscx_*BJ0@cnXTSQ{M}21a?o9R? zDr?cUsT1mjTp=}NtP0c)-H;U@fA){drNAOpYSzWHo(^5?c54xiOq-ZC?-SNF;_75c zqb`G2azCohlO*1oRl%IyKUgwd-#kZ`>w&|#)(FYuRiW1svTS=kiq2LQKJ#0AM#Mhu znpT(bGStdLyAH~EKDH{T5L^<@-UT0Av-k5>p52O3G2UN8?mMTLmE&`#4$B@&(!aK< zGxqH8Snp(U$2x$vjGBEt)}UTD^R$|ph4(!Cpv;I&zuH*lw4L62wizs~W?hUZ?&TS# zTiQLoNS#$n)mwxy-PuW)_hIuVw=P!IHK=>WW%(9~zD&0PY4iP@Psklwla;bkp~Xgl zJhug+;+}iku9e61c{vV0g2q#Q*u428IQSSfD=t*<2?+w1VO|~P|e=#JL^)L zQBaO~P*d-buQunP*&r`IThPiRuF3)3Pjf$*KNlTCiVSBZY_FurOtL9&CRa2fddW1K z(n4?1Ypjeq8;Ur?0uoe{E{OeXa=J#dK==L_?4xE5LN5qqJk;|EYdyLqmRfZ(2kFZA8lVitFfQ?0W4 z5gkXp)>mlh+Sl5S~Y$))Mj z;e5TJODzE%_=16uJ3UqdgD$TWGAC-ON#D^a)8n>-8{qon~XHuj-a* zw4Hr@CbUU2fm-(1agy`@{U>OT8VGP*Wv+zeY;HopJr&Jy4t#4SE&h9LF(af;<$zR@ z755aD77v+Br*)Y7JP^>zJKa@yEo7%77lj=$ST)STIemLFT>WKmUn{v`a0e?Rx#w$p zy)RJb?yH=R+NwZ#6$V=Dfvtx^*&%_s}b z;hx>+&z(*AEaYwvi!ZlFt%_UYqev|for84+Ifhq3+C15KZ!>M_#W%ON8RefzmNuY|J$Dc`@ydJe8PU^hp}^x_e11Km#J@1n=I<4{&Iz1dl6iX znVS5j%0;_?Itw-+&(jyzSTTha?cPEeGXWReH`-#(g&6;PBaim=D$6OAo7&?Bxf0ym z-2oQ}H&lG5T@b!Fc)(V0R3|hL6IKhQaj>5LphXUc3GZJ0JQ5Bkk$ZMF^x_x;rAO<| zp(7@VlnOS0eT}3n=5F?#xyG(^5f;7F!Lk^Jzc;O1$Uy8dQ15Eyh=Cbr$g31W1O?NZ z9m|t)F@5dTTP$uF*DrN{J6D|h%tmcb0gdwp~@P(9hUc^u3$t8IjID&a z<;C*b{YtH(l^#fbH_F8kwWgq`9U`@h#AGv|T)X(sX^n)slUZ8oAT|ERWk>xghsezp zLINLTFhjH_r#}s3fHNyf+rr0tI^CA{7QeKOZMwB4l_Io2Tl}a7E{ViD6PH!dyW@nf z$VeUJN4uftbQrSx5?Nzaw5Ige<-(esbQ{-uloxQ!=o132(I18*YHU>8kXrya%)U6d zSE5__vbcP(^_vp&TGH1hUxDUAXJA8NGXd(EP~3uw(>JC!n{nszlIlo@r;r|lYmK;^ z186=*+;-PEZbht3gX03_zsaiavB}Q0^IkVC1C|FqA&xfibVoR(Q;XccEc7el3yY{Cq(3`oQ04|cP*O=g zP-2F8Y-9CYwEUj#8JyX2+_`}`gu1v%T34;q=C^azcHOsEydj1SUngH=e~INZjXU-H zAeJMNp^M_sOR)eveOgm9LTTc5K03@|GVAt&ZNm;y0t<#1XL~7-y9Q_4QLOk%!z-el zGeGq~C?bi`2Es47uqGa-DaOTgK?#9oNxe{VXfOY2-DK#|US@HqCmOn7%(GL?TUr_;mxNUpkEmTOlcaPJ=e#zZSmeWtWI6S}^gc=iKTjA`)NdZ9gyJupUwOc4X%+BIWFg&uAO)@gp}&oWOo|zE zc9G{G9!k)YEkC$cvjTBLZTEbu#BjdYebjRJ7%tAvr0&9AXdLGQD%&a*nffGOb>V&n ze6k{x(q=>LWZ~deTej;W#0vc!DK_F}g3)h+bppaHn}*%lYUPIs%dg&D<(mGI}G+ zzI_OdeDJCo8Gfw>YF@q}2%R?4N;v0?y8*Q3sO|*<370^)(Zro7Q1MguEL9Aw`(xO) zy1CbChk}iGMwYwlWr?q(phdhw6L#6sM)Zms$}d}2)W`MX&53G{v~h1-h=g2fK3+wadXkz@q&ZXt+*tKD{j%GZZddb6#AN=?La+M zhh&TbeAoo- zDzD!8B$2t@87QLl0SbC=|9Kh{9WT-;s`SWi@M6T@y`vs*544O&(*L6 zX`*kt0UU6WnsB<;t_=lQEYF{dI!{puA#NnGug~<(P7!y*q}2~uQImHv2q}iOr2@3m zBl-~+6))iJSeubdj2#=9B6|3bJASh$4y zl7g|^dGW)a&zg{8H_GyJjxRtyU~0wA5VK;0!6-w7GV(F<_{bM|IYz=FW$5NNHmc1S zP#qXv+7*@+_41)94McIQ57XvQ#!pYnw1_7C`rg6_{gc;OWkI&eiCnKypi^Hsm*fCjguCV*h<3&MU&Ni^LsS(p`#8O`jE-GhMEIvier5R zY!Oj)1tEe{$Mc5D<0Z&hWLPMS^=5%GQrXq*NmuY0?cp;_olE_>O`>?(R15l``L|yp+KM^ftFXw zfwFv>fpJKhCCDSj0XgbO!=d)_{`2i<2x0@YVD$Vl@g%ntz`Ho;f121?@<CYLHaY)_lE(LPBRVMXx>~!t9xN>;h%O7Yu@Kv2fMbpv@-CzL|<7+C)#WlYTC@! z`dW4#T$RG%EVB?Y6-}Qntc-uM^Xq@pR!NcAx+L&=(B3SsEp~##0NUK__rXB@QK?QW z(CKM%mpLI(tL)+ZXX6)knpN93hDd*nAJ6LmK$Rs*gZy}%YwfEidy%sqaS1EuoCuKX zU+ZQ3)=%s#*L~F&(JIZ^VzZmADSLS0X$G90++WPQNqB4W+LB%^69u#akLWPSDyVhe z#oD&Uwgwt5CWLqzFrzj2;q2>SsB}^}UlykB&29!6Qnco)a+!^JrAKuustLK|Y~er!iRk zBEj~Di&(%WK+echV{>R85tnXsZ-!CqEq7W*Idaq$M(8`kvpJralnGWnj_OYLlm>`; zKS(SoJ-uv->dD;W(VIO&#tYk6i9GegLTkh#UqQ)5WkqTyl;e!7{qBp|`9#!hbUYzZ)!nLl`G z50ux28o2OMGh0p*t*D)bDJg~j0A*`m19l45+g+GBr;=3ISvMHZ;ZOT!JFY>xbnsrF zOAVZ5={Mor$-lXxr@u4tYm=y0IUvBg1}f=jOuV6GbVLwF)gxaHQ}YaqG&^sGbEn7? zmv_Y?4aZ*641zH#Y_=7%H4>^Hptt2X^btJ`u-nK<@j_GITBgwCIePC+CP*FkWLE2e zkg0UQ{^=z>&%!uC+A{r?VR%5(a-rY+^KD&}k^?%&wS7{~7tJP0;U&MUU7{mmvCk@n zH;!K#apqLs@Z8!=s96cm$Ct!VVuvKkuK5uQD;s)xvYAWIjE`(cz=e$JS&|p z=SCsJa~R5F<*OWb@zbtzoO(e$!~u}(qA!I4V3?W>UxF(%i#6}Ni+$e6^us6NTLzcs z)M;92m{Kg|HY5fxd@^t@|{I#r3!WH zx%V=PX0bpwQ<@GIAKQXJQYJZfeZ;rNqulUlFTEV~Mf_GzVrib&Y}lGdxQfJ#{nx&) zdNMTo&5>INqYA&eB$OAw8~UZ+8TvEF+1-89dgp0=2BdSW1-n+B|H4RIwF^c1+O)zf zsEtDTF)zc~9y+JBRXz^~4@|c1rGL#EjZYu#925iB3WtlRZ`0mAp|JqD*XJ%-{gV<|GU%9b-@2`Dr1=8-FmTL71 zE6dF-#KoGT=!(O%*E4QIbwQdD`46xL!sW#jf=J=#zHKcbzUaw1jMdx+Rm~RvhH&b; zcTm{LI%Q=mXxf`$eybu0`sMpq&G^ol@o~n)SyWbKBqUfpe{;x&eTQ^`1f(I| zQCp?75J?=-@RLIOv_q!FP&2lUYmW!;y11A3H~d(hNjnF~sz4fRX{C?5+=R>midtNq zg0eU}8^5&o?8n7zw;EMI<}Je+x}B8koV2lkKihN4SUrh;td!KGq-_W*#axS-_CocK zym`3)cK7t0B1Px`&i9#+qn;_H)36bL%Qwkl?JA3)g05b?Q&4E0`_D4Gs`2qC6m@%T zQqLfaBhrvJh_eww?&iHFHI0?(YL44Eev5#U?KP(!ui|^;iJ!ETQTvp4SF^ys`?aJ& zg_OdNv$X+vhhF|_#Ie~aE-uEqzk)YGqli%r9f z1Xyi7ewyu2|B%*2;uL8@6yy05^yK;{Q}V68&Y%43szc8IX=Pm%;oVExWWkGfRnphG0y0^Wn244Ua{xDeu1&+yAn(Ps>IaP z4P(;SYcZxLp3wspIZ<|Ec`~(vZd|Q1Tm*^>L$P=1tsJ-#itqtL`(k0g4t$a12*zPo zet~fhp}!82yFz8}&%wsfETQ>#Et_O4MMoH|^!;@{X@&);7P|0W#JOt60xp4!Ucx zpOtu$otC?A)79LETk`ZTW(90MVAhq~KTG6>paYkvhrNUh$pv8pE}6^>zia-wSE zvNLCZD_pU#pPZj5>l!m3=h?7r>Mp0_#^;K>mE4?XL$`nRo{m=f@fa;Yi2DTnyzlqL zJ=9aB9_g4&Kli&D!*BwWsH5Z-J8hRL^80bJpPW6qP9Azq$_Z#ajQ5Vm`$-uWmi3Y* zFMObmSOWhF@-7d2FjRK@6qzC0UvXupbRfH8Xj{2oE$I7hRI>qT3Aq>ly{9Lwe>IQ) z{Qg1uv9_3KYrT;8?AM)BiP>;-Y!-~_H+KO4Y%stdyB^8^Hah1uRp!O2`t2t?tcsU-;PNCPqlt;DF3sO|Do0okK_M0Hp2K|K}Odp zsb2z-2Mwt{Jz~kSIhdMSulr$O%B&T(&K^hQOLgpbp2vCFY_oGmhw1E3hpAhsqz#VL z2eNssac5&nJJC4~3X zPLcfF6He)N+1XQ*wo&mt`OkZ*233-tZHP{E);6mOK-rvBG0J|QH}B8$zLCbVvBfO2>1Z3h*S#8$)n~xFM3w2Ji-^7J zo8gR`14S&i@l&kL?NK`i9hUi5Zn0@u4MaG-3YrO4keJ8~=+nY*WzS{qbnr{NIG+U^w-aNd6Asy2`?W%u!#8vi-|;)neA$^1eismYmcR>qvZ|Eo6w=v$EE zQR1KM&y-o(f~v$w-k$Ls)^PbjG@n4`)o#UujczAp)ua)0=fu|Yi~QH8Or-Bt507?a!0@Y%d_~ik0fL<4YA7JvoqQ`T4pG_Cs7kQYT;hL@%u8HYU~g((_*P zI_2k4dZ1eQVigp5T9f9HX|V*ZHhtJGy6nOZt#A!=s->D8Dz+Kr{`x`RiM(!d1j~al zDHN)-!`l*o98cAJDx0#{!2o044b)l;dgV~@eNV@NHyG5+5LL{cuNn7BWNZDK^=+rG zHpSMFAACl95Aqp2C)g@l_BTv-rW^|8-dDQ?TaV>GvYr)rCBnGy@g8^*CdyeJ(@+ZQ zt&s9tSSGbDUn;$tWRMHjahyC1z)4m`)s`B5uy2#J6bJJ(=<-XoOIK)}T$PbIXuk8( zMa5%v)elv!nlUmmmj@qk#cR9mLWiUS4wpzvv!?PJfY(w)`2>s&bY7NNM}SZ%8MoX; zyS{MK;g?o67?iiFxfiwDmriC0By=FXid+*fj0_4I=Xgct%)WG~nVE1%sWpo)o2^$^ z)#c$RtOFW}dz9H%KD{`6^VIL{Tg>h#_%!kOSvidl{I@|XyQ`JkPVm~2>!vfFZ}5B) zFzU0Z6TlvU9np|HXxT1@dgW8YfG!& zMmJSFHf7^W*sbJebKMy!jlWDf?szXw7CB{&mIY%yaoI2Ga+hx$>Czs z{d!CKr1-O5ET7UnVYkXr4@b7EinnWn)t*8n;8DZr0oZ)c9SiD0-+*s0Nzp|X;yBB` z#Dk4hcD8}E;3z#rEO>Pb^`5EUoQPl|N4wr-YOq+wQ!lCBk!z}wnv{o{7EosWh~@qr z)@nRH!h{*8U&O9fBmZtW8Vh=N4^}Raab7rmNF#3*Ei>uC^HSyI)*zD$K1~M57Lr#p zgx#oETG%@o|VrE>|`3Z z+nTZacp2^qzT9Z~fHm)(dtYzl&s8v2RkF5!idIWJg>+vnV2zRXdI% zD7cROM2iIaDG5=0{P}ww}o$`brjr}qY3aW>E;&6nYxGfG5~g3 zfo{c)tp@6?#XD?v2gO-^o58O{EMr7XjtBi=W=uy-%<51ZlIXj4mHyRo^p3>oOuN9; zdZD*(7BU(N>;hNAFA{1YTIEF8tXE(w^?hoJHQwb-*NiOUeU5=yD;DvAR1sWmnt^h@ zX1Axfuv6vq#_;Sbt*VuRpw28RkMk4*eT$STo8y)*4J#3=;-3bWXHmW4Cj&ipnS~5K z)UF&b-f~JbYH+c7vD*}A$tzxC%|4|gS2So_nW#JyPCj2vL!RQ-4*w9-pw`Mpm4zPi z(LFkBX)@D3k;t>?4f>c*l@!4D(MqG?tcc|fY)Q8utzGnc z7IHanE%dJfW9i?+|Hrq$@v!a__^Xs@F=6X$%vm;eMKYUm}W=AJ#kPS6_qL zy2aT~uZVkijo!firnv|qJT=359HR|#^@Bgy#<&ntA zMD}8ja&-IiQ5ALtu!wO;|ErDDwko!T7#b-*bDAapYYOD{xsheU;X<2=YCQAPa$b;m zL-Y(yMyv@9&Y81Qh@a(Q*oogeBo`fzNGt-wzj*>VlSDLTv&?VX+gAIn!4 z;&$$`kVC7~xHQ6Y=ak&xUZe8rR(J9$c2kbGd)Yb67LH`J+k30NonT47zUIGh&wZTv zA!KBOE@wCcIeCB35PW5&fXz1oErMira(C*ae>Iqm$X|ijYWXp(7~v`F>h%$QiM7b& z?g7utOBVhPPwC)L*D3RDu0lIhLt#y+iO78&X^%3kekO8N4Ag4W!|A}N&e&ix%h7a> zwnU5q?ipZQKbEc0H;Yfuu9noY)=0Mase{c4S%-VaH0a%Bk7Fhwjp!vJFF(^dg=zib$XZ}lfL7~Oq zI#odIEr*3uyxtA($;|q;-!7e77#@DU_Ua(KLZ-k^wAE}m#1z@p;j(9=i?W}F+>4y` zB>HVwO@8j*w)Yat54!smdORjddxTmUzF+$|9^&f&Z$8YwG`%dCY>yVw8+oEwn2mTS z3}-^TNmqFzv0=wWv`UbfHczn^P!cA_G~)QdaES~Kby2UIwYEcw1EUVx-O#|b*T|2< z!Z8qp{YQB;jwKV@*naR>oy}{ucx%3UV>?>g4TXRj&nlvPM)+VMtgQj7&An*xnM^e` z)c(%WGt14GW_HjOGG9`PE+H;NKZC}^>7A3m6V7oGvRZ%-8^ln6A78k*?cI1mTeNI{ zW1F?IhgT$Dshr3$4%>(#D{;k>=+}vlNyBzJ1~d!w<{=Kg?*y*+&KRT>b+*(!TO+OB z>5N8RH0^{Jd;@_$-k{r>@+Gc5QfJ{E>&feCW*JN>Y&EoCL435SzH~9;jf+)ml*$OT z67I|VCEigupHk5R;X#KB3s_ye08CmQi1TiB*mfms@C7u*kCrCi8p}Kz+LW6;9f?&u zRF9ZRy%opt(rmfg<+3s!@cvXIn{x7(Q$Yb!TC@DIMClZltbu+ruA(eocfl=VXHvPD zhlVCJ83*dk!jCtl!(jY>;0L+9kDO7w&`;ie(+w8;@@e=@l~LHh(I< z8*mF$34tvIjWw^!)|rJ#?%Zudn3($Faii0K*IV%i4mCPl=K+z331Ou{3KtvPJ@6G2VpEB#!RFIk?%t*1q&5fXzksh<<~&K3!&LFUq;(j)VUD%?yF|3bV6ze&A<*Kt^ER5CaWZjduDFUFn8yGw{G}+P7aTi z#cj`|!g~x_xfInLbZ$9h*tQTAa68{zh!-+0sG+3RD-O5KGk^2~IIM95B2fhRm9fI$ zej_xx`4DqbK^bF^kB0TSnHJh3Rwf~hpORD8cQ5tln&h&%LheO5b24k*`7m84lYJ{l z#hFBIMdyib4_Tfkb^<(0oJ%w8^$D}hR2$g$H@H>Urh!LbdgqLD!@v_Og)>twkvZx% z<%t`o2a!A$@}(1}P%OCO4ZsNw&fFTMwVec8*dtDsdlWiHnxnZRg z<7EWZ`P}#WOV?r2WXEB^m-BHIuYO?r1@eC5I^$FVa31@r%(>nEgkw_?q|O4QW%7}h zoDX)Qd6IZS3L92|7%lTvTw=x_G#T)--0tMIy+m%`@r=O8nn9oFUNM{jI9W+)@>VKN zMb?DcKm(gUS)Ra=nnnGpW`T##nqNk%vDk`a=r~Wrccxm54jy^X6m?L?)IP&zbc+s( z*pO!|EIyoiprX+HqW<0Ur4M^^wGtnjKDgnFx6-TqIvIAoL#~vUYIUz9xY<0v%$(w` z4-F7EbC&al<#$NEe7f9n~)pdgJ&!xhxA}5FSyGW;VjhjJMY_%^u+MF?UyX zHE^o4DH4qC(U}qKS4Kqo4V2-5^_P5wOLayljYiuX0qG#OfDH3=qO?KF`tqu#q`0CD z-_$h(9~cJUFt%E6{|U-anPE)FVYj>JL6}w%PY9vB95My!Bjv z)ak%(0Ovt4WStKrGBcZx>&gfQq6Ayq=jfd*ZwyI|ol8wfszPcz27u|d_P*f5&)XAw zIUhWG1!Q9RDo8>76d1%i=P$!KI9#zZVdKAkrbAXU$0pA?O_dZ?!QuKm*tB(RQj}m}J-M-(gCR698adUHO z-AU((*ccsjL8!&Ww(C*oza49deR4-3a?N|8{mscIM@QJ{R=s5I@ezAG+@pdm zI%|pxG3vE@W8$pJXCR$Q8m^k&`VjCe6Ze`u8xv~ZRewNsmCdE;yhbsNY^7B&Z#NnX zAk7{K_>R#BdCYQdU^V1>SK)x)1_!$UnxR8Z7P`5YcPcHfGc2&XEZ;EWNs+cv+&=ov zguzltZCycJ)DR#bp)-Dmp*n6WY3}pSCor}8wg`g9Ass~?VPz}tG3#B{)1geirCBJi z9i81tOg}mQcWz*)i06>Fm@z}o=LgHPB|6;PT4fKG8%q#teAP&C1GstNa=_T_``9n} zMYhd(H$CT{RnjU01t!RAyW<97%T4b~=a+AJ@MTj>A@bCoT4+|>!!_3kfg{>vbe)qT z^5gR5CocM}wxmnG4nT1TOkbBEtR;pfK37JRu+LGRbzf&R`b}Ex5n6saDx>nD{VBZD zkiW_sG+6%dmU7wK)hHUwHyx?1j+9rqa7dankZw4TN)|=&duR2$*p=^$hwo2}M*{Vk z3vc$DeQpn>E~>(g`~>y*#SB)b*TNJteE7aP%<{#@X{KxG_Qo8<>w&)~SOIJgI>L5k z%70^f(Crgp!|HpCD2H59wD8m8GY>5CM)#9ws_w+02N;dPb5S0s_H1xkoH2Y@-SI-6QksbZi+LC@Gfo;fOz*lCJ-LwQ=fee5PJ+aK*H=2%b@ zdDziM^}g>+)DIyIf66{sD!Rk}f6SNm)cUon*Z#>lvN$X8W2uYp??9s)sz{8^*Doo` zCfS|%!{&0GKjB0Dq5VYS)F6=2xFPag);E91S|u!Gmq|W+P}X81BUL( zk##;TJj~21idVdOQFR)08904k9=rBE+kXR84HYXIoN&JWJi^B{`UZ0l;$Qpq-$LEt zib-pWT+wkxhnRJp@jD7Y+b<}6pO0iEYk`GEprr$s(!#W(L=RT%umAq#e-HWi$(ebb zC%Nds$ojK@8t8pj1Fj#k=>=rdziyF5)-YT!ST6SBPoroYBXMKvQiP-dbcbo}k){51 z>4&9yixp)B(e9?JK>Va(pKkua+MmcCzw`i-9N)Elq>Yg8)8PAO8t(~@HCUhm>*BFp z_5RLn=D+o20JRJXd2ls0NbS`mu**9qfMls>>`gy(s|eU1+N~2=A^EtYe|sS3nS;FL=&;Z1-}`@< zY)m(?#mU{Yx-OaZU7Bp)rJ0F3Uw37+vWRv~!mdIvYX%NL)R{!y&K+%XwX zK2%1DJ4;(q_jC*6w$Fx6(^u?&M`W7s`;6iZhck>8{us8P0_GUiC{y!)j~k+VRl+MV5dLKIZan8*%;FG4c;$w42&~NG94-Hvk;Ooo`qI6(YBxM`h2;SI>i{iM zQuak}tgNhT|LE;EnAqSqE~)H^`_5gSTh-MF+^Y~oy^+bqHddlG3ZjVKY6O}I9kr|= z%pdW54@izRoaO@?4o|wX5OA2S;R;t6c)P}MaFh^mf!yc*cH(!7z5Ne@gaL*x6M$L~ z#$jq_7g;EF`^52uFe&#LziGn(XDPhLpEC;IvB#i~eW=V7#~r9BDOD?wuN1N3yI8dZ zc%QBzH^53mxZF)8r*Fi;`wbH_t9JewlfSB0InE2WgPUm|>g1LswQve%!QX!e68 z@I2yqSy1!`@&iC3*}Ek5PE#EBpI@#ki~=fyC*Qyrv7sb#neA`2Uf3)$o-4snUQ&?? zW)0(4_j(anCc4!k=cEvAe^VO{S>aCDOhWvUbtEf`u-9v|;In|IQf4ISxMtb>l(@fAhcn|67Jk%|z)b^=|6M7_ zkHgeCh|jS8fs#@L(=i|5HfN4R?kO~q?$*3=yz4bCZP(MB1twR51zeK)YJ8$kC#s&Y zT1^(I9fYgjw6gkk@3*%UM@J3#I1Radm-*eR1g>m5^H?Gs+5_EL(>NH;%2Es%FUR~B zpSAG!-;pbvQtxTDv^OTT+GMU}U@gte4pm{SS?{Q?m7)G8U^q^)u41;a*socpcgxR- zH8|(`6q?DCF5XU{owC1-WnbTtD>cfaPmbmxM)nLqi^;qatO#!S;J?aUQwc=|LfQd6 zIu~jDr{RMnX@<6r0=4-T+x>-vw!J7n%uXU87dD_uuKh1Y>1ZZbvF|4*PIhRc z5z|2E7hcpFOo3u+)RJI+1m|E@QF`KkpLhRqEqpk#+cbt>-Dd~-IC$}hL$9%x0` zN&rCz8&I9|n-8Qwz4q;hHWci^tZW~C(*x%3;bl$EB?ENRIeW3ZG>NsZQ=)?_(k@XQ z>xw{X@zJZwa z9M*6qPrab9iD|arY2xrenq`&$ZHq1iAWP|)#FsBVkfq1pfkV=Ns3r}BQ+I)S=@YhC zXvp&m3X~e$7N(PG{&*xkk9!n|!NjLuQ(SuoGSN^px2rHh)%czWS8)o12Wf&c5JM|3+GMBZZXh08mr%bo|eYrUFWpVxmsmW((>oBu-3s zP7`sU3A8P+a&5g38XC86lAi6KzOU2s>T&}c=uPGM@)q%eESp0!%Fv?%3%?NvPy3-& zrS-c|Sx+F1{xC%Q&0|(Y{CQliYHLW4>$P{!uYXk9F8<+0ltQ z300spR#lxPuy&}0Hp1`FzdFMAs3lK(J;~owJU)AGT~7 zPKEtVqDjCB`o4?2I(-IHjj#8kk%H#Y0jW^@*5aF;&CEfdX;zm44`6m@#r>HjfK~`V zHJQ4^`ORr+Ci0?Er)1B*rM8RYw+y<7syXR)LH8o*C&5pXccJLgXXhDC_}tJK-wmvV zpv&GpEzb-84WZV>2GAnND-kQDHg}`ej4o;?nH8Bu12;QhDw$f_C4sXZZa1=Jijufq zrq5yepDs40TJ7J=GPh~D(uuqSseVuA^Nc6-&)UT}^Q~~PKNoc1 ze#;*1m7 z$GNOFU?yohzbQ-;VDF;CFZD`wyzK-U6!7T{Y21lkvVDDivu|bqfYU3)B!;%3_(96J ztwZMo9L$K%!vVF~670_lOm}vfs)XUF=n-$Y94L7&g$<%Pxu!HqRZed2^)!= zIAaw+#T>G4SlLitIvaX{hMdp1{%+=Lc?#@wgP``On4KKK&-q%iSKELhGti*;tpNVJ+h;QK1X7|UaekxDeWyE49yWw zoc_!mrr2L#t0p{PPp<=poRH~$*VH}*(PAccXQt-HIZ)jy-4dBsZ|09aY zLU!e(-q9>u#o{>jyxtW~j_R~i)#X6@G9SP82Y$^BC67wFQfxC*a|1l3zp6~UHqV7R z9R@TnOjp=06lLr+amCKQ-&;FP_=UI%Ite7ugqv{{xPXDGRwu3^{x*%zA*@?e%3@Xl z%bNk506qL5=&0RCE0`5}+WgFKHZ&IsbQ{9ILucOjsm48$QPTK+As!iYB$E&&9nhf8G z(~4UlWYKo-&A#leehSWi{OPV{jvJl$VZj{G=kJ0ix>8={CDzb$lGr~rmLx}6$*a}5 z#~P?&dL!)+&0*b>lyKC{a=>`%fl`%KA(Rlc?Yb`7+Pk{IFh47tmR~g18u6%CDde&+ z(3db(F?RmCmxsQjSH2313`$z3*jLrj8g__h2|^YSGl_ZNGc{fxwky7GFY z-?FzsZ@7qx`TlDEZNGK@DF<|ks%C`+ck$T&!`geeHPtTN!;gw0N)bUp5D*ao>AggX zMn&lYf^-x@K$^5fY6KLlfYM9oMS8EHh$y}H(1P@qL}>v6$+xkc^B#`x^|busqV1la(1!(DlnSkQ_Yfp*@(O$N}3yv^Q!q>-9=kYxotFH84xd z>X~fIuTyF7!@@2hN6KH1kmWslEmupE7cOGNPa(a@n&j6k3EDK zgj^lMoP^XTBT}uMi~2K7z_hjlV>?1;f=1G1E8N@kqBoW~iafA#d=77|vrLMsN~{NO z0JE=Ru25~O0dUas*Js;yw@*UeLhh!iKx{@*-$~dGN+j=z0>kF)N6S>}NJM2l`Mv%W z7!sJCg-$_~h>$3k@q~DJ?J8Y98w}~TfvtR99#4tg_6gawH#)%l?w!@q^Vw$EL&HO` zrQr#n?osfWqH5Y7+-X_=>86;=oE_go_p(O=*I6X(PFY7rL|m>TlQP=lB#l}l1o`EP zGJR^QitNzN(IC7dFVtKNLT|9(==_&cy?fcc8QG>#zCCH@*M(Chiz`M`%_2 zyqKb^M;$|9*nH>xi}>308kd&E$kIsK4ECik9XKvR*^!i>-=qj5)!fC!o&#%L2 zE1Ido)See6>|Rv#QfOlo_kM)PUv`OL8;lCY*`DBmP*8j3b_-gA2vrto9fj`{H#JGH zAg?`~E~afH!Rteu$KqYRZ}jIMZb_Y*y|FL3j&B_89FX38v+DXVP?#{`7OEDr`g(to z9jX&dz;JIY^|$IGmevi#7T(fteg{UOzq3+jZFysch>SF4yL?CQ+nN#{w>l&4Zy9-} zwxV2{TUCWz3s;#{IWrcB+|#Vv(oMWA3V{cnLE?qDoui{i=((pSNk$aaVc(DZpaPk5>MzM6Du%v%W|=hm5RXerIujDOT@Af zxsWmd_l$9qC4vU^1@PQ7JRKE*b=c zI=8lzqU&YxCq?z$VWNGO6FCX5Yba>I9%CNqE8VFJpca$5ySFwyKAp7jJJwTsq|x== zl4SA4m@WUL7yf$Jh;;5V+WC#N0oAQfkr9;msQ|;dZ)2KgTU{6?#J_yZ4N&pMt*ku1 z3lm`+Zh@NzUf!6^dH{7V#n(s}kqA>vrn1)a&9u3*&s;_**n&_BXr-pBz)0VJKky+xEGfwZXU1vRvlz&R4l#}PbKC^qD#Kk zwFjqjf>r?d!l)$_QkkR$Eb~a+W=FMXDw3}(OV3n|FVFXfj|Q$suBeF=xHPm_3~W+Awu$BRl*Dw{ry$gQmTpFGPPmqjRSit^&Mzg;_u0;j_Gm36 zzeXwdFWJQ>#G%1+7In(a6bf@uH8u9YAgztCPfv$R`X+B3xkzLH<_8sr_}nk(#^JE# zHZF-*WqeAO?XQ@5Vg)O|rgNrO48Q`}%HUQv1YMS?0?MY64mMa?_w%ZHE#qLD`@_>5 zyg7pPT6d3sg{!)4r)XNWer`%{E~8;-Mm9L!-uQO6NnEi)S)SPfB39qsIHoRELcD}m zt8yJf?8IN%Kw@_!D_66MEk}pMeb$3GU=R6a7k9xmt0(EaFPLeN>-mQ=&2&Z*+(JO3 zkj4^6WLocu$h_WTaxS-^-NOk5=FI}L>EizDRUW}|L(5Baz?d`942}x}wvd+cVxM>D z&i79?q0K4dW88}+4r7ACtsHf`ST=>VqLj2RhdRa#l)Hk6W}of2JsX1+ndP?q$~v7N zp9F5)laV+34!Pw7W3~(<|EvwN7XpA0UtLk9qGc6OSskd;_sR zZlh!?$TvpkzAk=d_*Kl0Uq3g3l@h~eJCqv=%))4q=c24^%eI-OQzVUT6Q*p+F)y(e zCqtEks+8lsvP7OPdE8-I^l{hcNo|cd!>^~}2tC^qCu>$YEiYx`w=z`qI6zwf&n#9S zGK${LHp7@YZ#@XSEeN%d-P1|AwGm}SZ9~rXfJKUH;3wC!`B-+T*`!*j`5mh_t0f%9 zngy4|o*+9byFsvRNMy5322xsTXv#3 z>A8F4c_~mNzBd0*%RqgGw6#Lc(>9S(tl$xM0PH8BbJfv;vSt!#ijhynZ;Nu!K-jD7 zv-nhs=V*}TA99UM=P8uA@?uul`cBy`PUZDlRMYryK=S zp05P9hr?Ji@0C}LYGU4&_}ItymtH>&V(wj|b&ZG?zxE#H@pz&C1+Za|G-IbknQ{KnDU%OpaD$868FXQtVS22t-)m0Uw^K-WZo)<({jNgvXUsQ zu`6=1p!Xl_Lfa`ljxDd(vRpA_3Fk0x+=G zt_zQ+^dcV$pJOTs8*}gMbv+Wty|mJGGt&6$Q9Z(#{PiGEnaG&;RGnrU8+>A3YG7^A z9ot38p%H01WQ@NDm;mGd%>)20UF3UKC&CkhI-eTd^t721aqyWj4|EE>_`R5Nhbw}# zV+Jv}9<4H4<1TWA$1KPEo>1tm8eI6s_)TnFK?AAX%hBfsb1%Y`N%&h2jAVYhYdo!M zQ1Q{I#ZoOB^?<#W>dv^g>5b6xgl%I55BofQ_#HBB(elLGnj&-${9LZ_>benkQejIBVU)k5fKB9^<73@NwUEd5XVWA5J@>`R>X< zR+LpI@jfY>($O2uU+#cHxmHcx&yQ=@DDu^?IR;+pP9dn{RvneuCvH`l=t}6=ExZ7x zMQ50$wFJGj92VTW3fSV3i<`8~c-C`x`$~>!v0fg|h}m=j70?dy&8u648C0z}R!)wI z$B{RKB|5mu#`A0IP2%%Z=YyPgos@5!f1S}y93ibv5IHJMXcZRzSscV(?6J49`bwQ< zZSjSbx?^j`L|C_|=`}aA$0P|g^P=ITzgZu=wXdrU%Y}1(t#3A9Qf1=t4wbLky!htg zUiJEvPLXOca@?Dk-`BYyP)Dk2&b`Q71UZOh`mO>?Xn;a(sF-E{o*0K2HX=W8*ZgwhwJ^i!%L zGYMZ(z1`005cu<)0vDYi((rANpsP%2sKCT3Z92MG;T4O62?h~Ty|IaMF!SS#^sPJ4 zyG)D&n4b}x?C-3g$F*TdS9zZGuC7_ zjvX5ZueGLoC!G+($x*lM_JFgoPIzSvQ3PE^Ru4kxsXO*uldejWTS%6 zZeoQ61^svcq|t$vZAk$)E`#MKyIC_QfKdw`yke<`8lwydy{<@by`N zaQwW$`=e&U5!{``z}l9r1o|*FLT*Q?OQEYr(ss z)8w9^El`~CsZpe<)kq|DXdKaf+@NS(n6M`=FI=Z}Y}_MKz}f1<3Z^P;9fuwmi$Rt8 zCnZS}!qe5uOr^9E@UKx!LW=M&X-v0A!L*P4K6W^_oEfULh;wVaaK5xrkOeIncX*1u&ei3T}Pg3Y?N?)s3og$YqYtnghlB}m-wL?IwIt+~Wq} zijdOcZHTMyoZYDdCSo>|u5P!ty^d(xO)cskGhIv;;(cCj%d81|D8 zG7U(!aFu7c8ZL0~FgX?IF(*|a^z5yR@=3}xF?>JR)cY--saoew&NNRE* zKJX{Dl+i53I^bXfOOj%C{d-08?5Nf|O&o$CDTf8NA=~z@u1F8Ja$5PCTShR_*0kn1 zIlC|~!Z=;8WfL2uH)Z}WFz3zUy8%VKz$RTK11URrjewLG4W7HusqDA&7(u!bMw0uqYZa`h-!n` z!T8ttEB;yjiGSLx)M+}rsa@9qM`VMpKZVZbE9x)h5qh59_U9xw#fz>FaqvUdKSH@; z7VN?=qa5Zpk>uHY`nH6{H_0g)=q_9G+OFcvJNkTgNHGI;O!5(=?IWd-g!-7h4d4&7 zoF*z7m~9IMkKC&2@gj`=js9gP&~a5MX6GU*Xu!bh)h1y?tPaYECR*ZBQDk)A5mJay zvQFG{;K0t3)?ZVz-DQ62dUfN5>|8S#3E2ho=eaRP;{(l}jcw&?Oiwnh6??3h-)1eh zu0^>j5Or+^OI|JzUzvJOn?#cq1>kWynhre3P8l?-4o2LyVe;<&6%N3Jsva_JgV@4R zp-#_NV&kF>qyY$qSv=;J=_~1?d=EKqS4J>Y$NcrwCCtI z5~?>!tG~~@c_D|m<{x~LxSOz21)o8g0S?-`Tm;?T%I`BPYsj(m=VbHTSalP#>0dT_ z{)Dt9g~`v18!bla=IVVx@FqFS9R)T@m;_Hqx9rf|6z?!BA2Hf7_FmK@NZD=MGAnMaWykI>y3vppr5h!3CBnuLv@@TwReX$tnCBRuil8n?z2K1YAiTgRYNLgPa` z@vN*pj+0Mod$-4m$Eu2eMCWr=y9t5cvkfR`ZyLv*-Lx+aUMU@l-(OW#(R4hZ>|*l& z3CMWlm*h(Qd|mc7K!32?zdO{1{erhZRjJLM@IkDN|fu>}9M$xy} zFNSZn`g3$lf@Ivydf-ep9~rR4UW<>SI3&4PC+>NOOvA)x1$DMOzF1- zAD$NW+*cFxDZc%&I#xH@KlFNn@}_+qw8~t+J3Ce@z+n+_*hjC~sBMf)q#Wiv*T#v1 zjp?}1@>(aqBF!3-6`!n5l6atmTA!_Pv2#u4S)^>Lc&Q->FaM~|!7*Rq3}}%NFw&g+ zl`6POMDlLc0-Uhvx?N6*t8~jY8Zh9C+&DjZOp4{WjR+&wf)dQoNFCW@!P8Q`t$0I1 zer9rSrs#DS1C3X*_Z*^5>$3d!H~|C0txmzdTTptpnuYUd1STTAbzErcJ5%Le5apOH zsy$lN^@gZ;fZF`IgpM#^D6qqs^p$ECoyvl?N@rL`P`b@$SW$8RSOpbBjloU1Y3px- zN5-X^%9bZPZ=5hv!Oa;3w6y7*gc8uIjYkJ!Lzg#TbyFnK!Xb!=9A|Kn zJ?kLACe*Que8!S$1pI@GLsVxXgw6Oikh4ui#l>o+u~@V0#zSUX3bD zWPR8lUb#0UizjbGdLCxRBL&~G*y9~)e8DiiJVQZ*Y+Ku6#+}exNqer_mFlrlcP^6S zbDTSiUD0BEkK=t!U}$tDc_k$Kid=~u+ANt&{sbGfXZ;?OoP2h%7+N>9Fw2y28`!%W zS|ziFhZJ?WXfs`d9*A5Ybbm#zZ~uy1Q~|ffBs*yWTq}9VZL@nAWt)f&I%Jx=M(4e4 ziuZe*|>tz$r~7&X8T>0h5^hb$9N&g z8W>t-OgD5_^yqK+u5e+AI@vJ}GN~um)*(+`jdh5GHJukC%aYqa;wZqr65KHJxps6f zb~U^DdzT{KPBsV2iCZbxgth}~@I3oWCNx;%hAKm;7cfn>u?reD3V|<8x%LiP+QVNp z+>OBBH5?!fNMLF`{+U0-E>R~&vAIrWL(qM3puEPYpFsDPMM~sxS%}mn=G6)nxB2?O z%PmJNIH=|4>nYhTl+$sn2zfJ&hy?hjZJ%#^EGuz<0^_R!Eb`nh%`Mn}egq?WKh7FV zqUReJ!Z1S^ITRT)KLZSY`uPM}@qS4e@yILU3glkzd9BxTsJ2R^29x5??tKHWiyU$*(ep!*l6*ko+=kaGghOp3{88%Fe=5&MWVRm!>2P3?Mk1MvG_es6UrGV93084$<5KyzS-@W`<8u z1>6Ep3P&%wc;BHEH#(ekBcF3|4cM8^Kz($9XnoNz%~m9p=^SjFUYfpFuSsM&O0Yh} zELiMt3)Ll7>fF(SJ8@)fZnV>T)JYG0*ddyLI5LPS)1c-NBx92FpWFqd#4sRXyRRaY zr`+-Ee4*<;hVoh4x$r8ukm%#D+2rhm_wrAw}jlxF?Q;-+^3Kyuft;&i&`G=+D^sEs9Z+>ZIo@!!x*j zOgC$|J|z0(SHxw2EpWwGx1&I$D=PrJ({NQhPQvzF+B_W(k4F*4?qJK&)4f%GBvZ<)Y#I zIdy6^Xv4s%kpc78I7RW|$7jxXs@3Er2A36&9xFYWt+}(Su|@ygHaVXehUY#IPeS5?y{=v2F zQEqRad1dkovs2zT$=erO_fd{khs@FfJonVeG3^(F6a9;WEBfi?w{4;%8L>7=LT3(V z#t4SJvVA~(!1VWr1+>O0U91mbJBB8{9P{k0r>@t!xbl)9%Bd{C^{iBVXi^o=zfzSl z4lmhE*C@BDD>@2(R9bJ4oc)x0Hd$V(pPM&iQGiol{-|m*_Y;q*|3STAdlbO^dlta& zv(~}^X8GQ@wR5{C;T-r7`P>%ZRrJ*NR^ZfVGb!;vJDSZKGF^_{Ly&q2!wSF|i z^T=mE{RDaXWSM)3$5EU2aeX*PT&Y+mvzXpaC#QE{n-vr#E2b)`K78@iy$cP|XJfur ztA2PC-X2z~!@Jb-;lGl!6Hgz%aWX&*h)ST~GTk^6DBcq}HT;0D@*e?)%L@+}#26VD z=$2!-g@rYpom=PFuFe@A76K*9<=9tEBvkqq*h<^COBouWI>)jdItcptrnAwamT|)e zj`+81C#(kOvxx^xd7Qw0OsNQZV6T2Z*&+(Ic*-lC1c3tjAHXzQ&22(NW0w zkRk=R?0@0&LO3Vs0wfj+8`wR~^7AX4s|iXXj29yE>R+-wp)jt}XB(68p9i-*!J3Sg^^l=6Cvi!FA5KY%0CZie+b0f+TQM}=@=+~jW{QVolX~u z2U?#`^{S5Xz$WP*F9P%LMqO(Z@l!}-!E*U%v^UDT#2cU=e>wa6NB(mFM`F&eAP?=L zz4~gId9dsv&>MJwL)&5HYM?ZMQKGtIp$$&|)i;4C=KKD%NPS4>+1}Zib&IaUVnnOu zx}Zc41S(NXJm4^}^Gc@hNbplt!p{?SXTq;MGpQTzoRIcz*9(sJu7U1_Mn*P)0_-FX zayNf-XI0dd{F8EW1n_KNkB6WY(f$8X@biBF@2&>C`{kMA6)P{63d$Be5cJZMjGu7z zn8C2p;PI}NW%*?v2M0hWRR8d=k`k>fV75I33PnB8>VA_{@Qr8XzJT;~5yr$Yz?`UL z|9FtuXBwI31)kr^B_UV~f;jOqbMHB@+fu)12l}rbbV2LU;cWeU7?j%PBUmJCv1;@| zr0C1d=hi6uIF3DXDc#Pi&7|B-zySdHC#-|SN^KjPM_}rb-x?i(5J@Gbg6qM94Af}t zl>=n7-|iNc{*)>};vXk}TAq~x#Rv5-%8J2^D6<@5JM)Q)qVfxfF9uDQSBQN@K}m=bpXtW8+9;TP?%ZG0^nmFrWu5>QWZlZT zmp#{c_9FAcMy*0Rv#XqU?i+z*M6tb`Td$6p=LwG#7RZt1mt-iw4<7zy4h~?Eyf(fC z<{v81*FEgpnOD<})jnJxFbxiJTv-44FLev}JZQ-^Jb^4#{n`!t>3?yNNvAi`9o_WELNWoiOEtgZ;ayS`ebjS$MLh z#Pfx6a;fM`3T>1{W_rR~g0|VGWGgodiHn3sqy>Ua&~`b=C2E@Ffsh@9k$nEH(dt#b zF!;~q56H~&^62PjoB$`yv(r@1b(`Nrei(dphW`>jf17-rX)r>KShYLf?Gm}YjpUdj zmkPJ*m(balatub8^FsOai}H>WYyI*%)(gvfHnAj+v68}NhH=f=`!7w894rNXdz>Fo zgn(Aw`F~$N1#WOV)z z*9mYlJ-zp8U=H|HAB)5Jvk~V}V=_vExl)a%dD8ed2hz$7k*;1^Cvd zav&Ftst?(Yk?gAOn777JXo-a zE%>-wV5X&Y!7w2*l1Ek+!Vni_TPP&?Sb9HqFq`b4IDZ_~^i;@q;F`tJ0ZQ7>fBPr@ z_^NQ9VjQLz;PDI1Dm)%=c)btM)$*hs{}I=|xjoYh9EVyrJ@qeBS7OLIP;~q@!BfW> zZx2v&y7=inzb?mKA98oY9{czs1sxD4IMeR-cqzX$)h;3RES+Xc@cHZKKVx>m@F_#C z5c^9@BC+eH$P5!&`!e&D*tFHdwLwu?3{=h!u?)7TD50=hfB%hJ7X~LU55lC>E_$z! zdH^MBa;A(M1$b5%Febud5AkOFF3WWYGW4L;s`{zx9+asls%X7^EDA3oRYP%z2eU%s=b` zEX(j6e-?!7q^)q_Qjr4U zpr;PUnDYueqXM1%#`{^i?)tTjgWc2_3^}?c*?i`hTFde|Ax_Jxm7z&^efr>EN zDgg&>9jq+oo$^+h?ce|4-``nj`}FP06)#I2bFfq4T+Amk}@rk zLn!`z$_hmIl%VTNy~y_Pr6(;x(RK*Gq^FL6o7hFP0*_agr6KVc2-~o1-{}vxfv_DH zl>Yn84p==zU_Wd}I{paTeE$}^Atz>de-T4>y%|nn__*Tw-a5v|9{wg27Q_wxI@7o|9uB8?J zNF~RfYl_E6i4fuv5`+h`^@9ih3o7@gj8f74oedJszkCLWaW;j?v?=+_JG0MAB{}_Q z%|{BujCr9i*(LVV%)&Y_HUq*2-OsbL^>UvCBn$rS+5b7rM}Hp$%hk=+>oAe)qz!O= zjF26n0%mT!^v!4PXwN=rYa4hXoegNm>t}%)6(>S>>cDKu%M#34CMZ~SLl67k%;_KB z)T4tr2GhsDEQf8HEhyzFz+b;cEzj-_lk@ikw|>OHIb4t4>EP1d23!{GDjdr zZZ&lSW%;#-{eLz2umGj|BB}GjLg$P8Q*Y!&!hyhzuJ@z53|trmDg`6MO`a!+#J*aX zNEnc20JYOg`0bIu{@R6*l8;LR+1x;`OY?MB9QXu&NpF-atf-InGO`~(d2eO%bBSu; zWE(ZR3-0ngb2vA?kxGBWE(3|CCK7k^=h%G#k9*kb%EY2t7 zAY2;(k^A#Tp}*M$@Z;7ZmJ#p80S)$>DDUD8v-?^#btYQT09+*ogyg6Yu0MzhTKW`h z4+iqYtcx1STWxN{yHx^>eQBUtvHoAm$(X0dzyOR9+TPy&F>MAIc5pZ`ap%6{Qy}ZO z0st5c&J@P58$W>t;;~P87zgMkcC%-*0~7`{k(m!Rk$?G$Z|~WkP{HHdim+jZ634tg z1&5_6^l9p&)@innA5yX(ip1Ru+`mrNgeK5bC?6COT|TNj3LK{P-w(4@e>hXTlPDwCq@U{+VP0_qfK%aj4(OcBKpWlqWxRE5~oVsV*M^U=9s$IRCdX0@`N^ z{5ff^jTJOdU2SiQr?%M{P=O?U(1U_ z6e>5(XIsN3-b4c=!kP;RnHk|G&mQ1PeF&W2u&?sRG|!*b*Zw8Y1Z894xDT@7mO1=5 zjo$qASAC04U0{C5S6LAc0)6AnvjDJZMqq+EKxM|g{QCE~6n9Pntz`LxJySOFo5ZQs z@20{4Zu40!Dlt&~qqdgTWT#wDTC=mW^I?w&yVA;t`<~!eo6mG{)GR4@fzmgN~NN~kOgadtlS!)Z~u~fhxPAJ6EF|| z#|sMod_g0Ihy~_!6FCaA%?(C4Vy>dnN2jc*+hR_pK1A^IU&nkX8SG=Iu4hA{sVvJj zOVMtdwd0ouy@8-|d*2MN2R&^I`arv1tz-c11GG{q(oW^pWcKBv^V{pS>gNy;om!#i zTlsb{wXF-MvXuhX$ea*MJ1$@stx9_F>|cWxk};f)Gc{>X?DVX#s4)bYIXi-n@o8;`$eMRR}o=!4Nf-&ux|ic9+`Z0 z{phpnB(elA4`;~YGw=Xa;QLdl{Qo7GC_$KRUt-o4FMbw>zP!||?R?1jKcdGX%`+4D zXF!QNAlfctWrX(Qv^ue!aJDt`wE3J?)}?cl|K);76gvUTIeCp;{1O&tS|EHPW1FP8ifQ)gyowgR)XAl@0>0oCefmf|a-*H3?lzbBc7Ki>;C zw{OL)?829KStoZD6^@66JzZ1GjUzj8cX}3!jlrSe{qD?62OsD7>2CAOAke6LIXr-< zJQ`h~TyBNIVESWo$xm{C^yxsj|DR8mt5$!i`1!8piCNu~sgDM`t$057&$|_0z7F(I zAimV|2-~b{K8y>2 z+_ADsU&Bp*(&x9EuCA*0QbY2^=Gs`4r7KM2%GSjz=U-FYjJh^24K#Ee=4Hq#Kig-G z&Ss+P&YwnZojfa{+r3jnefCox8d3bfo_~5T{Dz3cRG!l+L6JXoD4gj|9}>2XI116T zYSfDE(a&Pw0VLM*5@@F@XXK<~oe#w~goTP-IN)=PT;_i7fKbS$2aXm4*7=;ujxu`d zF+7I&R3FCR18ibVqcOEoj|IvD;CJg-17z#Fop~4^T|Ba5%n1-Z=(eO(%13s)4XU8C zcH!zk1NMh3Ob0zyiRnM_?R@%RXwQL!Kk>;ShBv3Exn(R~n)7aPu0gEmX@F7^5Vr4N z;g#djk|hrs0!mKoi2k4$mh%ycxVg5I2!|jef(R~%@P6AbHxaQo;$cZU<#P*3W#>* z-QWF-ae5TI6z{Mv)9)jzY2|qxHaZF}m#f!$1HcSp zd~F(Y7}dkAMZ0HZr2o@;!c~7Z7I_qYJ3H@2eaKG6PX)XpLl}R85`^jxMOZtz&7e5Fmt19=cI)jD)mxni zWP4pC1sJj|_}Jk-ORLf?P^ur@U~X;-dBQ{ z<6KvfkA0qm-H1|*q$4lDdRNZU%bqGNhOeKCTl;=^`vJ~z6`+ld`?#_8zsJJc-+_Z2 z`QFBfep1LL8Sn!{#J2)__LaLeuHm|w?>RT%ANXnDS@`&JSPhHs`1B%bpE$RLR&`UojuS{oqNcg{yk)wNdI~rW#S@9V}CwD{ax7=LjF`De90KZT`f8 zc#kfgmm{<3zw;*8^P%u%wjWGL`g(10SM&VLKT|Nry*cmQYX=bvrTtaV6ksQL@qd{pEA=A$Zp0rG9Xz%0l$&@i zBna-jObbe`$uTN7H7v8^oR4=&AM;YZ8{#^IY)JD)Sy)ce_|I-gpTOfw_2WY?tFxfI zu{-)^?s|C@t1Jf9Tfr|}7Axq~nYkp9gzMXi!5SXd**%`T_w*s&?_!T^t0Ox^6i&0l zvUwCqAuFRc*xnf=?GWMq5vBW zoH+aV@~T`_O&zd0KJSc6rz2y9CTA;A@< zvUrjMuJ`QdeKue4rQ~UwFMf6XvlJzEe!}DLO7mps0Q1`_s9$?5+26X`g**-(J0}FR z#ih^&1`fQsrH{_o)HXS+_I1^@?$A8HX1o_y`x;~;UNNbx%bSnlLv;L(#<+=Nf86lJUbv@&WU zeYJFG;ytSa%fnYj_VD2cp3qzUV&(DX*8J>OexK!^#5M8!FBWdp9~N%7hDa)=MLD58 z>}9a}J9}1%5Ktgyk{qLwYOGz~-IIaisS=10DwLW=x&yOWzf6i-Hkp*UEc-Y`h{rx5 zW>0*-1(FWw)Gv)(SsJkHta{=6U%^gYD(6aEH6lYU6(VCHOJTYd*dp z|M^lL1yS{J$YLJ5sZ7H)eg>>3G$Etx+C{ z=apXc%^i39RUVwSgG2qcDwNV@A$-x8teoHRHtJ@{5DCx$n?xb`kr zCK6s24UyExeO%bO<}X*zzfCf2lARymCc2)h1GKF;wmu{fCKkPGB(Y%k)#%8Z(5W}5 zd~2)M?0sE(4D6xNW?r-G8~|mO{@Uk&G2_bQp|=jG|NkQMbR*NsN2OyjTN(x^2jsqd z0(VZf*~CAR>s@sG%lpN>1vYh*c+8&_d z7jx}=#-~?*>Li|vhdp#}3IS%)zIGxl4V6cp_ZjBYZZj~{AoqW`E1!N2JVMTjAD?~G z2a?%)^y-)*IPk4lLn~{(yfA>CIk#0&n`XG2Xwb{8*ez0>De2RQ*pc0sDekgrQJ1EB zdpr~172G;Fg+KB9FP?41zijEIh%SaI-~_^J6QQ3e6nR6WAJ^_b6V*o`2yGw%B~f7h z*^XYB)V&Gk*5`q!CdA%@mtN~j#`)Nu3@i@_TkEes*nPW!Ywg_DM?@Tap0T5>;7Bx< zWgyg)t5ZsS919FFn5EH@Vo5OCC7!NyYG!i?H>m17 zpFrNb-t+A?RAEsqWlDjqtLkGc+xk*J)G`D4Dx$TI@V$M@E`6nJIj<8Hu<1JCq&v2u=$`lWy}{-c0W;bHDgo4GY6Z#tN7COqrS zC+nT@wsy49YcR)HI3;g8pRW~1lz8_#kKfp@HQN;?!Ds;J*?N?~#D!vM=+0qbheQGr5S5dn#r^m*k-U$1vr#8`jBuf$r7DA8Q6^v^Fj^j6(FiH|}X)L7*jkC%&7cf)C- z;fGBSeb^#bG{(8U5-LN+a@fQu+o8<7{ZUsYhwoh)20 zw)gdzx5;%4DC6RM8dJi1lNjs@ZWMup-GXj!4Bd1%17L{_2eHI3rk`@h_<=vg=X<(< z6|E08p=>vyM;Cfk)4ub)JO>6u|M-S@q(y|GIHBA}J+GRC(VSfdM28U;;!V3o2w;uP zr?k~?-}H|VZ63P}za%!-+QTtdE~%8@{qlW6oA4feH6<%71F2XlVzZ}k2dv@j+C*Z| zcu)ir2k9ef*Y;K%*Qit$|C$v^KnIsFH;~|U&8WOi|Lcm^9$Is?eHdPoWo^G={=o{l zBxbJ0l<(c$nfRK0mDc8|m2h?jB@n9E-Ivgrt8*ZZ7qvVK^`hmf0phICw+t)BrQE}n z6{@9fE^G4pDSrZ6;m3YF0w|(~a z(u)!1&5Ez@5Eli~ar(w7sNU&T2iz*W3iGvJqv%N+D9Wm9hxbv1n>YE|>fq>evLHQm zCJV=ZR;Z_V9e*Y4#eWj^9>nAZ>t#csHm%$Fl_h>joa5|H7|)dX9G$v!V|dqg)pF-) z7I@ohdi;PBoeW|z=PW}>lpLV--$3lHma8EpO!fu9eRkRQov_l`R!+7wkB(%>!f7lD zHgN15dYKA;Uhc@FuhOr_A!88&J59*&zG^p}Z6AlPYDeuqsJ7Wp=X{qUrf^(5%w?jj z897+22uQtoA0E?2700D*Ym8zXP6I7M0YFW62EO*?W_`#pEBV0qbE22)wdk9EtJ}}) z;1&M$;j{mIc%YaMciUHY+5o(M!qNihnGlEWR$hW8hSZ1@ zw2Z$aKe)JRQ9V0l8n>0yPWtBQ@+3QLrus-{rx_wA3}3r@&*!n#I#5od^^$tyJBe_i z6(R!0fxwT=xss60^R*-EO6r^95g;F!9w%toNu>> z@rT9SUbZu{0Md5W&x_138EE4rX0g-HM@jy-n%}1(D)IP5_wDCm3xg=jcj90Oc~CG7 z-jzrH7SL-iba|qW}{@VhQ@5#8^*PfSbEmb<^~O6G%cVoQ}@(rVo&^@KveS zF7`TZ^1K7$@HOEVoz`LrKJWw{0H6`O`91Klyo0$pb>H6sGTk zC3LebP}8#}fb!qF;c$LiNtP67Y_3;~|u6@jd zHE?WEFW9xwwxR)ss~ub{WEkb(o=|no4>^X^g9AL<@=S))ir1G% z+vMZ&>bt#Dnp(Mn&vuTn{*=BqcM}p25p7ObP0@1R|MR7O5fMTd<<{1S1R_q&C1Az? zYZb<;iuGHLVXa1^obTtI(n|Jw8Mr2ubLrXUt3Yw?EPz|gt*7Y;?>B$POl&Zw>rV%fU-IHFO8+{h{4#m$r8rc zr!X@Z%X@UE`+2IlpD*vH_kDcw<^1QI>zs34*E#3+`(01CN(!HEPVQAj4sIM{g`gyH z5{WeW_={z0hX8czsYmF)GvDRQF2thET7YGGo1I_n%N4j_x=3J zi3o07_?P8qkq=dbz}ZrMsq%CuY)19ego|Om+0l3EgCc(OuX9uGuC*6hjlqZRpz7Dw z{BsP;A`A}}F9OMkrZ&r`<5h%!s1x$@g3)}u2fvu5H3%pj_Zn2FpBo}R5q8r;^34Yf z)rWUXL|J`Y&+ua4^;Grqs=v~9XO0Ig4<9wUG@p8laIt2_uUSNPrYo1=YCvQ4Mq?h-E77sHwUMA+FNr4 zaZV zGB1}A623$>5(3#<4;9&Q!2p?$(}(Qsm4X(L)nRhwt~iG93O*xGM@QGOalg}gWnsUI zR)d7Lhi!j~pht@CEz)d-xNozJt87KX39YDgsjYIRTUSC4_}mJ% zbithU4%9^rVM<`Tns)JDCci3Ddo=MRT2DuDWn2Q;I?D{<-2F4+TQ}`nx^DJ8UB7w| z^4w%Aq+JAj7W`#2-rPO7SjNJRplddNq5@x;b@VMS!6GJALPXw}q;yk<@Ow4)o7u+hFO*$3!*irg08N$-!CaX>yMfTCG!z~~_P?%mco`NbK7}_Njz39u!->yB3 zU7X)rH2!E`HF-$DnrqT2L8_r73g}3Pj1HErDY{TD)+ZQ1XEcIO!cMWK-dR>e;yo(?ZZ|a|BKf*RAu}KLPpL&S5 zZtSQz!ZdR_^VPxBqZ9&GJTss!vgX5_*SQfhtWE15h@)YE<|T&#r`qf?(x)N3V7ww&zXyb$8bIaw-OYP!qo#MQp5Zqmczm(9{vldwZ( z;=NQs@68O&56641N6L=70C=GF{72W1He=x8w#mn5Ur$;WB)>mt0W1J`_-q_*MYknO^|!Cr%p$Kmt=x1JjCKS0 zjjaJ@Sks{5liIt~X?eoy9D~{QTYZ^?$^L|R5k{__*rbx8M{{GY1wcwq9!{?!i|vkZ z{KI;66?rtRP?*1Lmc0M!h?Cdtyt}(!cP!_bmeeKo7Z1qV{gxBAUm;JU+oLP&6J?)# zypMJ?Z$n_j4S{RdtNovMdByRgTO(4spF0!1BOP7b-o*>2$~S#!;Qhq^_6)YJ&1&Be zt|#ApXmb&r`TnYcya|`&z|f%j+-&X*aBsDSJ1vrx4(^^qNu4;nl=Hx`hiiU?O|qB{ z8}zeUqmA3~rkE>+G)hpvoQg%=Aly|FBKvr&(oFL|>z%6_XS z@7C2`{i_(us|VG7!FX@`7KcXujzhm@yf>z}lO7DCAF(Md!2u-8uudDOGfB0HVQv+U z{Yg=~41*<|+w9aDs1AY$XBenj=R zH{q1)#UAwS(yijRPo?4}(gIhs$~3F2nBq560&Ok4f)UC|tn#8j(Rj*pMSo}+pYB!4 z(?eoz?+;FIO%Ot&NH411q)N#k-32(?Y9f23+EY9Hnb=V3zj)<&|4sk#A=9@0BPa-_ zB(ecyD9!H|nuDkIth0epfp?}&7CLf6(UDWINy-CtMs5xn6AdggBu8Z`kk8-#$vII{ zT^k#}f7?zs%ICWI4le|wmCW@dj3p!<%J#D{Xula_;+rw{9|lxW#>xhJa+b$xhYu{r zN1LfGYSZUS{Cb^k)~Sft9%nDI^wS#;>6-LJm%57dZ0hmml>{D#c7IU)pi>AzSZPNR zv4jI32Cewww#3%UDRBK!*UBYV*wC95@qo9gD>q3zUi~tt;3P>$jm3#A0a<<_Q(+lt z8-mIaj+5(zc!?jGY`Xn)tc>u|ep~MNRwp)(vv>?Z3kO)eZs=P5HwLokXC#cqyj`)l zJK#FjzAhNP$JjisPNh$h;wqZwtX8hHMe*<20+0~Xh&o>>d7G9GuMQ2z#OOGjKoV***HB z{r&x|gnZ6M5W`cHf#`1~WAdLXBKiP%Qc;!RWpfZz6?wzNH2Di5~f)4k`J=E}kVPuO651_I2o!ybTGm zrd`;ZV)TrA@%X!y8n>LsTB0$39Ebe~tWZAW`Sq%)Fa1E4&Gx%HfQWAT04VGmnnrqP z@bA+p1<~Aru_dCrupBQL$ z;m+5(<8g79p7st930eP+gb#NgXcCv%U1ht;2rmiu48$pbFfUDZD)mgQP_hL&H|*3o zP0x>81S2vm>S;v)=l#`J&U@i*+u&9m(eV$7Q9%}hlVT?meaHAPlYl_rU|xg-Dj)3@ zkwB&I>7}2~Rog)PYk)xz6B~;|S!pzsyyGi!E!|k8@1|hsVgXOi`mC}jKig?=B!1k1 z<)V1EX2WI!Y|U%*1~Dxy$2r1w#r%9y-p=iXK>a;IfVX7~X`bbgpKXU#%_`mL6AQ%J z1F)_ANp0Rb-ToRYjT$btX|4J%XjiX$FNXJY!oASv z|> zGp>=d$Ls;4XJ6QnZI`=k+U>_gK>xT;2YufHWF@TYqeLV>?}yfWC@9^~J--W3@wbRZ z5X!4yZKuEo1L|nG2zn@}+OTI@U4h#7cF-I@QQ$de>{TB{e{SQ@2lEI|7FuZapS6ad zmS%ToNePs$Gs5D-(^Ev=MczN%Z*B=64BrCas%uCyg*1fyKd@jf(Mmb>9nyb!?M_2s zNQOPSNlpH6UDP_d=IHn-b&194w*yJc=ed(T!M&h$tD0f5gCv^kq|he z^gwnV3D~O6c)|thHE~h2HyJI02NHe#k`oiBT3I`k1t05bgf@FWbjZK{TBWeC@Xs5M zwrTQN(`&wbD0n%Bu*STJgjrT+!t8U`w-Pp46Q5WQwLbFxyUt(vY_85~B` z8$k9r`C|>R@1#LyKo;z?jO1ZY>mzQ!=J;nM0jaV2gkSnsUNyM4*dhuGJdSz~NI1L= z8v!MG{1orvLTq--=7PDNP z5q(z1%I=_ArIg?K`P5e(nGW}Q-9829m1!F3`nOK&j3Lu=p8tW0FGhEBw|($Hf^Sjy z1;NLPq~(Y7(1|y;j**<0YFQ-$N7$7!Ll#G}Q9a34wz9%~)vUG)p#on2?kJ zgOHD^)jnCTF;aZ@Cjqf-bdKQBAZsWK%1Hv=U+;J7AxQ$?{uS+S+R=L2(XPHLzh8Q; zr3Zlhb0WPoMcI)For-D7N3!#`e$<3B`0zXD;hG#xPe-0;-Ul1o`|JC(Zg0G)!%(~Fx|bXFtl0bg_L)mb>dLg zySg*hcXUIf@Inr4>|ms{x%oJk&gaXvDy#;_N)`Bkd|wH_EDtgzmF1A~y&XGgXc?IQ z^`BZ+8uw6{iZWR|K&U(DS5H{j7xm{U25#JV$U`1uwQOZ|=PSz`q|#WC#+_cKBC-O% zwo06to}PBtZ|)jzeka~hHIaBj3GIXt$2P>VT*j@=5Pjv}q)3pvlB8ErKFjIkz^3?a ztCi`CFvHm6==m5sp>kU!-_7uTX=jyRe=b4GV)*tm zyQ7-;(G7#NmPUxaEJ<+p0 z_9N**w!a?Tq1&QC&!GqzjZ++z<>lVztQ_~*m>31}oXs4~9ap!AHAP!wIk|S5z|f`c zjBw)v1OL80w)=3e*6{!{gmH}|ar$8|XC@EYurbz;t8XKE;vBSEFnuu(?`b87d3o$t zYi#Qq8zQ1o94}oNx-nL!gB)C^NM(VXvrCt+kVVp;@+k!)_%6bz{qWSDt~y;>1)Dy- zp0OB=LS!s6Bz!*wTLba*eUVLx$6SfzuhWfuruf6k-tPIxT{>FmZ#7kGz9rtNhns`~ zV{iCG7@#2V`T2Q+bVXR03$>Q_YB_iNQ%KGpK0Xz|c6}SOm15V15A1;(U^)1t;q~_V zFo}YfNxS&b8~MKU=D~>bDk9VdadB@g+{ zsyZL}M4kw|%|f$LoU@OjG(2rhubtZDw5l8e^6nNVph>4^p!-UC*Wqc4^=)md09H}) zfF%C0zkM#UEzjq7Wn*Xt!}v(yTic z3SDMoZr=kC#>Ts)cG;c%FzlK7n6W}Wgl?Cst1BJUN||_^O7{egfX@+$L@7uUR@SDE zjkr*u*z?NEW4NctBKx^`<{4-z3$*l;j;!zuC**xNB9dPzBJrv@mF=KBLQ7j6xLJ)7 zW8WERk-Z$Mrtu_QT&AL?W;mdG07R_nfOc`RlqVZ*<)%|Jn^LKC}VVJ+M(J5qmGyE6<5;RBfm~<>Rl;v~RdENDo#(JV%SABv?h3a(w z5Nfc|U80Z)-Bn%$vk~?OzcCU zEX?n9{GH8OjpB)GE7dN@bU#Y>*tuUiyL&r=dXKen*FuPBQsE6`FV;kbg+Xg~=vyUx z@@m!c0uH?uXJY+w@TbZ8NEXK$9|kV4?q7ae_db3n=xu5sno`Lv(-RXDjs7BoQ52q{ zNFKeod|z3+HyA}5O4n+NyVhXH9cm8aIW39NqzUHwF8aZa=NmC|9tx|Zc|@5xDnM~B z{OZjYZkIkhu`2u{Kc?=wOTIt9Gtr$SZ|wu|lNtBPSNqYze{GTmp#oP6@rImfg)Yo% zb)TaG!*=~e55!<5i@SlL{ED?0FS*@_s|a0{#i9$dbo$Lxsm_>AhCp5pW!yauRmF*| zNh7O)Xwa;iePt-2QL$k|-|b;Cr3F7E-e6-C$?YIQAub6~a;aE)C(Rh0FHI6cu|Ex$ z(4F;%5I&s?f~)jf4@dNU(2Zi5TM#Av zBLkeZ{9It^oXLx^>O~HnUt5yaEJPW8*ge(5@N-91eYGDegm_73QreK2EvWBFdIhOs zoUcWTO4B&jK4~W(7jqv8d_@2Tm2Q&+!{_d^1r~~-)%B`L|LD zou@)&!7wn(+^f9;s6wsc3@_6`eXr$si``V$$ShqwJrw7Rs7=Jx>s|&SAk6EFyNmhp zgLu+mDzS~0f#*^N6a2`s(7W_PK0n`uHRisxTh5*t72O_KgYD$#ydt%{0*n*0heu14 zT|WO^#zz=0WN>gWUlFz%gN&sz{aYjDr5Ub)^>fpnLJ-dwFiK6w;x z)5eCuz?wGlSJ&6qNj*^(ld=~H)Qr26Mu!d*IYF7Z%E#<%(^arzg*?AcZD}(fx;;*F zVLv2Pe~7)Q(CJZ)7<(D~BZu5VpnmBEY7@ZoT$vtv$3(DNTUasQM8(F$C(6hb^8Qaw z1{$ro@4ec4%Cykl-d?Pz{bYTct{}T!wT39WG+Z%YP3HEo7OEZC zNpmKMFkIv63Q=2jKU=5kbk=28Z2B9u81v%-949@xs9P7a*BYdGy7!^PxeUl7xUDY< z451kr%$@YCyZU>Ifsnk+O)bMv2EDEY8IeJJd^{Gb0?hwD0DU|Ajo2{`u4QM36I9pb zEc3Ut;>BF07&|Y%T$26#m1f|^K}KL@%um%wJA$->fU|RPg~SG02|lRy7L6&p?*{b= znFP5=t`}j*ril6iQORBXo29z6Dtao2J`B*=yDk`#DriESj7~4q zDV@`s@TD|co~CNWjN?(Cv}OkTN)vJ=&HK%It-^Z}dIp|?g6RHug%g|o6ooH2K;?tp;WVharYBNFzG?jRs%jy2=lp>7p;ew^9ATjn7LDT4dP;VF zMx=iX-L%?j83KKOWde4*ykJl0g(UIB+n^c|q(XC@^uiiBD!+b6d8CilmnQYExL>5T z2YxdsXQx&K+O64krrN#v3T7(tZwLCrM+)xO#vIBTy3qRAW__aqAF>Ntfne~MVDdCx zEE#ay2lNrB9f5dg@-tp^mtu^?hO6DRncz3a+(U&o*U7`j9t;uvC|U#e&r@xidtj2$ z(U;PlaPZ(kkIrX!=Yl2Xq_xDetAq@N2b)DpZ8xBEn-CJ!)yyn&J_u23pABnbaesw! zf#BY{?~>MYWyS>nIdp1~LvMe=3|u!&35xJe19#*PNsJGS$4s1G$L;&UCV&6z$Vb|y z9Riph2eg-hjY2@Fh2Vj7*72o?skz>ejDKjxK|4KSYp;V^Z~BriRzwed4U~C0vB-1E zJ!H;gcR}hNLgaAU9w%RNyS!)i@GNjG{)V@UW=shPll8cyvXVF{s?(tyS*xquDdB0D zZn`+{PiD?u>zDk} and check out all the available platform that can simplify plugin development. +## Developer documentation + +### High-level documentation + +#### Structure + +Refer to [divio documentation](https://documentation.divio.com/) for guidance on where and how to structure our high-level documentation. + + and + sections are both _explanation_ oriented, + covers both _tutorials_ and _How to_, and +the section covers _reference_ material. + +#### Location + +If the information spans multiple plugins, consider adding it to the [dev_docs](https://github.com/elastic/kibana/tree/master/dev_docs) folder. If it is plugin specific, consider adding it inside the plugin folder. Write it in an mdx file if you would like it to show up in our new (beta) documentation system. + + + +To add docs into the new docs system, create an `.mdx` file that +contains . Read about the syntax . An extra step is needed to add a menu item. will walk you through how to set the docs system +up locally and edit the nav menu. + + + +#### Keep content fresh + +A fresh pair of eyes are invaluable. Recruit new hires to read, review and update documentation. Leads should also periodically review documentation to ensure it stays up to date. File issues any time you notice documentation is outdated. + +#### Consider your target audience + +Documentation in the Kibana Developer Guide is targeted towards developers building Kibana plugins. Keep implementation details about internal plugin code out of these docs. + +#### High to low level + +When a developer first lands in our docs, think about their journey. Introduce basic concepts before diving into details. The left navigation should be set up so documents on top are higher level than documents near the bottom. + +#### Think outside-in + +It's easy to forget what it felt like to first write code in Kibana, but do your best to frame these docs "outside-in". Don't use esoteric, internal language unless a definition is documented and linked. The fresh eyes of a new hire can be a great asset. + +### API documentation + +We automatically generate . The following guidelines will help ensure your are useful. + +#### Code comments + +Every publicly exposed function, class, interface, type, parameter and property should have a comment using JSDoc style comments. + +- Use `@param` tags for every function parameter. +- Use `@returns` tags for return types. +- Use `@throws` when appropriate. +- Use `@beta` or `@deprecated` when appropriate. +- Use `@internal` to indicate this API item is intended for internal use only, which will also remove it from the docs. + +#### Interfaces vs inlined types + +Prefer types and interfaces over complex inline objects. For example, prefer: + +```ts +/** +* The SearchSpec interface contains settings for creating a new SearchService, like +* username and password. +*/ +export interface SearchSpec { + /** + * Stores the username. Duh, + */ + username: string; + /** + * Stores the password. I hope it's encrypted! + */ + password: string; +} + + /** + * Retrieve search services + * @param searchSpec Configuration information for initializing the search service. + * @returns the id of the search service + */ +export getSearchService: (searchSpec: SearchSpec) => string; +``` + +over: + +```ts +/** + * Retrieve search services + * @param searchSpec Configuration information for initializing the search service. + * @returns the id of the search service + */ +export getSearchService: (searchSpec: { username: string; password: string }) => string; +``` + +In the former, there will be a link to the `SearchSpec` interface with documentation for the `username` and `password` properties. In the latter the object will render inline, without comments: + +![prefer interfaces documentation](./assets/dev_docs_nested_object.png) + +#### Export every type used in a public API + +When a publicly exported API items references a private type, this results in a broken link in our docs system. The private type is, by proxy, part of your public API, and as such, should be exported. + +Do: + +```ts +export interface AnInterface { bar: string }; +export type foo: string | AnInterface; +``` + +Don't: + +```ts +interface AnInterface { bar: string }; +export type foo: string | AnInterface; +``` + +#### Avoid “Pick” + +`Pick` not only ends up being unhelpful in our documentation system, but it's also of limited help in your IDE. For that reason, avoid `Pick` and other similarly complex types on your public API items. Using these semantics internally is fine. + +![pick api documentation](./assets/api_doc_pick.png) + +### Example plugins + +Running Kibana with `yarn start --run-examples` will include all [example plugins](https://github.com/elastic/kibana/tree/master/examples). These are tested examples of platform services in use. We strongly encourage anyone providing a platform level service or to include a tutorial that links to a tested example plugin. This is better than relying on copied code snippets, which can quickly get out of date. + ## Performance Build with scalability in mind. From 32212644da19833cd033d018af7ade17f1a1a029 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 26 Mar 2021 08:11:35 +0100 Subject: [PATCH 20/98] [Application Usage] use incrementCounter on daily reports instead of creating transactional documents (#94923) * initial implementation * add test for upsertAttributes * update generated doc * remove comment * removal transactional documents from the collector aggregation logic * rename generic type * add missing doc file * split rollups into distinct files * for api integ test * extract types to their own file * only roll transactional documents during startup * proper fix is better fix * perform daily rolling until success * unskip flaky test * fix unit tests --- ...ver.savedobjectsincrementcounteroptions.md | 3 +- ...ncrementcounteroptions.upsertattributes.md | 13 + ...savedobjectsrepository.incrementcounter.md | 18 +- .../service/lib/repository.test.js | 28 +++ .../saved_objects/service/lib/repository.ts | 58 ++++- src/core/server/server.api.md | 5 +- .../collectors/application_usage/constants.ts | 10 +- .../collectors/application_usage/index.ts | 1 + .../daily.test.ts} | 205 +--------------- .../{rollups.ts => rollups/daily.ts} | 126 ++-------- .../application_usage/rollups/index.ts | 11 + .../application_usage/rollups/total.test.ts | 194 +++++++++++++++ .../application_usage/rollups/total.ts | 106 ++++++++ .../application_usage/rollups/utils.ts | 11 + .../application_usage/saved_objects_types.ts | 6 +- .../collectors/application_usage/schema.ts | 2 +- ...emetry_application_usage_collector.test.ts | 228 ++++++------------ .../telemetry_application_usage_collector.ts | 72 ++---- .../collectors/application_usage/types.ts | 40 +++ .../common/application_usage.ts | 23 ++ .../usage_collection/server/report/schema.ts | 20 +- .../report/store_application_usage.test.ts | 115 +++++++++ .../server/report/store_application_usage.ts | 87 +++++++ .../server/report/store_report.test.mocks.ts | 12 + .../server/report/store_report.test.ts | 53 ++-- .../server/report/store_report.ts | 22 +- .../apis/telemetry/telemetry_local.ts | 11 +- 27 files changed, 880 insertions(+), 600 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md rename src/plugins/kibana_usage_collection/server/collectors/application_usage/{rollups.test.ts => rollups/daily.test.ts} (51%) rename src/plugins/kibana_usage_collection/server/collectors/application_usage/{rollups.ts => rollups/daily.ts} (55%) create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts create mode 100644 src/plugins/usage_collection/common/application_usage.ts create mode 100644 src/plugins/usage_collection/server/report/store_application_usage.test.ts create mode 100644 src/plugins/usage_collection/server/report/store_application_usage.ts create mode 100644 src/plugins/usage_collection/server/report/store_report.test.mocks.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index 68e9bb09456cd..8da2458cf007e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions ``` ## Properties @@ -18,4 +18,5 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt | [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | | [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | | [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | +| [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) | Attributes | Attributes to use when upserting the document if it doesn't exist. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md new file mode 100644 index 0000000000000..d5657dd65771f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) > [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) + +## SavedObjectsIncrementCounterOptions.upsertAttributes property + +Attributes to use when upserting the document if it doesn't exist. + +Signature: + +```typescript +upsertAttributes?: Attributes; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index eb18e064c84e2..59d98bf4d607b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -9,7 +9,7 @@ Increments all the specified counter fields (by one by default). Creates the doc Signature: ```typescript -incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; +incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; ``` ## Parameters @@ -19,7 +19,7 @@ incrementCounter(type: string, id: string, counterFields: Arraystring | The type of saved object whose fields should be incremented | | id | string | The id of the document whose fields should be incremented | | counterFields | Array<string | SavedObjectsIncrementCounterField> | An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) | -| options | SavedObjectsIncrementCounterOptions | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | +| options | SavedObjectsIncrementCounterOptions<T> | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | Returns: @@ -52,5 +52,19 @@ repository 'stats.apiCalls', ]) +// Increment the apiCalls field counter by 4 +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + { fieldName: 'stats.apiCalls' incrementBy: 4 }, + ]) + +// Initialize the document with arbitrary fields if not present +repository.incrementCounter<{ appId: string }>( + 'dashboard_counter_type', + 'counter_id', + [ 'stats.apiCalls'], + { upsertAttributes: { appId: 'myId' } } +) + ``` diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 37572c83e4c88..ce48e8dc9a317 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -23,6 +23,7 @@ import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { esKuery } from '../../es_query'; import { errors as EsErrors } from '@elastic/elasticsearch'; + const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -3654,6 +3655,33 @@ describe('SavedObjectsRepository', () => { ); }); + it(`uses the 'upsertAttributes' option when specified`, async () => { + const upsertAttributes = { + foo: 'bar', + hello: 'dolly', + }; + await incrementCounterSuccess(type, id, counterFields, { namespace, upsertAttributes }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + upsert: expect.objectContaining({ + [type]: { + foo: 'bar', + hello: 'dolly', + ...counterFields.reduce((aggs, field) => { + return { + ...aggs, + [field]: 1, + }; + }, {}), + }, + }), + }), + }), + expect.anything() + ); + }); + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index aa1e62c1652ca..6e2a1d6ec0511 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -76,10 +76,16 @@ import { // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Left = { tag: 'Left'; error: Record }; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type Right = { tag: 'Right'; value: Record }; +interface Left { + tag: 'Left'; + error: Record; +} + +interface Right { + tag: 'Right'; + value: Record; +} + type Either = Left | Right; const isLeft = (either: Either): either is Left => either.tag === 'Left'; const isRight = (either: Either): either is Right => either.tag === 'Right'; @@ -98,7 +104,8 @@ export interface SavedObjectsRepositoryOptions { /** * @public */ -export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { +export interface SavedObjectsIncrementCounterOptions + extends SavedObjectsBaseOptions { /** * (default=false) If true, sets all the counter fields to 0 if they don't * already exist. Existing fields will be left as-is and won't be incremented. @@ -111,6 +118,10 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt * operation. See {@link MutatingOperationRefreshSetting} */ refresh?: MutatingOperationRefreshSetting; + /** + * Attributes to use when upserting the document if it doesn't exist. + */ + upsertAttributes?: Attributes; } /** @@ -1694,6 +1705,20 @@ export class SavedObjectsRepository { * .incrementCounter('dashboard_counter_type', 'counter_id', [ * 'stats.apiCalls', * ]) + * + * // Increment the apiCalls field counter by 4 + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * { fieldName: 'stats.apiCalls' incrementBy: 4 }, + * ]) + * + * // Initialize the document with arbitrary fields if not present + * repository.incrementCounter<{ appId: string }>( + * 'dashboard_counter_type', + * 'counter_id', + * [ 'stats.apiCalls'], + * { upsertAttributes: { appId: 'myId' } } + * ) * ``` * * @param type - The type of saved object whose fields should be incremented @@ -1706,7 +1731,7 @@ export class SavedObjectsRepository { type: string, id: string, counterFields: Array, - options: SavedObjectsIncrementCounterOptions = {} + options: SavedObjectsIncrementCounterOptions = {} ): Promise> { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); @@ -1728,12 +1753,16 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options; + const { + migrationVersion, + refresh = DEFAULT_REFRESH_SETTING, + initialize = false, + upsertAttributes, + } = options; const normalizedCounterFields = counterFields.map((counterField) => { const fieldName = typeof counterField === 'string' ? counterField : counterField.fieldName; const incrementBy = typeof counterField === 'string' ? 1 : counterField.incrementBy || 1; - return { fieldName, incrementBy: initialize ? 0 : incrementBy, @@ -1757,11 +1786,14 @@ export class SavedObjectsRepository { type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: normalizedCounterFields.reduce((acc, counterField) => { - const { fieldName, incrementBy } = counterField; - acc[fieldName] = incrementBy; - return acc; - }, {} as Record), + attributes: { + ...(upsertAttributes ?? {}), + ...normalizedCounterFields.reduce((acc, counterField) => { + const { fieldName, incrementBy } = counterField; + acc[fieldName] = incrementBy; + return acc; + }, {} as Record), + }, migrationVersion, updated_at: time, }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 73f8a44075162..cf1647ef5cec3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2735,11 +2735,12 @@ export interface SavedObjectsIncrementCounterField { } // @public (undocumented) -export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { initialize?: boolean; // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; refresh?: MutatingOperationRefreshSetting; + upsertAttributes?: Attributes; } // @public @@ -2839,7 +2840,7 @@ export class SavedObjectsRepository { // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; + incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts index 1910ba054bf8e..f072f044925bf 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts @@ -12,15 +12,9 @@ export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; /** - * Roll daily indices every 30 minutes. - * This means that, assuming a user can visit all the 44 apps we can possibly report - * in the 3 minutes interval the browser reports to the server, up to 22 users can have the same - * behaviour and we wouldn't need to paginate in the transactional documents (less than 10k docs). - * - * Based on a more normal expected use case, the users could visit up to 5 apps in those 3 minutes, - * allowing up to 200 users before reaching the limit. + * Roll daily indices every 24h */ -export const ROLL_DAILY_INDICES_INTERVAL = 30 * 60 * 1000; +export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; /** * Start rolling indices after 5 minutes up diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts index 676f5fddc16e1..2d2d07d9d1894 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts @@ -7,3 +7,4 @@ */ export { registerApplicationUsageCollector } from './telemetry_application_usage_collector'; +export { rollDailyData as migrateTransactionalDocs } from './rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts similarity index 51% rename from src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts index 7d86bc41e0b90..5acd1fb9c9c3a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts @@ -6,21 +6,16 @@ * Side Public License, v 1. */ -import { rollDailyData, rollTotals } from './rollups'; -import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; -import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; -import { SavedObjectsErrorHelpers } from '../../../../../core/server'; -import { - SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './saved_objects_types'; +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../../core/server/mocks'; +import { SavedObjectsErrorHelpers } from '../../../../../../core/server'; +import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TRANSACTIONAL_TYPE } from '../saved_objects_types'; +import { rollDailyData } from './daily'; describe('rollDailyData', () => { const logger = loggingSystemMock.createLogger(); - test('returns undefined if no savedObjectsClient initialised yet', async () => { - await expect(rollDailyData(logger, undefined)).resolves.toBe(undefined); + test('returns false if no savedObjectsClient initialised yet', async () => { + await expect(rollDailyData(logger, undefined)).resolves.toBe(false); }); test('handle empty results', async () => { @@ -33,7 +28,7 @@ describe('rollDailyData', () => { throw new Error(`Unexpected type [${type}]`); } }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); expect(savedObjectClient.get).not.toBeCalled(); expect(savedObjectClient.bulkCreate).not.toBeCalled(); expect(savedObjectClient.delete).not.toBeCalled(); @@ -101,7 +96,7 @@ describe('rollDailyData', () => { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); expect(savedObjectClient.get).toHaveBeenCalledTimes(2); expect(savedObjectClient.get).toHaveBeenNthCalledWith( 1, @@ -196,7 +191,7 @@ describe('rollDailyData', () => { throw new Error('Something went terribly wrong'); }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(false); expect(savedObjectClient.get).toHaveBeenCalledTimes(1); expect(savedObjectClient.get).toHaveBeenCalledWith( SAVED_OBJECTS_DAILY_TYPE, @@ -206,185 +201,3 @@ describe('rollDailyData', () => { expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); }); }); - -describe('rollTotals', () => { - const logger = loggingSystemMock.createLogger(); - - test('returns undefined if no savedObjectsClient initialised yet', async () => { - await expect(rollTotals(logger, undefined)).resolves.toBe(undefined); - }); - - test('handle empty results', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_DAILY_TYPE: - case SAVED_OBJECTS_TOTAL_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); - }); - - test('migrate some documents', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_DAILY_TYPE: - return { - saved_objects: [ - { - id: 'appId-2:2020-01-01', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-2', - timestamp: '2020-01-01T10:31:00.000Z', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'appId-1:2020-01-01', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 2.5, - numberOfClicks: 2, - }, - }, - { - id: 'appId-1:2020-01-01:viewId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - viewId: 'viewId-1', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }, - ], - total: 3, - page, - per_page: perPage, - }; - case SAVED_OBJECTS_TOTAL_TYPE: - return { - saved_objects: [ - { - id: 'appId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'appId-1___viewId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-1', - viewId: 'viewId-1', - minutesOnScreen: 4, - numberOfClicks: 2, - }, - }, - { - id: 'appId-2___viewId-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId-2', - viewId: 'viewId-1', - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }, - ], - total: 3, - page, - per_page: perPage, - }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-1', - attributes: { - appId: 'appId-1', - viewId: MAIN_APP_DEFAULT_VIEW_ID, - minutesOnScreen: 3.0, - numberOfClicks: 3, - }, - }, - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-1___viewId-1', - attributes: { - appId: 'appId-1', - viewId: 'viewId-1', - minutesOnScreen: 5.0, - numberOfClicks: 3, - }, - }, - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-2___viewId-1', - attributes: { - appId: 'appId-2', - viewId: 'viewId-1', - minutesOnScreen: 1.0, - numberOfClicks: 1, - }, - }, - { - type: SAVED_OBJECTS_TOTAL_TYPE, - id: 'appId-2', - attributes: { - appId: 'appId-2', - viewId: MAIN_APP_DEFAULT_VIEW_ID, - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(3); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId-2:2020-01-01' - ); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId-1:2020-01-01' - ); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId-1:2020-01-01:viewId-1' - ); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts similarity index 55% rename from src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts rename to src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts index df7e7662b49cf..a7873c7d5dfe9 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts @@ -6,18 +6,20 @@ * Side Public License, v 1. */ -import { ISavedObjectsRepository, SavedObject, Logger } from 'kibana/server'; import moment from 'moment'; +import type { Logger } from '@kbn/logging'; +import { + ISavedObjectsRepository, + SavedObject, + SavedObjectsErrorHelpers, +} from '../../../../../../core/server'; +import { getDailyId } from '../../../../../usage_collection/common/application_usage'; import { ApplicationUsageDaily, - ApplicationUsageTotal, ApplicationUsageTransactional, SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TOTAL_TYPE, SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './saved_objects_types'; -import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; -import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; +} from '../saved_objects_types'; /** * For Rolling the daily data, we only care about the stored attributes and the version (to avoid overwriting via concurrent requests) @@ -27,18 +29,17 @@ type ApplicationUsageDailyWithVersion = Pick< 'version' | 'attributes' >; -export function serializeKey(appId: string, viewId: string) { - return `${appId}___${viewId}`; -} - /** * Aggregates all the transactional events into daily aggregates * @param logger * @param savedObjectsClient */ -export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { +export async function rollDailyData( + logger: Logger, + savedObjectsClient?: ISavedObjectsRepository +): Promise { if (!savedObjectsClient) { - return; + return false; } try { @@ -58,10 +59,7 @@ export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedO } = doc; const dayId = moment(timestamp).format('YYYY-MM-DD'); - const dailyId = - !viewId || viewId === MAIN_APP_DEFAULT_VIEW_ID - ? `${appId}:${dayId}` - : `${appId}:${dayId}:${viewId}`; + const dailyId = getDailyId({ dayId, appId, viewId }); const existingDoc = toCreate.get(dailyId) || @@ -103,9 +101,11 @@ export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedO } } } while (toCreate.size > 0); + return true; } catch (err) { logger.debug(`Failed to rollup transactional to daily entries`); logger.debug(err); + return false; } } @@ -125,7 +125,11 @@ async function getDailyDoc( dayId: string ): Promise { try { - return await savedObjectsClient.get(SAVED_OBJECTS_DAILY_TYPE, id); + const { attributes, version } = await savedObjectsClient.get( + SAVED_OBJECTS_DAILY_TYPE, + id + ); + return { attributes, version }; } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return { @@ -142,91 +146,3 @@ async function getDailyDoc( throw err; } } - -/** - * Moves all the daily documents into aggregated "total" documents as we don't care about any granularity after 90 days - * @param logger - * @param savedObjectsClient - */ -export async function rollTotals(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { - if (!savedObjectsClient) { - return; - } - - try { - const [ - { saved_objects: rawApplicationUsageTotals }, - { saved_objects: rawApplicationUsageDaily }, - ] = await Promise.all([ - savedObjectsClient.find({ - perPage: 10000, - type: SAVED_OBJECTS_TOTAL_TYPE, - }), - savedObjectsClient.find({ - perPage: 10000, - type: SAVED_OBJECTS_DAILY_TYPE, - filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.timestamp < now-90d`, - }), - ]); - - const existingTotals = rawApplicationUsageTotals.reduce( - ( - acc, - { - attributes: { appId, viewId = MAIN_APP_DEFAULT_VIEW_ID, numberOfClicks, minutesOnScreen }, - } - ) => { - const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); - - return { - ...acc, - // No need to sum because there should be 1 document per appId only - [key]: { appId, viewId, numberOfClicks, minutesOnScreen }, - }; - }, - {} as Record< - string, - { appId: string; viewId: string; minutesOnScreen: number; numberOfClicks: number } - > - ); - - const totals = rawApplicationUsageDaily.reduce((acc, { attributes }) => { - const { - appId, - viewId = MAIN_APP_DEFAULT_VIEW_ID, - numberOfClicks, - minutesOnScreen, - } = attributes; - const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); - const existing = acc[key] || { minutesOnScreen: 0, numberOfClicks: 0 }; - - return { - ...acc, - [key]: { - appId, - viewId, - numberOfClicks: numberOfClicks + existing.numberOfClicks, - minutesOnScreen: minutesOnScreen + existing.minutesOnScreen, - }, - }; - }, existingTotals); - - await Promise.all([ - Object.entries(totals).length && - savedObjectsClient.bulkCreate( - Object.entries(totals).map(([id, entry]) => ({ - type: SAVED_OBJECTS_TOTAL_TYPE, - id, - attributes: entry, - })), - { overwrite: true } - ), - ...rawApplicationUsageDaily.map( - ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_DAILY_TYPE, id) // There is no bulkDelete :( - ), - ]); - } catch (err) { - logger.debug(`Failed to rollup daily entries to totals`); - logger.debug(err); - } -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts new file mode 100644 index 0000000000000..8f3d83613aa9d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { rollDailyData } from './daily'; +export { rollTotals } from './total'; +export { serializeKey } from './utils'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts new file mode 100644 index 0000000000000..9fea955ab5d8a --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../../core/server/mocks'; +import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../../usage_collection/common/constants'; +import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TOTAL_TYPE } from '../saved_objects_types'; +import { rollTotals } from './total'; + +describe('rollTotals', () => { + const logger = loggingSystemMock.createLogger(); + + test('returns undefined if no savedObjectsClient initialised yet', async () => { + await expect(rollTotals(logger, undefined)).resolves.toBe(undefined); + }); + + test('handle empty results', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_DAILY_TYPE: + case SAVED_OBJECTS_TOTAL_TYPE: + return { saved_objects: [], total: 0, page, per_page: perPage }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); + }); + + test('migrate some documents', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_DAILY_TYPE: + return { + saved_objects: [ + { + id: 'appId-2:2020-01-01', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-2', + timestamp: '2020-01-01T10:31:00.000Z', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + { + id: 'appId-1:2020-01-01', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + timestamp: '2020-01-01T11:31:00.000Z', + minutesOnScreen: 2.5, + numberOfClicks: 2, + }, + }, + { + id: 'appId-1:2020-01-01:viewId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + viewId: 'viewId-1', + timestamp: '2020-01-01T11:31:00.000Z', + minutesOnScreen: 1, + numberOfClicks: 1, + }, + }, + ], + total: 3, + page, + per_page: perPage, + }; + case SAVED_OBJECTS_TOTAL_TYPE: + return { + saved_objects: [ + { + id: 'appId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + { + id: 'appId-1___viewId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + viewId: 'viewId-1', + minutesOnScreen: 4, + numberOfClicks: 2, + }, + }, + { + id: 'appId-2___viewId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-2', + viewId: 'viewId-1', + minutesOnScreen: 1, + numberOfClicks: 1, + }, + }, + ], + total: 3, + page, + per_page: perPage, + }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-1', + attributes: { + appId: 'appId-1', + viewId: MAIN_APP_DEFAULT_VIEW_ID, + minutesOnScreen: 3.0, + numberOfClicks: 3, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-1___viewId-1', + attributes: { + appId: 'appId-1', + viewId: 'viewId-1', + minutesOnScreen: 5.0, + numberOfClicks: 3, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-2___viewId-1', + attributes: { + appId: 'appId-2', + viewId: 'viewId-1', + minutesOnScreen: 1.0, + numberOfClicks: 1, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-2', + attributes: { + appId: 'appId-2', + viewId: MAIN_APP_DEFAULT_VIEW_ID, + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + { overwrite: true } + ); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(3); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-2:2020-01-01' + ); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-1:2020-01-01' + ); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-1:2020-01-01:viewId-1' + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts new file mode 100644 index 0000000000000..e27c7b897d995 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/total.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Logger } from '@kbn/logging'; +import type { ISavedObjectsRepository } from 'kibana/server'; +import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../../usage_collection/common/constants'; +import { + ApplicationUsageDaily, + ApplicationUsageTotal, + SAVED_OBJECTS_DAILY_TYPE, + SAVED_OBJECTS_TOTAL_TYPE, +} from '../saved_objects_types'; +import { serializeKey } from './utils'; + +/** + * Moves all the daily documents into aggregated "total" documents as we don't care about any granularity after 90 days + * @param logger + * @param savedObjectsClient + */ +export async function rollTotals(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { + if (!savedObjectsClient) { + return; + } + + try { + const [ + { saved_objects: rawApplicationUsageTotals }, + { saved_objects: rawApplicationUsageDaily }, + ] = await Promise.all([ + savedObjectsClient.find({ + perPage: 10000, + type: SAVED_OBJECTS_TOTAL_TYPE, + }), + savedObjectsClient.find({ + perPage: 10000, + type: SAVED_OBJECTS_DAILY_TYPE, + filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.timestamp < now-90d`, + }), + ]); + + const existingTotals = rawApplicationUsageTotals.reduce( + ( + acc, + { + attributes: { appId, viewId = MAIN_APP_DEFAULT_VIEW_ID, numberOfClicks, minutesOnScreen }, + } + ) => { + const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); + + return { + ...acc, + // No need to sum because there should be 1 document per appId only + [key]: { appId, viewId, numberOfClicks, minutesOnScreen }, + }; + }, + {} as Record< + string, + { appId: string; viewId: string; minutesOnScreen: number; numberOfClicks: number } + > + ); + + const totals = rawApplicationUsageDaily.reduce((acc, { attributes }) => { + const { + appId, + viewId = MAIN_APP_DEFAULT_VIEW_ID, + numberOfClicks, + minutesOnScreen, + } = attributes; + const key = viewId === MAIN_APP_DEFAULT_VIEW_ID ? appId : serializeKey(appId, viewId); + const existing = acc[key] || { minutesOnScreen: 0, numberOfClicks: 0 }; + + return { + ...acc, + [key]: { + appId, + viewId, + numberOfClicks: numberOfClicks + existing.numberOfClicks, + minutesOnScreen: minutesOnScreen + existing.minutesOnScreen, + }, + }; + }, existingTotals); + + await Promise.all([ + Object.entries(totals).length && + savedObjectsClient.bulkCreate( + Object.entries(totals).map(([id, entry]) => ({ + type: SAVED_OBJECTS_TOTAL_TYPE, + id, + attributes: entry, + })), + { overwrite: true } + ), + ...rawApplicationUsageDaily.map( + ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_DAILY_TYPE, id) // There is no bulkDelete :( + ), + ]); + } catch (err) { + logger.debug(`Failed to rollup daily entries to totals`); + logger.debug(err); + } +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts new file mode 100644 index 0000000000000..8be00e6287883 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/utils.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function serializeKey(appId: string, viewId: string) { + return `${appId}___${viewId}`; +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index 9e71b5c3b032e..f2b996f3af97a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server'; +import type { SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server'; /** * Used for accumulating the totals of all the stats older than 90d @@ -17,6 +17,7 @@ export interface ApplicationUsageTotal extends SavedObjectAttributes { minutesOnScreen: number; numberOfClicks: number; } + export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; /** @@ -25,6 +26,8 @@ export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; export interface ApplicationUsageTransactional extends ApplicationUsageTotal { timestamp: string; } + +/** @deprecated transactional type is no longer used, and only preserved for backward compatibility */ export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional'; /** @@ -62,6 +65,7 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe }); // Type for storing ApplicationUsageTransactional (declaring empty mappings because we don't use the internal fields for query/aggregations) + // Remark: this type is deprecated and only here for BWC reasons. registerType({ name: SAVED_OBJECTS_TRANSACTIONAL_TYPE, hidden: false, diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 062d751ef454c..693e9132fe536 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -7,7 +7,7 @@ */ import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; -import { ApplicationUsageTelemetryReport } from './telemetry_application_usage_collector'; +import { ApplicationUsageTelemetryReport } from './types'; const commonSchema: MakeSchemaFrom = { appId: { type: 'keyword', _meta: { description: 'The application being tracked' } }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index 3e8434d446033..f1b21af5506e6 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -11,74 +11,99 @@ import { Collector, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; -import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; import { registerApplicationUsageCollector, transformByApplicationViews, - ApplicationUsageViews, } from './telemetry_application_usage_collector'; -import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; -import { - SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './saved_objects_types'; +import { ApplicationUsageViews } from './types'; -describe('telemetry_application_usage', () => { - jest.useFakeTimers(); +import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TOTAL_TYPE } from './saved_objects_types'; - const logger = loggingSystemMock.createLogger(); +// use fake timers to avoid triggering rollups during tests +jest.useFakeTimers(); +describe('telemetry_application_usage', () => { + let logger: ReturnType; let collector: Collector; + let usageCollectionMock: ReturnType; + let savedObjectClient: ReturnType; + let getSavedObjectClient: jest.MockedFunction<() => undefined | typeof savedObjectClient>; - const usageCollectionMock = createUsageCollectionSetupMock(); - usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = new Collector(logger, config); - return createUsageCollectionSetupMock().makeUsageCollector(config); - }); - - const getUsageCollector = jest.fn(); const registerType = jest.fn(); const mockedFetchContext = createCollectorFetchContextMock(); - beforeAll(() => - registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) - ); - afterAll(() => jest.clearAllTimers()); + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + usageCollectionMock = createUsageCollectionSetupMock(); + savedObjectClient = savedObjectsRepositoryMock.create(); + getSavedObjectClient = jest.fn().mockReturnValue(savedObjectClient); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + registerApplicationUsageCollector( + logger, + usageCollectionMock, + registerType, + getSavedObjectClient + ); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); test('registered collector is set', () => { expect(collector).not.toBeUndefined(); }); test('if no savedObjectClient initialised, return undefined', async () => { + getSavedObjectClient.mockReturnValue(undefined); + expect(collector.isReady()).toBe(false); expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); - jest.runTimersToTime(ROLL_INDICES_START); }); - test('when savedObjectClient is initialised, return something', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation( - async () => - ({ - saved_objects: [], - total: 0, - } as any) + test('calls `savedObjectsClient.find` with the correct parameters', async () => { + savedObjectClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + per_page: 20, + page: 0, + }); + + await collector.fetch(mockedFetchContext); + + expect(savedObjectClient.find).toHaveBeenCalledTimes(2); + + expect(savedObjectClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + type: SAVED_OBJECTS_TOTAL_TYPE, + }) + ); + expect(savedObjectClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + type: SAVED_OBJECTS_DAILY_TYPE, + }) ); - getUsageCollector.mockImplementation(() => savedObjectClient); + }); - jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run + test('when savedObjectClient is initialised, return something', async () => { + savedObjectClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + per_page: 20, + page: 0, + }); expect(collector.isReady()).toBe(true); expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); - test('it only gets 10k even when there are more documents (ES limitation)', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - const total = 10000; + test('it aggregates total and daily data', async () => { savedObjectClient.find.mockImplementation(async (opts) => { switch (opts.type) { case SAVED_OBJECTS_TOTAL_TYPE: @@ -95,18 +120,6 @@ describe('telemetry_application_usage', () => { ], total: 1, } as any; - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - const doc = { - id: 'test-id', - attributes: { - appId: 'appId', - timestamp: new Date().toISOString(), - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }; - const savedObjects = new Array(total).fill(doc); - return { saved_objects: savedObjects, total: total + 1 }; case SAVED_OBJECTS_DAILY_TYPE: return { saved_objects: [ @@ -125,122 +138,21 @@ describe('telemetry_application_usage', () => { } }); - getUsageCollector.mockImplementation(() => savedObjectClient); - - jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run - expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { appId: 'appId', viewId: 'main', - clicks_total: total + 1 + 10, - clicks_7_days: total + 1, - clicks_30_days: total + 1, - clicks_90_days: total + 1, - minutes_on_screen_total: (total + 1) * 0.5 + 10, - minutes_on_screen_7_days: (total + 1) * 0.5, - minutes_on_screen_30_days: (total + 1) * 0.5, - minutes_on_screen_90_days: (total + 1) * 0.5, + clicks_total: 1 + 10, + clicks_7_days: 1, + clicks_30_days: 1, + clicks_90_days: 1, + minutes_on_screen_total: 0.5 + 10, + minutes_on_screen_7_days: 0.5, + minutes_on_screen_30_days: 0.5, + minutes_on_screen_90_days: 0.5, views: [], }, }); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - id: 'appId', - type: SAVED_OBJECTS_TOTAL_TYPE, - attributes: { - appId: 'appId', - viewId: 'main', - minutesOnScreen: 10.5, - numberOfClicks: 11, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId:YYYY-MM-DD' - ); - }); - - test('old transactional data not migrated yet', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async (opts) => { - switch (opts.type) { - case SAVED_OBJECTS_TOTAL_TYPE: - case SAVED_OBJECTS_DAILY_TYPE: - return { saved_objects: [], total: 0 } as any; - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - return { - saved_objects: [ - { - id: 'test-id', - attributes: { - appId: 'appId', - timestamp: new Date(0).toISOString(), - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'test-id-2', - attributes: { - appId: 'appId', - viewId: 'main', - timestamp: new Date(0).toISOString(), - minutesOnScreen: 2, - numberOfClicks: 2, - }, - }, - { - id: 'test-id-3', - attributes: { - appId: 'appId', - viewId: 'viewId-1', - timestamp: new Date(0).toISOString(), - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }, - ], - total: 1, - }; - } - }); - - getUsageCollector.mockImplementation(() => savedObjectClient); - - expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ - appId: { - appId: 'appId', - viewId: 'main', - clicks_total: 3, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 2.5, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - views: [ - { - appId: 'appId', - viewId: 'viewId-1', - clicks_total: 1, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 1, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - }, - ], - }, - }); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index ee1b42e61a6ca..a01f1bca4f0e0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -11,57 +11,21 @@ import { timer } from 'rxjs'; import { ISavedObjectsRepository, Logger, SavedObjectsServiceSetup } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { MAIN_APP_DEFAULT_VIEW_ID } from '../../../../usage_collection/common/constants'; -import { serializeKey } from './rollups'; - import { ApplicationUsageDaily, ApplicationUsageTotal, - ApplicationUsageTransactional, registerMappings, SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, } from './saved_objects_types'; import { applicationUsageSchema } from './schema'; -import { rollDailyData, rollTotals } from './rollups'; +import { rollTotals, rollDailyData, serializeKey } from './rollups'; import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_DAILY_INDICES_INTERVAL, ROLL_INDICES_START, } from './constants'; - -export interface ApplicationViewUsage { - appId: string; - viewId: string; - clicks_total: number; - clicks_7_days: number; - clicks_30_days: number; - clicks_90_days: number; - minutes_on_screen_total: number; - minutes_on_screen_7_days: number; - minutes_on_screen_30_days: number; - minutes_on_screen_90_days: number; -} - -export interface ApplicationUsageViews { - [serializedKey: string]: ApplicationViewUsage; -} - -export interface ApplicationUsageTelemetryReport { - [appId: string]: { - appId: string; - viewId: string; - clicks_total: number; - clicks_7_days: number; - clicks_30_days: number; - clicks_90_days: number; - minutes_on_screen_total: number; - minutes_on_screen_7_days: number; - minutes_on_screen_30_days: number; - minutes_on_screen_90_days: number; - views?: ApplicationViewUsage[]; - }; -} +import { ApplicationUsageTelemetryReport, ApplicationUsageViews } from './types'; export const transformByApplicationViews = ( report: ApplicationUsageViews @@ -92,6 +56,21 @@ export function registerApplicationUsageCollector( ) { registerMappings(registerType); + timer(ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL).subscribe(() => + rollTotals(logger, getSavedObjectsClient()) + ); + + const dailyRollingSub = timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe( + async () => { + const success = await rollDailyData(logger, getSavedObjectsClient()); + // we only need to roll the transactional documents once to assure BWC + // once we rolling succeeds, we can stop. + if (success) { + dailyRollingSub.unsubscribe(); + } + } + ); + const collector = usageCollection.makeUsageCollector( { type: 'application_usage', @@ -105,7 +84,6 @@ export function registerApplicationUsageCollector( const [ { saved_objects: rawApplicationUsageTotals }, { saved_objects: rawApplicationUsageDaily }, - { saved_objects: rawApplicationUsageTransactional }, ] = await Promise.all([ savedObjectsClient.find({ type: SAVED_OBJECTS_TOTAL_TYPE, @@ -115,10 +93,6 @@ export function registerApplicationUsageCollector( type: SAVED_OBJECTS_DAILY_TYPE, perPage: 10000, // We can have up to 44 apps * 91 days = 4004 docs. This limit is OK }), - savedObjectsClient.find({ - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - perPage: 10000, // If we have more than those, we won't report the rest (they'll be rolled up to the daily soon enough to become a problem) - }), ]); const applicationUsageFromTotals = rawApplicationUsageTotals.reduce( @@ -156,10 +130,7 @@ export function registerApplicationUsageCollector( const nowMinus30 = moment().subtract(30, 'days'); const nowMinus90 = moment().subtract(90, 'days'); - const applicationUsage = [ - ...rawApplicationUsageDaily, - ...rawApplicationUsageTransactional, - ].reduce( + const applicationUsage = rawApplicationUsageDaily.reduce( ( acc, { @@ -224,11 +195,4 @@ export function registerApplicationUsageCollector( ); usageCollection.registerCollector(collector); - - timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() => - rollDailyData(logger, getSavedObjectsClient()) - ); - timer(ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL).subscribe(() => - rollTotals(logger, getSavedObjectsClient()) - ); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts new file mode 100644 index 0000000000000..bef835e922d8d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface ApplicationViewUsage { + appId: string; + viewId: string; + clicks_total: number; + clicks_7_days: number; + clicks_30_days: number; + clicks_90_days: number; + minutes_on_screen_total: number; + minutes_on_screen_7_days: number; + minutes_on_screen_30_days: number; + minutes_on_screen_90_days: number; +} + +export interface ApplicationUsageViews { + [serializedKey: string]: ApplicationViewUsage; +} + +export interface ApplicationUsageTelemetryReport { + [appId: string]: { + appId: string; + viewId: string; + clicks_total: number; + clicks_7_days: number; + clicks_30_days: number; + clicks_90_days: number; + minutes_on_screen_total: number; + minutes_on_screen_7_days: number; + minutes_on_screen_30_days: number; + minutes_on_screen_90_days: number; + views?: ApplicationViewUsage[]; + }; +} diff --git a/src/plugins/usage_collection/common/application_usage.ts b/src/plugins/usage_collection/common/application_usage.ts new file mode 100644 index 0000000000000..c9dd489000d35 --- /dev/null +++ b/src/plugins/usage_collection/common/application_usage.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MAIN_APP_DEFAULT_VIEW_ID } from './constants'; + +export const getDailyId = ({ + appId, + dayId, + viewId, +}: { + viewId: string; + appId: string; + dayId: string; +}) => { + return !viewId || viewId === MAIN_APP_DEFAULT_VIEW_ID + ? `${appId}:${dayId}` + : `${appId}:${dayId}:${viewId}`; +}; diff --git a/src/plugins/usage_collection/server/report/schema.ts b/src/plugins/usage_collection/server/report/schema.ts index 93203a33cd1e1..350ec8d90e765 100644 --- a/src/plugins/usage_collection/server/report/schema.ts +++ b/src/plugins/usage_collection/server/report/schema.ts @@ -9,6 +9,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { METRIC_TYPE } from '@kbn/analytics'; +const applicationUsageReportSchema = schema.object({ + minutesOnScreen: schema.number(), + numberOfClicks: schema.number(), + appId: schema.string(), + viewId: schema.string(), +}); + export const reportSchema = schema.object({ reportVersion: schema.maybe(schema.oneOf([schema.literal(3)])), userAgent: schema.maybe( @@ -38,17 +45,8 @@ export const reportSchema = schema.object({ }) ) ), - application_usage: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - minutesOnScreen: schema.number(), - numberOfClicks: schema.number(), - appId: schema.string(), - viewId: schema.string(), - }) - ) - ), + application_usage: schema.maybe(schema.recordOf(schema.string(), applicationUsageReportSchema)), }); export type ReportSchemaType = TypeOf; +export type ApplicationUsageReport = TypeOf; diff --git a/src/plugins/usage_collection/server/report/store_application_usage.test.ts b/src/plugins/usage_collection/server/report/store_application_usage.test.ts new file mode 100644 index 0000000000000..c4c9e5746e6cb --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_application_usage.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import moment from 'moment'; +import { savedObjectsRepositoryMock } from '../../../../core/server/mocks'; +import { getDailyId } from '../../common/application_usage'; +import { storeApplicationUsage } from './store_application_usage'; +import { ApplicationUsageReport } from './schema'; + +const createReport = (parts: Partial): ApplicationUsageReport => ({ + appId: 'appId', + viewId: 'viewId', + numberOfClicks: 0, + minutesOnScreen: 0, + ...parts, +}); + +describe('storeApplicationUsage', () => { + let repository: ReturnType; + let timestamp: Date; + + beforeEach(() => { + repository = savedObjectsRepositoryMock.create(); + timestamp = new Date(); + }); + + it('does not call `repository.incrementUsageCounters` when the report list is empty', async () => { + await storeApplicationUsage(repository, [], timestamp); + expect(repository.incrementCounter).not.toHaveBeenCalled(); + }); + + it('calls `repository.incrementUsageCounters` with the correct parameters', async () => { + const report = createReport({ + appId: 'app1', + viewId: 'view1', + numberOfClicks: 2, + minutesOnScreen: 5, + }); + + await storeApplicationUsage(repository, [report], timestamp); + + expect(repository.incrementCounter).toHaveBeenCalledTimes(1); + + expect(repository.incrementCounter).toHaveBeenCalledWith( + ...expectedIncrementParams(report, timestamp) + ); + }); + + it('aggregates reports with the same appId/viewId tuple', async () => { + const report1 = createReport({ + appId: 'app1', + viewId: 'view1', + numberOfClicks: 2, + minutesOnScreen: 5, + }); + const report2 = createReport({ + appId: 'app1', + viewId: 'view2', + numberOfClicks: 1, + minutesOnScreen: 7, + }); + const report3 = createReport({ + appId: 'app1', + viewId: 'view1', + numberOfClicks: 3, + minutesOnScreen: 9, + }); + + await storeApplicationUsage(repository, [report1, report2, report3], timestamp); + + expect(repository.incrementCounter).toHaveBeenCalledTimes(2); + + expect(repository.incrementCounter).toHaveBeenCalledWith( + ...expectedIncrementParams( + { + appId: 'app1', + viewId: 'view1', + numberOfClicks: report1.numberOfClicks + report3.numberOfClicks, + minutesOnScreen: report1.minutesOnScreen + report3.minutesOnScreen, + }, + timestamp + ) + ); + expect(repository.incrementCounter).toHaveBeenCalledWith( + ...expectedIncrementParams(report2, timestamp) + ); + }); +}); + +const expectedIncrementParams = ( + { appId, viewId, minutesOnScreen, numberOfClicks }: ApplicationUsageReport, + timestamp: Date +) => { + const dayId = moment(timestamp).format('YYYY-MM-DD'); + return [ + 'application_usage_daily', + getDailyId({ appId, viewId, dayId }), + [ + { fieldName: 'numberOfClicks', incrementBy: numberOfClicks }, + { fieldName: 'minutesOnScreen', incrementBy: minutesOnScreen }, + ], + { + upsertAttributes: { + appId, + viewId, + timestamp: moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(), + }, + }, + ]; +}; diff --git a/src/plugins/usage_collection/server/report/store_application_usage.ts b/src/plugins/usage_collection/server/report/store_application_usage.ts new file mode 100644 index 0000000000000..2058b054fda8c --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_application_usage.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import moment from 'moment'; +import { Writable } from '@kbn/utility-types'; +import { ISavedObjectsRepository } from 'src/core/server'; +import { ApplicationUsageReport } from './schema'; +import { getDailyId } from '../../common/application_usage'; + +type WritableApplicationUsageReport = Writable; + +export const storeApplicationUsage = async ( + repository: ISavedObjectsRepository, + appUsages: ApplicationUsageReport[], + timestamp: Date +) => { + if (!appUsages.length) { + return; + } + + const dayId = getDayId(timestamp); + const aggregatedReports = aggregateAppUsages(appUsages); + + return Promise.allSettled( + aggregatedReports.map(async (report) => incrementUsageCounters(repository, report, dayId)) + ); +}; + +const aggregateAppUsages = (appUsages: ApplicationUsageReport[]) => { + return [ + ...appUsages + .reduce((map, appUsage) => { + const key = getKey(appUsage); + const aggregated: WritableApplicationUsageReport = map.get(key) ?? { + appId: appUsage.appId, + viewId: appUsage.viewId, + minutesOnScreen: 0, + numberOfClicks: 0, + }; + + aggregated.minutesOnScreen += appUsage.minutesOnScreen; + aggregated.numberOfClicks += appUsage.numberOfClicks; + + map.set(key, aggregated); + return map; + }, new Map()) + .values(), + ]; +}; + +const incrementUsageCounters = ( + repository: ISavedObjectsRepository, + { appId, viewId, numberOfClicks, minutesOnScreen }: WritableApplicationUsageReport, + dayId: string +) => { + const dailyId = getDailyId({ appId, viewId, dayId }); + + return repository.incrementCounter( + 'application_usage_daily', + dailyId, + [ + { fieldName: 'numberOfClicks', incrementBy: numberOfClicks }, + { fieldName: 'minutesOnScreen', incrementBy: minutesOnScreen }, + ], + { + upsertAttributes: { + appId, + viewId, + timestamp: getTimestamp(dayId), + }, + } + ); +}; + +const getKey = ({ viewId, appId }: ApplicationUsageReport) => `${appId}___${viewId}`; + +const getDayId = (timestamp: Date) => moment(timestamp).format('YYYY-MM-DD'); + +const getTimestamp = (dayId: string) => { + // Concatenating the day in YYYY-MM-DD form to T00:00:00Z to reduce the TZ effects + return moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(); +}; diff --git a/src/plugins/usage_collection/server/report/store_report.test.mocks.ts b/src/plugins/usage_collection/server/report/store_report.test.mocks.ts new file mode 100644 index 0000000000000..d151e7d7a5ddd --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_report.test.mocks.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const storeApplicationUsageMock = jest.fn(); +jest.doMock('./store_application_usage', () => ({ + storeApplicationUsage: storeApplicationUsageMock, +})); diff --git a/src/plugins/usage_collection/server/report/store_report.test.ts b/src/plugins/usage_collection/server/report/store_report.test.ts index 7174a54067246..dfcdd1f8e7e42 100644 --- a/src/plugins/usage_collection/server/report/store_report.test.ts +++ b/src/plugins/usage_collection/server/report/store_report.test.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { storeApplicationUsageMock } from './store_report.test.mocks'; + import { savedObjectsRepositoryMock } from '../../../../core/server/mocks'; import { storeReport } from './store_report'; import { ReportSchemaType } from './schema'; @@ -16,8 +18,17 @@ describe('store_report', () => { const momentTimestamp = moment(); const date = momentTimestamp.format('DDMMYYYY'); + let repository: ReturnType; + + beforeEach(() => { + repository = savedObjectsRepositoryMock.create(); + }); + + afterEach(() => { + storeApplicationUsageMock.mockReset(); + }); + test('stores report for all types of data', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); const report: ReportSchemaType = { reportVersion: 3, userAgent: { @@ -53,9 +64,9 @@ describe('store_report', () => { }, }, }; - await storeReport(savedObjectClient, report); + await storeReport(repository, report); - expect(savedObjectClient.create).toHaveBeenCalledWith( + expect(repository.create).toHaveBeenCalledWith( 'ui-metric', { count: 1 }, { @@ -63,51 +74,45 @@ describe('store_report', () => { overwrite: true, } ); - expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith( + expect(repository.incrementCounter).toHaveBeenNthCalledWith( 1, 'ui-metric', 'test-app-name:test-event-name', [{ fieldName: 'count', incrementBy: 3 }] ); - expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith( + expect(repository.incrementCounter).toHaveBeenNthCalledWith( 2, 'ui-counter', `test-app-name:${date}:${METRIC_TYPE.LOADED}:test-event-name`, [{ fieldName: 'count', incrementBy: 1 }] ); - expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith( + expect(repository.incrementCounter).toHaveBeenNthCalledWith( 3, 'ui-counter', `test-app-name:${date}:${METRIC_TYPE.CLICK}:test-event-name`, [{ fieldName: 'count', incrementBy: 2 }] ); - expect(savedObjectClient.bulkCreate).toHaveBeenNthCalledWith(1, [ - { - type: 'application_usage_transactional', - attributes: { - numberOfClicks: 3, - minutesOnScreen: 10, - appId: 'appId', - viewId: 'appId_view', - timestamp: expect.any(Date), - }, - }, - ]); + + expect(storeApplicationUsageMock).toHaveBeenCalledTimes(1); + expect(storeApplicationUsageMock).toHaveBeenCalledWith( + repository, + Object.values(report.application_usage as Record), + expect.any(Date) + ); }); test('it should not fail if nothing to store', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); const report: ReportSchemaType = { reportVersion: 3, userAgent: void 0, uiCounter: void 0, application_usage: void 0, }; - await storeReport(savedObjectClient, report); + await storeReport(repository, report); - expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); - expect(savedObjectClient.incrementCounter).not.toHaveBeenCalled(); - expect(savedObjectClient.create).not.toHaveBeenCalled(); - expect(savedObjectClient.create).not.toHaveBeenCalled(); + expect(repository.bulkCreate).not.toHaveBeenCalled(); + expect(repository.incrementCounter).not.toHaveBeenCalled(); + expect(repository.create).not.toHaveBeenCalled(); + expect(repository.create).not.toHaveBeenCalled(); }); }); diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts index c3e04990d5793..0545a54792d45 100644 --- a/src/plugins/usage_collection/server/report/store_report.ts +++ b/src/plugins/usage_collection/server/report/store_report.ts @@ -10,6 +10,7 @@ import { ISavedObjectsRepository } from 'src/core/server'; import moment from 'moment'; import { chain, sumBy } from 'lodash'; import { ReportSchemaType } from './schema'; +import { storeApplicationUsage } from './store_application_usage'; export async function storeReport( internalRepository: ISavedObjectsRepository, @@ -17,11 +18,11 @@ export async function storeReport( ) { const uiCounters = report.uiCounter ? Object.entries(report.uiCounter) : []; const userAgents = report.userAgent ? Object.entries(report.userAgent) : []; - const appUsage = report.application_usage ? Object.values(report.application_usage) : []; + const appUsages = report.application_usage ? Object.values(report.application_usage) : []; const momentTimestamp = moment(); - const timestamp = momentTimestamp.toDate(); const date = momentTimestamp.format('DDMMYYYY'); + const timestamp = momentTimestamp.toDate(); return Promise.allSettled([ // User Agent @@ -64,21 +65,6 @@ export async function storeReport( ]; }), // Application Usage - ...[ - (async () => { - if (!appUsage.length) return []; - const { saved_objects: savedObjects } = await internalRepository.bulkCreate( - appUsage.map((metric) => ({ - type: 'application_usage_transactional', - attributes: { - ...metric, - timestamp, - }, - })) - ); - - return savedObjects; - })(), - ], + storeApplicationUsage(internalRepository, appUsages, timestamp), ]); } diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index a7b4da566b143..d0a09ee58d335 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -156,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) { describe('application usage limits', () => { function createSavedObject(viewId?: string) { return supertest - .post('/api/saved_objects/application_usage_transactional') + .post('/api/saved_objects/application_usage_daily') .send({ attributes: { appId: 'test-app', @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { await Promise.all( savedObjectIds.map((savedObjectId) => { return supertest - .delete(`/api/saved_objects/application_usage_transactional/${savedObjectId}`) + .delete(`/api/saved_objects/application_usage_daily/${savedObjectId}`) .expect(200); }) ); @@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/saved_objects/_bulk_create') .send( new Array(10001).fill(0).map(() => ({ - type: 'application_usage_transactional', + type: 'application_usage_daily', attributes: { appId: 'test-app', minutesOnScreen: 1, @@ -248,13 +248,12 @@ export default function ({ getService }: FtrProviderContext) { // The SavedObjects API does not allow bulk deleting, and deleting one by one takes ages and the tests timeout await es.deleteByQuery({ index: '.kibana', - body: { query: { term: { type: 'application_usage_transactional' } } }, + body: { query: { term: { type: 'application_usage_daily' } } }, conflicts: 'proceed', }); }); - // flaky https://github.com/elastic/kibana/issues/94513 - it.skip("should only use the first 10k docs for the application_usage data (they'll be rolled up in a later process)", async () => { + it("should only use the first 10k docs for the application_usage data (they'll be rolled up in a later process)", async () => { const stats = await retrieveTelemetry(supertest); expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ 'test-app': { From 02a8f11ec88cf6940da0f886d6ca9a5816314de5 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 26 Mar 2021 11:50:21 +0300 Subject: [PATCH 21/98] [Timelion] Allow import/export of timelion-sheet saved object (#95048) * [Timelion] Allow import/export of timelion-sheet saved object Closes: #9107 * visualize.show -> timelion.show Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/timelion/server/plugin.ts | 1 + .../server/saved_objects/timelion_sheet.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 66348c572117d..226a978fe5d88 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -47,6 +47,7 @@ export class TimelionPlugin implements Plugin { core.capabilities.registerProvider(() => ({ timelion: { save: true, + show: true, }, })); core.savedObjects.registerType(timelionSheetSavedObjectType); diff --git a/src/plugins/timelion/server/saved_objects/timelion_sheet.ts b/src/plugins/timelion/server/saved_objects/timelion_sheet.ts index 52d7f59a7c734..231e049280bb1 100644 --- a/src/plugins/timelion/server/saved_objects/timelion_sheet.ts +++ b/src/plugins/timelion/server/saved_objects/timelion_sheet.ts @@ -12,6 +12,20 @@ export const timelionSheetSavedObjectType: SavedObjectsType = { name: 'timelion-sheet', hidden: false, namespaceType: 'single', + management: { + icon: 'visTimelion', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/timelion#/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'timelion.show', + }; + }, + }, mappings: { properties: { description: { type: 'text' }, From 2af094a63d93da906c5a60ee40c4a8372099f574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 26 Mar 2021 11:32:46 +0100 Subject: [PATCH 22/98] [Security Solution] Put Artifacts by Policy feature behind a feature flag (#95284) * Added sync_master file for tracking/triggering PRs for merging master into feature branch * removed unnecessary (temporary) markdown file * Trusted apps by policy api (#88025) * Initial version of API for trusted apps per policy. * Fixed compilation errors because of missing new property. * Mapping from tags to policies and back. (No testing) * Fixed compilation error after pulling in main. * Fixed failing tests. * Separated out the prefix in tag for policy reference into constant. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [SECURITY_SOLUTION][ENDPOINT] Ability to create a Trusted App as either Global or Policy Specific (#88707) * Create form supports selecting policies or making Trusted app global * New component `EffectedPolicySelect` - for selecting policies * Enhanced `waitForAction()` test utility to provide a `validate()` option * [SECURITY SOLUTION][ENDPOINT] UI for editing Trusted Application items (#89479) * Add Edit button to TA card UI * Support additional url params (`show`, `id`) * Refactor TrustedAppForm to support Editing of an existing entry * [SECURITY SOLUTION][ENDPOINT] API (`PUT`) for Trusted Apps Edit flow (#90333) * New API route for Update (`PUT`) * Connect UI to Update (PUT) API * Add `version` to TrustedApp type and return it on the API responses * Refactor - moved some public/server shared modules to top-level `common/*` * [SECURITY SOLUTION][ENDPOINT] Trusted Apps API to retrieve a single Trusted App item (#90842) * Get One Trusted App API - route, service, handler * Adjust UI to call GET api to retrieve trusted app for edit * Deleted ununsed trusted app types file * Add UI handling of non-existing TA for edit or when id is missing in url * [Security Solution][Endpoint] Multiple misc. updates/fixes for Edit Trusted Apps (#91656) * correct trusted app schema to ensure `version` is not exposed on TS type for POST * Added updated_by, updated_on properties to TrustedApp * Refactored TA List view to fix bug where card was not updated on a successful edit * Test cases for card interaction from the TA List view * Change title of policy selection to `Assignment` * Selectable Policy CSS adjustments based on UX feedback * Fix failing server tests * [Security Solution][Endpoint] Trusted Apps list API KQL filtering support (#92611) * Fix bad merge from master * Fix trusted apps generator * Add `kuery` to the GET (list) Trusted Apps api * Refactor schema with Put method after merging changes with master * WIP: allow effectScope only when feature flag is enabled * Fixes errors with non declared logger * Uses experimental features module to allow or not effectScope on create/update trusted app schema * Set default value for effectScope when feature flag is disabled * Adds experimentals into redux store. Also creates hook to retrieve a feature flag value from state * Hides effectPolicy when feature flag is not enabled * Fixes unit test mocking hook and adds new test case * Changes file extension for custom hook * Adds new unit test for custom hook * Hides horizontal bar with feature flag * Compress text area depending on feature flag * Fixes failing test because feature flag * Fixes wrong import and unit test * Thwrows error if invalid feature flag check * Adds snapshoot checks with feature flag enabled/disabled * Test snapshots * Changes type name * Add experimentalFeatures in app context * Fixes type checks due AppContext changes * Fixes test due changes on custom hook Co-authored-by: Paul Tavares Co-authored-by: Bohdan Tsymbala Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> --- x-pack/plugins/lists/server/index.ts | 5 +- .../common/endpoint/constants.ts | 2 + .../endpoint/schema/trusted_apps.test.ts | 72 +- .../common/endpoint/schema/trusted_apps.ts | 43 +- .../trusted_apps/to_update_trusted_app.ts | 30 + .../trusted_apps/validations.ts} | 2 +- .../common/endpoint/types/index.ts | 5 + .../common/endpoint/types/trusted_apps.ts | 38 + .../common/experimental_features.ts | 1 + .../components/item_details_card/index.tsx | 85 +- .../hooks/use_experimental_features.test.ts | 47 + .../common/hooks/use_experimental_features.ts | 28 + .../public/common/store/app/model.ts | 2 + .../public/common/store/reducer.test.ts | 3 + .../public/common/store/reducer.ts | 5 +- .../public/common/store/test_utils.ts | 15 +- .../security_solution/public/common/types.ts | 4 + .../public/management/common/routing.ts | 20 +- .../pages/trusted_apps/service/index.ts | 36 + .../state/trusted_apps_list_page_state.ts | 9 +- .../pages/trusted_apps/state/type_guards.ts | 16 + .../pages/trusted_apps/store/action.ts | 11 + .../pages/trusted_apps/store/builders.ts | 3 + .../trusted_apps/store/middleware.test.ts | 36 +- .../pages/trusted_apps/store/middleware.ts | 173 +- .../pages/trusted_apps/store/reducer.test.ts | 8 +- .../pages/trusted_apps/store/reducer.ts | 32 +- .../pages/trusted_apps/store/selectors.ts | 56 +- .../pages/trusted_apps/test_utils/index.ts | 4 + .../trusted_apps_page.test.tsx.snap | 5573 +++++++++++++++++ .../components/create_trusted_app_flyout.tsx | 99 +- .../create_trusted_app_form.test.tsx | 318 +- .../components/create_trusted_app_form.tsx | 332 +- .../effected_policy_select.test.tsx | 167 + .../effected_policy_select.tsx | 197 + .../effected_policy_select/index.ts} | 6 +- .../effected_policy_select/test_utils.ts | 44 + .../__snapshots__/index.test.tsx.snap | 18 + .../trusted_app_card/index.stories.tsx | 24 +- .../trusted_app_card/index.test.tsx | 12 +- .../components/trusted_app_card/index.tsx | 142 +- .../__snapshots__/index.test.tsx.snap | 638 +- .../components/trusted_apps_grid/index.tsx | 27 +- .../__snapshots__/index.test.tsx.snap | 102 + .../components/trusted_apps_list/index.tsx | 324 +- .../pages/trusted_apps/view/translations.ts | 18 +- .../view/trusted_apps_notifications.tsx | 29 +- .../view/trusted_apps_page.test.tsx | 403 +- .../trusted_apps/view/trusted_apps_page.tsx | 12 +- .../security_solution/public/plugin.tsx | 8 +- .../scripts/endpoint/trusted_apps/index.ts | 7 +- .../server/endpoint/mocks.ts | 2 + .../artifacts/download_artifact.test.ts | 2 + .../endpoint/routes/metadata/metadata.test.ts | 3 + .../routes/metadata/metadata_v1.test.ts | 2 + .../routes/metadata/query_builders.test.ts | 9 + .../routes/metadata/query_builders_v1.test.ts | 9 + .../endpoint/routes/policy/handlers.test.ts | 3 + .../endpoint/routes/trusted_apps/errors.ts | 20 + .../routes/trusted_apps/handlers.test.ts | 307 +- .../endpoint/routes/trusted_apps/handlers.ts | 145 +- .../endpoint/routes/trusted_apps/index.ts | 41 +- .../routes/trusted_apps/mapping.test.ts | 83 +- .../endpoint/routes/trusted_apps/mapping.ts | 80 +- .../routes/trusted_apps/service.test.ts | 140 +- .../endpoint/routes/trusted_apps/service.ts | 84 +- .../routes/trusted_apps/test_utils.ts | 33 + .../server/endpoint/types.ts | 2 + .../plugins/security_solution/server/index.ts | 3 + .../lib/hosts/elasticsearch_adapter.test.ts | 2 + .../security_solution/server/plugin.ts | 3 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 73 files changed, 9574 insertions(+), 694 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts rename x-pack/plugins/security_solution/common/endpoint/{validation/trusted_apps.ts => service/trusted_apps/validations.ts} (93%) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx rename x-pack/plugins/security_solution/public/management/pages/trusted_apps/{types.ts => view/components/effected_policy_select/index.ts} (70%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/test_utils.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/test_utils.ts diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 1ebdf9f04bf9d..250b5e79ed109 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -12,7 +12,10 @@ import { ListPlugin } from './plugin'; // exporting these since its required at top level in siem plugin export { ListClient } from './services/lists/list_client'; -export { CreateExceptionListItemOptions } from './services/exception_lists/exception_list_client_types'; +export { + CreateExceptionListItemOptions, + UpdateExceptionListItemOptions, +} from './services/exception_lists/exception_list_client_types'; export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export type { ListPluginSetup, ListsApiRequestHandlerContext } from './types'; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 90e025de1dcc8..d9f67e31196ca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -15,8 +15,10 @@ export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency'; export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; +export const TRUSTED_APPS_GET_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; +export const TRUSTED_APPS_UPDATE_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_SUMMARY_API = '/api/endpoint/trusted_apps/summary'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index e9ae439d0ac8c..326795ae55662 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -5,8 +5,18 @@ * 2.0. */ -import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps'; -import { ConditionEntryField, OperatingSystem } from '../types'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, + PutTrustedAppUpdateRequestSchema, +} from './trusted_apps'; +import { + ConditionEntry, + ConditionEntryField, + NewTrustedApp, + OperatingSystem, + PutTrustedAppsRequestParams, +} from '../types'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -72,17 +82,18 @@ describe('When invoking Trusted Apps Schema', () => { }); describe('for POST Create', () => { - const createConditionEntry = (data?: T) => ({ + const createConditionEntry = (data?: T): ConditionEntry => ({ field: ConditionEntryField.PATH, type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T) => ({ + const createNewTrustedApp = (data?: T): NewTrustedApp => ({ name: 'Some Anti-Virus App', description: 'this one is ok', - os: 'windows', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, entries: [createConditionEntry()], ...(data || {}), }); @@ -329,4 +340,55 @@ describe('When invoking Trusted Apps Schema', () => { }); }); }); + + describe('for PUT Update', () => { + const createConditionEntry = (data?: T): ConditionEntry => ({ + field: ConditionEntryField.PATH, + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + ...(data || {}), + }); + const createNewTrustedApp = (data?: T): NewTrustedApp => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + }); + + const updateParams = (data?: T): PutTrustedAppsRequestParams => ({ + id: 'validId', + ...(data || {}), + }); + + const body = PutTrustedAppUpdateRequestSchema.body; + const params = PutTrustedAppUpdateRequestSchema.params; + + it('should not error on a valid message', () => { + const bodyMsg = createNewTrustedApp(); + const paramsMsg = updateParams(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + expect(params.validate(paramsMsg)).toStrictEqual(paramsMsg); + }); + + it('should validate `id` params is required', () => { + expect(() => params.validate(updateParams({ id: undefined }))).toThrow(); + }); + + it('should validate `id` params to be string', () => { + expect(() => params.validate(updateParams({ id: 1 }))).toThrow(); + }); + + it('should validate `version`', () => { + const bodyMsg = createNewTrustedApp({ version: 'v1' }); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `version` must be string', () => { + const bodyMsg = createNewTrustedApp({ version: 1 }); + expect(() => body.validate(bodyMsg)).toThrow(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 6d40dc75fd1c1..e582744e1a141 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -6,8 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { ConditionEntryField, OperatingSystem } from '../types'; -import { getDuplicateFields, isValidHash } from '../validation/trusted_apps'; +import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types'; +import { getDuplicateFields, isValidHash } from '../service/trusted_apps/validations'; export const DeleteTrustedAppsRequestSchema = { params: schema.object({ @@ -15,10 +15,17 @@ export const DeleteTrustedAppsRequestSchema = { }), }; +export const GetOneTrustedAppRequestSchema = { + params: schema.object({ + id: schema.string(), + }), +}; + export const GetTrustedAppsRequestSchema = { query: schema.object({ page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), + kuery: schema.maybe(schema.string()), }), }; @@ -40,18 +47,18 @@ const CommonEntrySchema = { schema.siblingRef('field'), ConditionEntryField.HASH, schema.string({ - validate: (hash) => + validate: (hash: string) => isValidHash(hash) ? undefined : `invalidField.${ConditionEntryField.HASH}`, }), schema.conditional( schema.siblingRef('field'), ConditionEntryField.PATH, schema.string({ - validate: (field) => + validate: (field: string) => field.length > 0 ? undefined : `invalidField.${ConditionEntryField.PATH}`, }), schema.string({ - validate: (field) => + validate: (field: string) => field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER}`, }) ) @@ -99,7 +106,7 @@ const EntrySchemaDependingOnOS = schema.conditional( */ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, { minSize: 1, - validate(entries) { + validate(entries: ConditionEntry[]) { return ( getDuplicateFields(entries) .map((field) => `duplicatedEntry.${field}`) @@ -108,8 +115,8 @@ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, { }, }); -export const PostTrustedAppCreateRequestSchema = { - body: schema.object({ +const getTrustedAppForOsScheme = (forUpdateFlow: boolean = false) => + schema.object({ name: schema.string({ minLength: 1, maxLength: 256 }), description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })), os: schema.oneOf([ @@ -117,6 +124,26 @@ export const PostTrustedAppCreateRequestSchema = { schema.literal(OperatingSystem.LINUX), schema.literal(OperatingSystem.MAC), ]), + effectScope: schema.oneOf([ + schema.object({ + type: schema.literal('global'), + }), + schema.object({ + type: schema.literal('policy'), + policies: schema.arrayOf(schema.string({ minLength: 1 })), + }), + ]), entries: EntriesSchema, + ...(forUpdateFlow ? { version: schema.maybe(schema.string()) } : {}), + }); + +export const PostTrustedAppCreateRequestSchema = { + body: getTrustedAppForOsScheme(), +}; + +export const PutTrustedAppUpdateRequestSchema = { + params: schema.object({ + id: schema.string(), }), + body: getTrustedAppForOsScheme(true), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts new file mode 100644 index 0000000000000..fcde1d44b682d --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/to_update_trusted_app.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MaybeImmutable, NewTrustedApp, UpdateTrustedApp } from '../../types'; + +const NEW_TRUSTED_APP_KEYS: Array = [ + 'name', + 'effectScope', + 'entries', + 'description', + 'os', + 'version', +]; + +export const toUpdateTrustedApp = ( + trustedApp: MaybeImmutable +): UpdateTrustedApp => { + const trustedAppForUpdate: UpdateTrustedApp = {} as UpdateTrustedApp; + + for (const key of NEW_TRUSTED_APP_KEYS) { + // This should be safe. Its needed due to the inter-dependency on property values (`os` <=> `entries`) + // @ts-expect-error + trustedAppForUpdate[key] = trustedApp[key]; + } + return trustedAppForUpdate; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts similarity index 93% rename from x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts rename to x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts index faad639eeacb3..b0828be6af6c5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/validation/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConditionEntry, ConditionEntryField } from '../types'; +import { ConditionEntry, ConditionEntryField } from '../../types'; const HASH_LENGTHS: readonly number[] = [ 32, // MD5 diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 87268f02a16e1..0b41dc5608fe9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -62,6 +62,11 @@ type ImmutableMap = ReadonlyMap, Immutable>; type ImmutableSet = ReadonlySet>; type ImmutableObject = { readonly [K in keyof T]: Immutable }; +/** + * Utility type that will return back a union of the given [T]ype and an Immutable version of it + */ +export type MaybeImmutable = T | Immutable; + /** * Stats for related events for a particular node in a resolver graph. */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index a5c3c1eab52b3..d36958c11d2a1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -9,14 +9,22 @@ import { TypeOf } from '@kbn/config-schema'; import { ApplicationStart } from 'kibana/public'; import { DeleteTrustedAppsRequestSchema, + GetOneTrustedAppRequestSchema, GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, + PutTrustedAppUpdateRequestSchema, } from '../schema/trusted_apps'; import { OperatingSystem } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; +export type GetOneTrustedAppRequestParams = TypeOf; + +export interface GetOneTrustedAppResponse { + data: TrustedApp; +} + /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; @@ -39,6 +47,15 @@ export interface PostTrustedAppCreateResponse { data: TrustedApp; } +/** API request params for updating a Trusted App */ +export type PutTrustedAppsRequestParams = TypeOf; + +/** API Request body for Updating a new Trusted App entry */ +export type PutTrustedAppUpdateRequest = TypeOf & + (MacosLinuxConditionEntries | WindowsConditionEntries); + +export type PutTrustedAppUpdateResponse = PostTrustedAppCreateResponse; + export interface GetTrustedAppsSummaryResponse { total: number; windows: number; @@ -76,17 +93,38 @@ export interface WindowsConditionEntries { entries: WindowsConditionEntry[]; } +export interface GlobalEffectScope { + type: 'global'; +} + +export interface PolicyEffectScope { + type: 'policy'; + /** An array of Endpoint Integration Policy UUIDs */ + policies: string[]; +} + +export type EffectScope = GlobalEffectScope | PolicyEffectScope; + /** Type for a new Trusted App Entry */ export type NewTrustedApp = { name: string; description?: string; + effectScope: EffectScope; } & (MacosLinuxConditionEntries | WindowsConditionEntries); +/** An Update to a Trusted App Entry */ +export type UpdateTrustedApp = NewTrustedApp & { + version?: string; +}; + /** A trusted app entry */ export type TrustedApp = NewTrustedApp & { + version: string; id: string; created_at: string; created_by: string; + updated_at: string; + updated_by: string; }; /** diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index c764c31a2d781..19de81cb95c3f 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -13,6 +13,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; */ const allowedExperimentalValues = Object.freeze({ fleetServerEnabled: false, + trustedAppsByPolicyEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 6fcf688fff7a7..c9fb502956053 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import React, { FC, isValidElement, memo, ReactElement, ReactNode, useMemo } from 'react'; +import React, { + FC, + isValidElement, + memo, + PropsWithChildren, + ReactElement, + ReactNode, + useMemo, +} from 'react'; import styled from 'styled-components'; import { EuiPanel, @@ -92,41 +100,46 @@ export const ItemDetailsAction: FC> = memo( ItemDetailsAction.displayName = 'ItemDetailsAction'; -export const ItemDetailsCard: FC = memo(({ children }) => { - const childElements = useMemo( - () => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]), - [children] - ); - - return ( - - - - - {childElements.get(ItemDetailsPropertySummary)} - - - - - -
{childElements.get(OTHER_NODES)}
-
- {childElements.has(ItemDetailsAction) && ( - - - {childElements.get(ItemDetailsAction)?.map((action, index) => ( - - {action} - - ))} - +export type ItemDetailsCardProps = PropsWithChildren<{ + 'data-test-subj'?: string; +}>; +export const ItemDetailsCard = memo( + ({ children, 'data-test-subj': dataTestSubj }) => { + const childElements = useMemo( + () => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]), + [children] + ); + + return ( + + + + + {childElements.get(ItemDetailsPropertySummary)} + + + + + +
{childElements.get(OTHER_NODES)}
- )} -
-
-
-
- ); -}); + {childElements.has(ItemDetailsAction) && ( + + + {childElements.get(ItemDetailsAction)?.map((action, index) => ( + + {action} + + ))} + + + )} +
+
+
+
+ ); + } +); ItemDetailsCard.displayName = 'ItemDetailsCard'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts new file mode 100644 index 0000000000000..2ac5948641d7d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; +import { useIsExperimentalFeatureEnabled } from './use_experimental_features'; + +jest.mock('react-redux'); +const useSelectorMock = useSelector as jest.Mock; +const mockAppState = { + app: { + enableExperimental: { + featureA: true, + featureB: false, + }, + }, +}; + +describe('useExperimentalFeatures', () => { + beforeEach(() => { + useSelectorMock.mockImplementation((cb) => { + return cb(mockAppState); + }); + }); + afterEach(() => { + useSelectorMock.mockClear(); + }); + it('throws an error when unexisting feature', async () => { + expect(() => + useIsExperimentalFeatureEnabled('unexistingFeature' as keyof ExperimentalFeatures) + ).toThrowError(); + }); + it('returns true when existing feature and is enabled', async () => { + const result = useIsExperimentalFeatureEnabled('featureA' as keyof ExperimentalFeatures); + + expect(result).toBeTruthy(); + }); + it('returns false when existing feature and is disabled', async () => { + const result = useIsExperimentalFeatureEnabled('featureB' as keyof ExperimentalFeatures); + + expect(result).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts new file mode 100644 index 0000000000000..247b7624914cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { State } from '../../common/store'; +import { + ExperimentalFeatures, + getExperimentalAllowedValues, +} from '../../../common/experimental_features'; + +const allowedExperimentalValues = getExperimentalAllowedValues(); + +export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { + return useSelector(({ app: { enableExperimental } }: State) => { + if (!enableExperimental || !(feature in enableExperimental)) { + throw new Error( + `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join( + ', ' + )}` + ); + } + return enableExperimental[feature]; + }); +}; diff --git a/x-pack/plugins/security_solution/public/common/store/app/model.ts b/x-pack/plugins/security_solution/public/common/store/app/model.ts index 38ecedc0c7ba7..5a252e4aa48f2 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/model.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { Note } from '../../lib/note'; export type ErrorState = ErrorModel; @@ -24,4 +25,5 @@ export type ErrorModel = Error[]; export interface AppModel { notesById: NotesById; errors: ErrorState; + enableExperimental?: ExperimentalFeatures; } diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts index 9a2289765e85d..d2808a02c8621 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { parseExperimentalConfigValue } from '../../..//common/experimental_features'; import { createInitialState } from './reducer'; jest.mock('../lib/kibana', () => ({ @@ -22,6 +23,7 @@ describe('createInitialState', () => { kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], configIndexPatterns: ['auditbeat-*', 'filebeat'], signalIndexName: 'siem-signals-default', + enableExperimental: parseExperimentalConfigValue([]), } ); @@ -35,6 +37,7 @@ describe('createInitialState', () => { kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], configIndexPatterns: [], signalIndexName: 'siem-signals-default', + enableExperimental: parseExperimentalConfigValue([]), } ); diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index 27fddafc3781f..c2ef2563fe63e 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -21,6 +21,7 @@ import { ManagementPluginReducer } from '../../management'; import { State } from './types'; import { AppAction } from './actions'; import { KibanaIndexPatterns } from './sourcerer/model'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & @@ -36,14 +37,16 @@ export const createInitialState = ( kibanaIndexPatterns, configIndexPatterns, signalIndexName, + enableExperimental, }: { kibanaIndexPatterns: KibanaIndexPatterns; configIndexPatterns: string[]; signalIndexName: string | null; + enableExperimental: ExperimentalFeatures; } ): PreloadedState => { const preloadedState: PreloadedState = { - app: initialAppState, + app: { ...initialAppState, enableExperimental }, dragAndDrop: initialDragAndDropState, ...pluginsInitState, inputs: createInitialInputsState(), diff --git a/x-pack/plugins/security_solution/public/common/store/test_utils.ts b/x-pack/plugins/security_solution/public/common/store/test_utils.ts index c1d54192c86b1..7616dfccddaff 100644 --- a/x-pack/plugins/security_solution/public/common/store/test_utils.ts +++ b/x-pack/plugins/security_solution/public/common/store/test_utils.ts @@ -9,6 +9,10 @@ import { Dispatch } from 'redux'; import { State, ImmutableMiddlewareFactory } from './types'; import { AppAction } from './actions'; +interface WaitForActionOptions { + validate?: (action: A extends { type: T } ? A : never) => boolean; +} + /** * Utilities for testing Redux middleware */ @@ -21,7 +25,10 @@ export interface MiddlewareActionSpyHelper(actionType: T) => Promise; + waitForAction: ( + actionType: T, + options?: WaitForActionOptions + ) => Promise; /** * A property holding the information around the calls that were processed by the internal * `actionSpyMiddelware`. This property holds the information typically found in Jets's mocked @@ -78,7 +85,7 @@ export const createSpyMiddleware = < let spyDispatch: jest.Mock>; return { - waitForAction: async (actionType) => { + waitForAction: async (actionType, options = {}) => { type ResolvedAction = A extends { type: typeof actionType } ? A : never; // Error is defined here so that we get a better stack trace that points to the test from where it was used @@ -87,6 +94,10 @@ export const createSpyMiddleware = < return new Promise((resolve, reject) => { const watch: ActionWatcher = (action) => { if (action.type === actionType) { + if (options.validate && !options.validate(action as ResolvedAction)) { + return; + } + watchers.delete(watch); clearTimeout(timeout); resolve(action as ResolvedAction); diff --git a/x-pack/plugins/security_solution/public/common/types.ts b/x-pack/plugins/security_solution/public/common/types.ts index 68346847eb8d1..f1a7cdc8abc60 100644 --- a/x-pack/plugins/security_solution/public/common/types.ts +++ b/x-pack/plugins/security_solution/public/common/types.ts @@ -10,3 +10,7 @@ export interface ServerApiError { error: string; message: string; } + +export interface SecuritySolutionUiConfigType { + enableExperimental: string[]; +} diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index cbcc054e7c6a9..bf754720f314b 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -108,6 +108,7 @@ const normalizeTrustedAppsPageLocation = ( : {}), ...(!isDefaultOrMissing(location.view_type, 'grid') ? { view_type: location.view_type } : {}), ...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}), + ...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}), }; } else { return {}; @@ -147,11 +148,20 @@ export const extractListPaginationParams = (query: querystring.ParsedUrlQuery) = export const extractTrustedAppsListPageLocation = ( query: querystring.ParsedUrlQuery -): TrustedAppsListPageLocation => ({ - ...extractListPaginationParams(query), - view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid', - show: extractFirstParamValue(query, 'show') === 'create' ? 'create' : undefined, -}); +): TrustedAppsListPageLocation => { + const showParamValue = extractFirstParamValue( + query, + 'show' + ) as TrustedAppsListPageLocation['show']; + + return { + ...extractListPaginationParams(query), + view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid', + show: + showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined, + id: extractFirstParamValue(query, 'id'), + }; +}; export const getTrustedAppsListPath = (location?: Partial): string => { const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 578043f4321e9..5f572251daeda 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -10,7 +10,9 @@ import { HttpStart } from 'kibana/public'; import { TRUSTED_APPS_CREATE_API, TRUSTED_APPS_DELETE_API, + TRUSTED_APPS_GET_API, TRUSTED_APPS_LIST_API, + TRUSTED_APPS_UPDATE_API, TRUSTED_APPS_SUMMARY_API, } from '../../../../../common/endpoint/constants'; @@ -21,19 +23,39 @@ import { PostTrustedAppCreateRequest, PostTrustedAppCreateResponse, GetTrustedAppsSummaryResponse, + PutTrustedAppUpdateRequest, + PutTrustedAppUpdateResponse, + PutTrustedAppsRequestParams, + GetOneTrustedAppRequestParams, + GetOneTrustedAppResponse, } from '../../../../../common/endpoint/types/trusted_apps'; import { resolvePathVariables } from './utils'; +import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/services/ingest'; export interface TrustedAppsService { + getTrustedApp(params: GetOneTrustedAppRequestParams): Promise; getTrustedAppsList(request: GetTrustedAppsListRequest): Promise; deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise; createTrustedApp(request: PostTrustedAppCreateRequest): Promise; + updateTrustedApp( + params: PutTrustedAppsRequestParams, + request: PutTrustedAppUpdateRequest + ): Promise; + getPolicyList( + options?: Parameters[1] + ): ReturnType; } export class TrustedAppsHttpService implements TrustedAppsService { constructor(private http: HttpStart) {} + async getTrustedApp(params: GetOneTrustedAppRequestParams) { + return this.http.get( + resolvePathVariables(TRUSTED_APPS_GET_API, params) + ); + } + async getTrustedAppsList(request: GetTrustedAppsListRequest) { return this.http.get(TRUSTED_APPS_LIST_API, { query: request, @@ -50,7 +72,21 @@ export class TrustedAppsHttpService implements TrustedAppsService { }); } + async updateTrustedApp( + params: PutTrustedAppsRequestParams, + updatedTrustedApp: PutTrustedAppUpdateRequest + ) { + return this.http.put( + resolvePathVariables(TRUSTED_APPS_UPDATE_API, params), + { body: JSON.stringify(updatedTrustedApp) } + ); + } + async getTrustedAppsSummary() { return this.http.get(TRUSTED_APPS_SUMMARY_API); } + + getPolicyList(options?: Parameters[1]) { + return sendGetEndpointSpecificPackagePolicies(this.http, options); + } } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index ea934881f6220..1c1fca4b55abc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -7,6 +7,7 @@ import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { AsyncResourceState } from '.'; +import { GetPolicyListResponse } from '../../policy/types'; export interface Pagination { pageIndex: number; @@ -29,7 +30,9 @@ export interface TrustedAppsListPageLocation { page_index: number; page_size: number; view_type: ViewType; - show?: 'create'; + show?: 'create' | 'edit'; + /** Used for editing. The ID of the selected trusted app */ + id?: string; } export interface TrustedAppsListPageState { @@ -51,9 +54,13 @@ export interface TrustedAppsListPageState { entry: NewTrustedApp; isValid: boolean; }; + /** The trusted app to be edited (when in edit mode) */ + editItem?: AsyncResourceState; confirmed: boolean; submissionResourceState: AsyncResourceState; }; + /** A list of all available polices for use in associating TA to policies */ + policies: AsyncResourceState; location: TrustedAppsListPageLocation; active: boolean; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 66f4eff81dbdd..3f9e9d53f69e4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -8,7 +8,11 @@ import { ConditionEntry, ConditionEntryField, + EffectScope, + GlobalEffectScope, MacosLinuxConditionEntry, + MaybeImmutable, + PolicyEffectScope, WindowsConditionEntry, } from '../../../../../common/endpoint/types'; @@ -23,3 +27,15 @@ export const isMacosLinuxTrustedAppCondition = ( ): condition is MacosLinuxConditionEntry => { return condition.field !== ConditionEntryField.SIGNER; }; + +export const isGlobalEffectScope = ( + effectedScope: MaybeImmutable +): effectedScope is GlobalEffectScope => { + return effectedScope.type === 'global'; +}; + +export const isPolicyEffectScope = ( + effectedScope: MaybeImmutable +): effectedScope is PolicyEffectScope => { + return effectedScope.type === 'policy'; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index aaa05f550b208..34f48142c7032 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -9,6 +9,7 @@ import { Action } from 'redux'; import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; import { AsyncResourceState, TrustedAppsListData } from '../state'; +import { GetPolicyListResponse } from '../../policy/types'; export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>; @@ -51,6 +52,10 @@ export type TrustedAppCreationDialogFormStateUpdated = Action<'trustedAppCreatio }; }; +export type TrustedAppCreationEditItemStateChanged = Action<'trustedAppCreationEditItemStateChanged'> & { + payload: AsyncResourceState; +}; + export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialogConfirmed'>; export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>; @@ -59,6 +64,10 @@ export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> & payload: AsyncResourceState; }; +export type TrustedAppsPoliciesStateChanged = Action<'trustedAppsPoliciesStateChanged'> & { + payload: AsyncResourceState; +}; + export type TrustedAppsPageAction = | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged @@ -67,8 +76,10 @@ export type TrustedAppsPageAction = | TrustedAppDeletionDialogConfirmed | TrustedAppDeletionDialogClosed | TrustedAppCreationSubmissionResourceStateChanged + | TrustedAppCreationEditItemStateChanged | TrustedAppCreationDialogStarted | TrustedAppCreationDialogFormStateUpdated | TrustedAppCreationDialogConfirmed | TrustedAppsExistResponse + | TrustedAppsPoliciesStateChanged | TrustedAppCreationDialogClosed; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index 3acb55904d298..ece2c9e29750f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -28,6 +28,7 @@ export const defaultNewTrustedApp = (): NewTrustedApp => ({ os: OperatingSystem.WINDOWS, entries: [defaultConditionEntry()], description: '', + effectScope: { type: 'global' }, }); export const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({ @@ -48,10 +49,12 @@ export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ }, deletionDialog: initialDeletionDialogState(), creationDialog: initialCreationDialogState(), + policies: { type: 'UninitialisedResourceState' }, location: { page_index: MANAGEMENT_DEFAULT_PAGE, page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, show: undefined, + id: undefined, view_type: 'grid', }, active: false, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index 064b108848d2f..ed45d077dd0ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -21,10 +21,11 @@ import { } from '../test_utils'; import { TrustedAppsService } from '../service'; -import { Pagination, TrustedAppsListPageState } from '../state'; +import { Pagination, TrustedAppsListPageLocation, TrustedAppsListPageState } from '../state'; import { initialTrustedAppsPageState } from './builders'; import { trustedAppsPageReducer } from './reducer'; import { createTrustedAppsPageMiddleware } from './middleware'; +import { Immutable } from '../../../../../common/endpoint/types'; const initialNow = 111111; const dateNowMock = jest.fn(); @@ -32,7 +33,7 @@ dateNowMock.mockReturnValue(initialNow); Date.now = dateNowMock; -const initialState = initialTrustedAppsPageState(); +const initialState: Immutable = initialTrustedAppsPageState(); const createGetTrustedListAppsResponse = (pagination: Partial) => { const fullPagination = { ...createDefaultPagination(), ...pagination }; @@ -49,6 +50,9 @@ const createTrustedAppsServiceMock = (): jest.Mocked => ({ getTrustedAppsList: jest.fn(), deleteTrustedApp: jest.fn(), createTrustedApp: jest.fn(), + getPolicyList: jest.fn(), + updateTrustedApp: jest.fn(), + getTrustedApp: jest.fn(), }); const createStoreSetup = (trustedAppsService: TrustedAppsService) => { @@ -87,6 +91,15 @@ describe('middleware', () => { }; }; + const createLocationState = ( + params?: Partial + ): TrustedAppsListPageLocation => { + return { + ...initialState.location, + ...(params ?? {}), + }; + }; + beforeEach(() => { dateNowMock.mockReturnValue(initialNow); }); @@ -102,7 +115,10 @@ describe('middleware', () => { describe('refreshing list resource state', () => { it('refreshes the list when location changes and data gets outdated', async () => { const pagination = { pageIndex: 2, pageSize: 50 }; - const location = { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' }; + const location = createLocationState({ + page_index: 2, + page_size: 50, + }); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -136,7 +152,10 @@ describe('middleware', () => { it('does not refresh the list when location changes and data does not get outdated', async () => { const pagination = { pageIndex: 2, pageSize: 50 }; - const location = { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' }; + const location = createLocationState({ + page_index: 2, + page_size: 50, + }); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -161,7 +180,7 @@ describe('middleware', () => { it('refreshes the list when data gets outdated with and outdate action', async () => { const newNow = 222222; const pagination = { pageIndex: 0, pageSize: 10 }; - const location = { page_index: 0, page_size: 10, show: undefined, view_type: 'grid' }; + const location = createLocationState(); const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -224,7 +243,10 @@ describe('middleware', () => { freshDataTimestamp: initialNow, }, active: true, - location: { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' }, + location: createLocationState({ + page_index: 2, + page_size: 50, + }), }); const infiniteLoopTest = async () => { @@ -240,7 +262,7 @@ describe('middleware', () => { const entry = createSampleTrustedApp(3); const notFoundError = createServerApiError('Not Found'); const pagination = { pageIndex: 0, pageSize: 10 }; - const location = { page_index: 0, page_size: 10, show: undefined, view_type: 'grid' }; + const location = createLocationState(); const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination); const listView = createLoadedListViewWithPagination(initialNow, pagination); const listViewNew = createLoadedListViewWithPagination(newNow, pagination); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 3e83b213f0f7e..7f940f14f9c6c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { Immutable, PostTrustedAppCreateRequest, @@ -54,7 +55,15 @@ import { getListTotalItemsCount, trustedAppsListPageActive, entriesExistState, + policiesState, + isEdit, + isFetchingEditTrustedAppItem, + editItemId, + editingTrustedApp, + getListItems, + editItemState, } from './selectors'; +import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; const createTrustedAppsListResourceStateChangedAction = ( newState: Immutable> @@ -139,9 +148,11 @@ const submitCreationIfNeeded = async ( store: ImmutableMiddlewareAPI, trustedAppsService: TrustedAppsService ) => { - const submissionResourceState = getCreationSubmissionResourceState(store.getState()); - const isValid = isCreationDialogFormValid(store.getState()); - const entry = getCreationDialogFormEntry(store.getState()); + const currentState = store.getState(); + const submissionResourceState = getCreationSubmissionResourceState(currentState); + const isValid = isCreationDialogFormValid(currentState); + const entry = getCreationDialogFormEntry(currentState); + const editMode = isEdit(currentState); if (isStaleResourceState(submissionResourceState) && entry !== undefined && isValid) { store.dispatch( @@ -152,12 +163,27 @@ const submitCreationIfNeeded = async ( ); try { + let responseTrustedApp: TrustedApp; + + if (editMode) { + responseTrustedApp = ( + await trustedAppsService.updateTrustedApp( + { id: editItemId(currentState)! }, + // TODO: try to remove the cast + entry as PostTrustedAppCreateRequest + ) + ).data; + } else { + // TODO: try to remove the cast + responseTrustedApp = ( + await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest) + ).data; + } + store.dispatch( createTrustedAppCreationSubmissionResourceStateChanged({ type: 'LoadedResourceState', - // TODO: try to remove the cast - data: (await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest)) - .data, + data: responseTrustedApp, }) ); store.dispatch({ @@ -268,6 +294,139 @@ const checkTrustedAppsExistIfNeeded = async ( } }; +export const retrieveListOfPoliciesIfNeeded = async ( + { getState, dispatch }: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const currentState = getState(); + const currentPoliciesState = policiesState(currentState); + const isLoading = isLoadingResourceState(currentPoliciesState); + const isPageActive = trustedAppsListPageActive(currentState); + const isCreateFlow = isCreationDialogLocation(currentState); + + if (isPageActive && isCreateFlow && !isLoading) { + dispatch({ + type: 'trustedAppsPoliciesStateChanged', + payload: { + type: 'LoadingResourceState', + previousState: currentPoliciesState, + } as TrustedAppsListPageState['policies'], + }); + + try { + const policyList = await trustedAppsService.getPolicyList({ + query: { + page: 1, + perPage: 1000, + }, + }); + + dispatch({ + type: 'trustedAppsPoliciesStateChanged', + payload: { + type: 'LoadedResourceState', + data: policyList, + }, + }); + } catch (error) { + dispatch({ + type: 'trustedAppsPoliciesStateChanged', + payload: { + type: 'FailedResourceState', + error: error.body || error, + lastLoadedState: getLastLoadedResourceState(policiesState(getState())), + }, + }); + } + } +}; + +const fetchEditTrustedAppIfNeeded = async ( + { getState, dispatch }: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const currentState = getState(); + const isPageActive = trustedAppsListPageActive(currentState); + const isEditFlow = isEdit(currentState); + const isAlreadyFetching = isFetchingEditTrustedAppItem(currentState); + const editTrustedAppId = editItemId(currentState); + + if (isPageActive && isEditFlow && !isAlreadyFetching) { + if (!editTrustedAppId) { + const errorMessage = i18n.translate( + 'xpack.securitySolution.trustedapps.middleware.editIdMissing', + { + defaultMessage: 'No id provided', + } + ); + + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'FailedResourceState', + error: Object.assign(new Error(errorMessage), { statusCode: 404, error: errorMessage }), + }, + }); + return; + } + + let trustedAppForEdit = editingTrustedApp(currentState); + + // If Trusted App is already loaded, then do nothing + if (trustedAppForEdit && trustedAppForEdit.id === editTrustedAppId) { + return; + } + + // See if we can get the Trusted App record from the current list of Trusted Apps being displayed + trustedAppForEdit = getListItems(currentState).find((ta) => ta.id === editTrustedAppId); + + try { + // Retrieve Trusted App record via API if it was not in the list data. + // This would be the case when linking from another place or using an UUID for a Trusted App + // that is not currently displayed on the list view. + if (!trustedAppForEdit) { + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'LoadingResourceState', + // No easy way to get around this that I can see. `previousState` does not + // seem to allow everything that `editItem` state can hold, so not even sure if using + // type guards would work here + // @ts-ignore + previousState: editItemState(currentState)!, + }, + }); + + trustedAppForEdit = (await trustedAppsService.getTrustedApp({ id: editTrustedAppId })).data; + } + + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'LoadedResourceState', + data: trustedAppForEdit, + }, + }); + + dispatch({ + type: 'trustedAppCreationDialogFormStateUpdated', + payload: { + entry: toUpdateTrustedApp(trustedAppForEdit), + isValid: true, + }, + }); + } catch (e) { + dispatch({ + type: 'trustedAppCreationEditItemStateChanged', + payload: { + type: 'FailedResourceState', + error: e, + }, + }); + } + } +}; + export const createTrustedAppsPageMiddleware = ( trustedAppsService: TrustedAppsService ): ImmutableMiddleware => { @@ -282,6 +441,8 @@ export const createTrustedAppsPageMiddleware = ( if (action.type === 'userChangedUrl') { updateCreationDialogIfNeeded(store); + retrieveListOfPoliciesIfNeeded(store, trustedAppsService); + fetchEditTrustedAppIfNeeded(store, trustedAppsService); } if (action.type === 'trustedAppCreationDialogConfirmed') { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 5f37d0d674558..6965172ef773d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -37,7 +37,13 @@ describe('reducer', () => { expect(result).toStrictEqual({ ...initialState, - location: { page_index: 5, page_size: 50, show: 'create', view_type: 'list' }, + location: { + page_index: 5, + page_size: 50, + show: 'create', + view_type: 'list', + id: undefined, + }, active: true, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index aff5cacf081c6..ea7bbb44c9bf2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -29,6 +29,8 @@ import { TrustedAppCreationDialogConfirmed, TrustedAppCreationDialogClosed, TrustedAppsExistResponse, + TrustedAppsPoliciesStateChanged, + TrustedAppCreationEditItemStateChanged, } from './action'; import { TrustedAppsListPageState } from '../state'; @@ -37,7 +39,7 @@ import { initialDeletionDialogState, initialTrustedAppsPageState, } from './builders'; -import { entriesExistState } from './selectors'; +import { entriesExistState, trustedAppsListPageActive } from './selectors'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -110,7 +112,7 @@ const trustedAppCreationDialogStarted: CaseReducer = ( + state, + action +) => { + return { + ...state, + creationDialog: { ...state.creationDialog, editItem: action.payload }, + }; +}; + const trustedAppCreationDialogConfirmed: CaseReducer = ( state ) => { @@ -155,6 +167,16 @@ const updateEntriesExists: CaseReducer = (state, { pay return state; }; +const updatePolicies: CaseReducer = (state, { payload }) => { + if (trustedAppsListPageActive(state)) { + return { + ...state, + policies: payload, + }; + } + return state; +}; + export const trustedAppsPageReducer: StateReducer = ( state = initialTrustedAppsPageState(), action @@ -187,6 +209,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppCreationDialogFormStateUpdated': return trustedAppCreationDialogFormStateUpdated(state, action); + case 'trustedAppCreationEditItemStateChanged': + return handleUpdateToEditItemState(state, action); + case 'trustedAppCreationDialogConfirmed': return trustedAppCreationDialogConfirmed(state, action); @@ -198,6 +223,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppsExistStateChanged': return updateEntriesExists(state, action); + + case 'trustedAppsPoliciesStateChanged': + return updatePolicies(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index baa68eb314140..7c131c3eaa7a9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -24,6 +24,7 @@ import { TrustedAppsListPageLocation, TrustedAppsListPageState, } from '../state'; +import { GetPolicyListResponse } from '../../policy/types'; export const needsRefreshOfListData = (state: Immutable): boolean => { const freshDataTimestamp = state.listView.freshDataTimestamp; @@ -130,7 +131,7 @@ export const getDeletionDialogEntry = ( }; export const isCreationDialogLocation = (state: Immutable): boolean => { - return state.location.show === 'create'; + return !!state.location.show; }; export const getCreationSubmissionResourceState = ( @@ -185,3 +186,56 @@ export const entriesExist: (state: Immutable) => boole export const trustedAppsListPageActive: (state: Immutable) => boolean = ( state ) => state.active; + +export const policiesState = ( + state: Immutable +): Immutable => state.policies; + +export const loadingPolicies: ( + state: Immutable +) => boolean = createSelector(policiesState, (policies) => isLoadingResourceState(policies)); + +export const listOfPolicies: ( + state: Immutable +) => Immutable = createSelector(policiesState, (policies) => { + return isLoadedResourceState(policies) ? policies.data.items : []; +}); + +export const isEdit: (state: Immutable) => boolean = createSelector( + getCurrentLocation, + ({ show }) => { + return show === 'edit'; + } +); + +export const editItemId: ( + state: Immutable +) => string | undefined = createSelector(getCurrentLocation, ({ id }) => { + return id; +}); + +export const editItemState: ( + state: Immutable +) => Immutable['creationDialog']['editItem'] = (state) => { + return state.creationDialog.editItem; +}; + +export const isFetchingEditTrustedAppItem: ( + state: Immutable +) => boolean = createSelector(editItemState, (editTrustedAppState) => { + return editTrustedAppState ? isLoadingResourceState(editTrustedAppState) : false; +}); + +export const editTrustedAppFetchError: ( + state: Immutable +) => ServerApiError | undefined = createSelector(editItemState, (itemForEditState) => { + return itemForEditState && getCurrentResourceError(itemForEditState); +}); + +export const editingTrustedApp: ( + state: Immutable +) => undefined | Immutable = createSelector(editItemState, (editTrustedAppState) => { + if (editTrustedAppState && isLoadedResourceState(editTrustedAppState)) { + return editTrustedAppState.data; + } +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index faf111b1a55d8..faffc6b04a0cd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -44,12 +44,16 @@ const generate = (count: number, generator: (i: number) => T) => export const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedApp => { return { id: String(i), + version: 'abc123', name: generate(longTexts ? 10 : 1, () => `trusted app ${i}`).join(' '), description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '), created_at: '1 minute ago', created_by: 'someone', + updated_at: '1 minute ago', + updated_by: 'someone', os: OPERATING_SYSTEMS[i % 3], entries: [], + effectScope: { type: 'global' }, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap new file mode 100644 index 0000000000000..35fc520558d6a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -0,0 +1,5573 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`When on the Trusted Apps Page and the Add Trusted App button is clicked and there is a feature flag for agents policy should display agents policy if feature flag is enabled 1`] = ` +Object { + "asFragment": [Function], + "baseElement": + .c0 { + padding: 24px; +} + +.c0.siemWrapperPage--fullHeight { + height: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.c0.siemWrapperPage--noPadding { + padding: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.c0.siemWrapperPage--withTimeline { + padding-bottom: 70px; +} + +.c3 { + margin-top: 8px; +} + +.c3 .siemSubtitle__item { + color: #6a717d; + font-size: 12px; + line-height: 1.5; +} + +.c1 { + margin-bottom: 24px; +} + +.c2 { + display: block; +} + +.c4 .euiFlyout { + z-index: 4001; +} + +.c5 .and-badge { + padding-top: 20px; + padding-bottom: calc(32px + (8px * 2) + 3px); +} + +.c5 .group-entries { + margin-bottom: 8px; +} + +.c5 .group-entries > * { + margin-bottom: 8px; +} + +.c5 .group-entries > *:last-child { + margin-bottom: 0; +} + +.c5 .and-button { + min-width: 95px; +} + +.c6 .policy-name .euiSelectableListItem__text { + -webkit-text-decoration: none !important; + text-decoration: none !important; + color: #343741 !important; +} + +.c7 { + background-color: #f5f7fa; + padding: 16px; +} + +.c10 { + padding: 16px; +} + +.c8.c8.c8 { + width: 40%; +} + +.c9.c9.c9 { + width: 60%; +} + +@media only screen and (min-width:575px) { + .c3 .siemSubtitle__item { + display: inline-block; + margin-right: 16px; + } + + .c3 .siemSubtitle__item:last-child { + margin-right: 0; + } +} + +
+
+
+
+
+

+ Trusted Applications +

+
+

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

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