From 0f5620e5c48bdbad3a6b44f4c34b62d858fc8e2a Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 10 Jun 2021 09:17:22 +0100 Subject: [PATCH 01/99] [ML] Fixes display of job group badges in recognizer wizard (#101775) --- .../jobs/new_job/recognize/components/edit_job.tsx | 6 +++--- .../jobs/new_job/recognize/components/job_item.tsx | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx index d2780691e551d9..408e86512ed6dc 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx @@ -51,9 +51,9 @@ export const EditJob: FC = ({ job, jobOverride, existingGroupIds, ); const handleValidation = () => { - const jobGroupsValidationResult = - formState.jobGroups ?? - [].map((group) => groupValidator(group)).filter((result) => result !== null); + const jobGroupsValidationResult = (formState.jobGroups ?? []) + .map((group) => groupValidator(group)) + .filter((result) => result !== null); setValidationResult({ jobGroups: jobGroupsValidationResult, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index b97c16e02d9007..0daf4f28f33d31 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -87,12 +87,11 @@ export const JobItem: FC = memo( - {jobGroups ?? - [].map((group) => ( - - {group} - - ))} + {(jobGroups ?? []).map((group) => ( + + {group} + + ))} {setupResult && setupResult.error && ( From 144e014dbf7a85e6f22c9ef25c2596c58114bd9d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 10 Jun 2021 11:45:25 +0300 Subject: [PATCH 02/99] [Cases] Improve connectors mapping (#101145) --- .../cases/common/api/connectors/index.ts | 2 + .../configure_cases/mapping.test.tsx | 2 +- .../configure_cases/translations.ts | 3 +- .../plugins/cases/server/client/cases/push.ts | 5 +- .../cases/server/client/cases/utils.test.ts | 9 + .../cases/server/client/cases/utils.ts | 25 +- .../cases/server/client/configure/client.ts | 41 +- .../client/configure/create_mappings.ts | 25 +- .../server/client/configure/get_fields.ts | 37 - .../server/client/configure/get_mappings.ts | 12 +- .../cases/server/client/configure/mock.ts | 657 ------------------ .../cases/server/client/configure/types.ts | 10 +- .../client/configure/update_mappings.ts | 25 +- .../server/client/configure/utils.test.ts | 33 - .../cases/server/client/configure/utils.ts | 125 ---- .../cases/server/connectors/factory.ts | 28 + .../plugins/cases/server/connectors/index.ts | 15 +- ...rvice_formatter.test.ts => format.test.ts} | 13 +- ...xternal_service_formatter.ts => format.ts} | 14 +- .../cases/server/connectors/jira/index.ts | 15 + .../cases/server/connectors/jira/mapping.ts | 28 + .../cases/server/connectors/jira/types.ts | 17 + ...rvice_formatter.test.ts => format.test.ts} | 6 +- ...xternal_service_formatter.ts => format.ts} | 10 +- .../server/connectors/resilient/index.ts | 15 + .../server/connectors/resilient/mapping.ts | 28 + .../server/connectors/resilient/types.ts | 13 + .../server/connectors/servicenow/index.ts | 23 + ..._formmater.test.ts => itsm_format.test.ts} | 8 +- .../{itsm_formatter.ts => itsm_format.ts} | 10 +- .../connectors/servicenow/itsm_mapping.ts | 28 + ...r_formatter.test.ts => sir_format.test.ts} | 12 +- .../{sir_formatter.ts => sir_format.ts} | 24 +- .../connectors/servicenow/sir_mapping.ts | 28 + .../server/connectors/servicenow/types.ts | 35 + .../plugins/cases/server/connectors/types.ts | 13 +- .../api/__fixtures__/mock_saved_objects.ts | 85 +-- .../tests/common/configure/patch_configure.ts | 37 +- .../tests/common/configure/post_configure.ts | 121 +++- 39 files changed, 516 insertions(+), 1121 deletions(-) delete mode 100644 x-pack/plugins/cases/server/client/configure/get_fields.ts delete mode 100644 x-pack/plugins/cases/server/client/configure/mock.ts delete mode 100644 x-pack/plugins/cases/server/client/configure/utils.test.ts delete mode 100644 x-pack/plugins/cases/server/client/configure/utils.ts create mode 100644 x-pack/plugins/cases/server/connectors/factory.ts rename x-pack/plugins/cases/server/connectors/jira/{external_service_formatter.test.ts => format.test.ts} (71%) rename x-pack/plugins/cases/server/connectors/jira/{external_service_formatter.ts => format.ts} (59%) create mode 100644 x-pack/plugins/cases/server/connectors/jira/index.ts create mode 100644 x-pack/plugins/cases/server/connectors/jira/mapping.ts create mode 100644 x-pack/plugins/cases/server/connectors/jira/types.ts rename x-pack/plugins/cases/server/connectors/resilient/{external_service_formatter.test.ts => format.test.ts} (76%) rename x-pack/plugins/cases/server/connectors/resilient/{external_service_formatter.ts => format.ts} (56%) create mode 100644 x-pack/plugins/cases/server/connectors/resilient/index.ts create mode 100644 x-pack/plugins/cases/server/connectors/resilient/mapping.ts create mode 100644 x-pack/plugins/cases/server/connectors/resilient/types.ts create mode 100644 x-pack/plugins/cases/server/connectors/servicenow/index.ts rename x-pack/plugins/cases/server/connectors/servicenow/{itsm_formmater.test.ts => itsm_format.test.ts} (74%) rename x-pack/plugins/cases/server/connectors/servicenow/{itsm_formatter.ts => itsm_format.ts} (58%) create mode 100644 x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.ts rename x-pack/plugins/cases/server/connectors/servicenow/{sir_formatter.test.ts => sir_format.test.ts} (90%) rename x-pack/plugins/cases/server/connectors/servicenow/{sir_formatter.ts => sir_format.ts} (76%) create mode 100644 x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.ts create mode 100644 x-pack/plugins/cases/server/connectors/servicenow/types.ts diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index f9b7c8b12c2cd1..2a81396025d9af 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -38,6 +38,8 @@ export enum ConnectorTypes { none = '.none', } +export const connectorTypes = Object.values(ConnectorTypes); + const ConnectorJiraTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.jira), fields: rt.union([JiraFieldsRT, rt.null]), diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx index 0a1da1219342ed..3cf01be40eb4ae 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx @@ -50,7 +50,7 @@ describe('Mapping', () => { wrappingComponent: TestProviders, }); expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe( - 'Field mappings require an established connection to ServiceNow ITSM. Please check your connection credentials.' + 'Failed to retrieve mappings for ServiceNow ITSM.' ); }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 2fb2133ba470c1..a379b03a4f675f 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -102,8 +102,7 @@ export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => { export const FIELD_MAPPING_DESC_ERR = (thirdPartyName: string): string => { return i18n.translate('xpack.cases.configureCases.fieldMappingDescErr', { values: { thirdPartyName }, - defaultMessage: - 'Field mappings require an established connection to { thirdPartyName }. Please check your connection credentials.', + defaultMessage: 'Failed to retrieve mappings for { thirdPartyName }.', }); }; export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index dd527122d06168..c232f73c2a2351 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -25,6 +25,7 @@ import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } fro import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; +import { casesConnectors } from '../../connectors'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -110,8 +111,7 @@ export const push = async ( }); const connectorMappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector.id, - connectorType: connector.actionTypeId, + connector: theCase.connector, }); if (connectorMappings.length === 0) { @@ -125,6 +125,7 @@ export const push = async ( connector: connector as ActionConnector, mappings: connectorMappings[0].attributes.mappings, alerts, + casesConnectors, }); const pushRes = await actionsClient.execute({ diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 9f18fa4931e62e..bfd5d1279420be 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -30,6 +30,7 @@ import { } from './utils'; import { flattenCaseSavedObject } from '../../common'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { casesConnectors } from '../../connectors'; const formatComment = { commentId: commentObj.id, @@ -443,6 +444,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res).toEqual({ @@ -471,6 +473,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res.comments).toEqual([ @@ -501,6 +504,7 @@ describe('utils', () => { }, ], alerts: [], + casesConnectors, }); expect(res.comments).toEqual([]); @@ -531,6 +535,7 @@ describe('utils', () => { }, ], alerts: [], + casesConnectors, }); expect(res.comments).toEqual([ @@ -561,6 +566,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res.comments).toEqual([ @@ -595,6 +601,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res).toEqual({ @@ -626,6 +633,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }).catch((e) => { expect(e).not.toBeNull(); expect(e).toEqual( @@ -645,6 +653,7 @@ describe('utils', () => { connector: { ...connector, actionTypeId: 'not-supported' }, mappings, alerts: [], + casesConnectors, }).catch((e) => { expect(e).not.toBeNull(); expect(e).toEqual(new Error('Invalid external service')); diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index ebcc5a07b4edde..d920c517a00044 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -12,17 +12,15 @@ import { CaseFullExternalService, CaseResponse, CaseUserActionsResponse, - CommentAttributes, - CommentRequestAlertType, - CommentRequestUserType, CommentResponse, CommentResponseAlertsType, CommentType, ConnectorMappingsAttributes, - ConnectorTypes, + CommentAttributes, + CommentRequestUserType, + CommentRequestAlertType, } from '../../../common'; import { ActionsClient } from '../../../../actions/server'; -import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; import { BasicParams, @@ -39,6 +37,7 @@ import { TransformFieldsArgs, } from './types'; import { getAlertIds } from '../utils'; +import { CasesConnectorsMap } from '../../connectors'; interface CreateIncidentArgs { actionsClient: ActionsClient; @@ -47,6 +46,7 @@ interface CreateIncidentArgs { connector: ActionConnector; mappings: ConnectorMappingsAttributes[]; alerts: CasesClientGetAlertsResponse; + casesConnectors: CasesConnectorsMap; } export const getLatestPushInfo = ( @@ -70,9 +70,6 @@ export const getLatestPushInfo = ( return null; }; -const isConnectorSupported = (connectorId: string): connectorId is FormatterConnectorTypes => - Object.values(ConnectorTypes).includes(connectorId as ConnectorTypes); - const getCommentContent = (comment: CommentResponse): string => { if (comment.type === CommentType.user) { return comment.comment; @@ -99,6 +96,7 @@ export const createIncident = async ({ connector, mappings, alerts, + casesConnectors, }: CreateIncidentArgs): Promise => { const { comments: caseComments, @@ -110,20 +108,15 @@ export const createIncident = async ({ updated_by: updatedBy, } = theCase; - if (!isConnectorSupported(connector.actionTypeId)) { - throw new Error('Invalid external service'); - } - const params = { title, description, createdAt, createdBy, updatedAt, updatedBy }; const latestPushInfo = getLatestPushInfo(connector.id, userActions); const externalId = latestPushInfo?.pushedInfo?.external_id ?? null; const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated']; let currentIncident: ExternalServiceParams | undefined; - const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format( - theCase, - alerts - ); + const externalServiceFields = + casesConnectors.get(connector.actionTypeId)?.format(theCase, alerts) ?? {}; + let incident: Partial = { ...externalServiceFields }; if (externalId) { diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index d95667d5eee047..2f486556e4ae54 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -26,7 +26,6 @@ import { excess, GetConfigureFindRequest, GetConfigureFindRequestRt, - GetFieldsResponse, throwErrors, CasesConfigurationsResponse, CaseConfigurationsResponseRt, @@ -41,7 +40,6 @@ import { } from '../../common'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; -import { getFields } from './get_fields'; import { getMappings } from './get_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -49,12 +47,7 @@ import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter } from '../utils'; -import { - ConfigurationGetFields, - MappingsArgs, - CreateMappingsArgs, - UpdateMappingsArgs, -} from './types'; +import { MappingsArgs, CreateMappingsArgs, UpdateMappingsArgs } from './types'; import { createMappings } from './create_mappings'; import { updateMappings } from './update_mappings'; import { @@ -69,7 +62,6 @@ import { * @ignore */ export interface InternalConfigureSubClient { - getFields(params: ConfigurationGetFields): Promise; getMappings( params: MappingsArgs ): Promise['saved_objects']>; @@ -116,12 +108,9 @@ export const createInternalConfigurationSubClient = ( casesClientInternal: CasesClientInternal ): InternalConfigureSubClient => { const configureSubClient: InternalConfigureSubClient = { - getFields: (params: ConfigurationGetFields) => getFields(params, clientArgs), getMappings: (params: MappingsArgs) => getMappings(params, clientArgs), - createMappings: (params: CreateMappingsArgs) => - createMappings(params, clientArgs, casesClientInternal), - updateMappings: (params: UpdateMappingsArgs) => - updateMappings(params, clientArgs, casesClientInternal), + createMappings: (params: CreateMappingsArgs) => createMappings(params, clientArgs), + updateMappings: (params: UpdateMappingsArgs) => updateMappings(params, clientArgs), }; return Object.freeze(configureSubClient); @@ -194,8 +183,7 @@ async function get( if (connector != null) { try { mappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector.id, - connectorType: connector.type, + connector: transformESConnectorToCaseConnector(connector), }); } catch (e) { error = e.isBoom @@ -303,22 +291,22 @@ async function update( try { const resMappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector != null ? connector.id : configuration.attributes.connector.id, - connectorType: connector != null ? connector.type : configuration.attributes.connector.type, + connector: + connector != null + ? connector + : transformESConnectorToCaseConnector(configuration.attributes.connector), }); mappings = resMappings.length > 0 ? resMappings[0].attributes.mappings : []; if (connector != null) { if (resMappings.length !== 0) { mappings = await casesClientInternal.configuration.updateMappings({ - connectorId: connector.id, - connectorType: connector.type, + connector, mappingId: resMappings[0].id, }); } else { mappings = await casesClientInternal.configuration.createMappings({ - connectorId: connector.id, - connectorType: connector.type, + connector, owner: configuration.attributes.owner, }); } @@ -326,9 +314,9 @@ async function update( } catch (e) { error = e.isBoom ? e.output.payload.message - : `Error connecting to ${ + : `Error creating mapping for ${ connector != null ? connector.name : configuration.attributes.connector.name - } instance`; + }`; } const patch = await caseConfigureService.patch({ @@ -429,14 +417,13 @@ async function create( try { mappings = await casesClientInternal.configuration.createMappings({ - connectorId: configuration.connector.id, - connectorType: configuration.connector.type, + connector: configuration.connector, owner: configuration.owner, }); } catch (e) { error = e.isBoom ? e.output.payload.message - : `Error connecting to ${configuration.connector.name} instance`; + : `Error creating mapping for ${configuration.connector.name}`; } const post = await caseConfigureService.post({ diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index b01f10d7a9e43b..bb4c32ae57071a 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -5,40 +5,33 @@ * 2.0. */ -import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappingsAttributes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; -import { CasesClientArgs, CasesClientInternal } from '..'; +import { CasesClientArgs } from '..'; import { CreateMappingsArgs } from './types'; +import { casesConnectors } from '../../connectors'; export const createMappings = async ( - { connectorType, connectorId, owner }: CreateMappingsArgs, - clientArgs: CasesClientArgs, - casesClientInternal: CasesClientInternal + { connector, owner }: CreateMappingsArgs, + clientArgs: CasesClientArgs ): Promise => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { - if (connectorType === ConnectorTypes.none) { - return []; - } - - const res = await casesClientInternal.configuration.getFields({ - connectorId, - connectorType, - }); + const mappings = casesConnectors.get(connector.type)?.getMapping() ?? []; const theMapping = await connectorMappingsService.post({ unsecuredSavedObjectsClient, attributes: { - mappings: res.defaultMappings, + mappings, owner, }, references: [ { type: ACTION_SAVED_OBJECT_TYPE, name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, - id: connectorId, + id: connector.id, }, ], }); @@ -46,7 +39,7 @@ export const createMappings = async ( return theMapping.attributes.mappings; } catch (error) { throw createCaseError({ - message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.ts b/x-pack/plugins/cases/server/client/configure/get_fields.ts deleted file mode 100644 index 78627cfaca6ed1..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; - -import { GetFieldsResponse } from '../../../common/api'; -import { createDefaultMapping, formatFields } from './utils'; -import { CasesClientArgs } from '..'; - -interface ConfigurationGetFields { - connectorId: string; - connectorType: string; -} - -export const getFields = async ( - { connectorType, connectorId }: ConfigurationGetFields, - clientArgs: CasesClientArgs -): Promise => { - const { actionsClient } = clientArgs; - const results = await actionsClient.execute({ - actionId: connectorId, - params: { - subAction: 'getFields', - subActionParams: {}, - }, - }); - if (results.status === 'error') { - throw Boom.failedDependency(results.serviceMessage); - } - const fields = formatFields(results.data, connectorType); - - return { fields, defaultMappings: createDefaultMapping(fields, connectorType) }; -}; diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index 3489c06b1da5ad..2fa0e8454bacfa 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -6,29 +6,25 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { ConnectorMappings, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappings } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; import { CasesClientArgs } from '..'; import { MappingsArgs } from './types'; export const getMappings = async ( - { connectorType, connectorId }: MappingsArgs, + { connector }: MappingsArgs, clientArgs: CasesClientArgs ): Promise['saved_objects']> => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { - if (connectorType === ConnectorTypes.none) { - return []; - } - const myConnectorMappings = await connectorMappingsService.find({ unsecuredSavedObjectsClient, options: { hasReference: { type: ACTION_SAVED_OBJECT_TYPE, - id: connectorId, + id: connector.id, }, }, }); @@ -36,7 +32,7 @@ export const getMappings = async ( return myConnectorMappings.saved_objects; } catch (error) { throw createCaseError({ - message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + message: `Failed to retrieve mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/configure/mock.ts b/x-pack/plugins/cases/server/client/configure/mock.ts deleted file mode 100644 index ad982a5cc12434..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/mock.ts +++ /dev/null @@ -1,657 +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 { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; -import { - JiraGetFieldsResponse, - ResilientGetFieldsResponse, - ServiceNowGetFieldsResponse, -} from './utils.test'; -interface TestMappings { - [key: string]: ConnectorMappingsAttributes[]; -} -export const mappings: TestMappings = { - [ConnectorTypes.jira]: [ - { - source: 'title', - target: 'summary', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, - ], - [`${ConnectorTypes.jira}-alt`]: [ - { - source: 'title', - target: 'title', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, - ], - [ConnectorTypes.resilient]: [ - { - source: 'title', - target: 'name', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, - ], - [ConnectorTypes.serviceNowITSM]: [ - { - source: 'title', - target: 'short_description', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - action_type: 'append', - }, - ], - [ConnectorTypes.serviceNowSIR]: [ - { - source: 'title', - target: 'short_description', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - action_type: 'append', - }, - ], -}; - -const jiraFields: JiraGetFieldsResponse = { - summary: { - required: true, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'string', - }, - name: 'Summary', - }, - issuetype: { - required: true, - allowedValues: [ - { - self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10023', - id: '10023', - description: 'A problem or error.', - iconUrl: - 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10303&avatarType=issuetype', - name: 'Bug', - subtask: false, - avatarId: 10303, - }, - ], - defaultValue: {}, - schema: { - type: 'issuetype', - }, - name: 'Issue Type', - }, - attachment: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'array', - items: 'attachment', - }, - name: 'Attachment', - }, - duedate: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'date', - }, - name: 'Due date', - }, - description: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'string', - }, - name: 'Description', - }, - project: { - required: true, - allowedValues: [ - { - self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10015', - id: '10015', - key: 'RJ2', - name: 'RJ2', - projectTypeKey: 'business', - simplified: false, - avatarUrls: { - '48x48': - 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10015&avatarId=10412', - '24x24': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10015&avatarId=10412', - '16x16': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10015&avatarId=10412', - '32x32': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10015&avatarId=10412', - }, - }, - ], - defaultValue: {}, - schema: { - type: 'project', - }, - name: 'Project', - }, - assignee: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'user', - }, - name: 'Assignee', - }, - labels: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'array', - items: 'string', - }, - name: 'Labels', - }, -}; -const resilientFields: ResilientGetFieldsResponse = [ - { input_type: 'text', name: 'addr', read_only: false, text: 'Address' }, - { - input_type: 'boolean', - name: 'alberta_health_risk_assessment', - read_only: false, - text: 'Alberta Health Risk Assessment', - }, - { input_type: 'number', name: 'hard_liability', read_only: true, text: 'Assessed Liability' }, - { input_type: 'text', name: 'city', read_only: false, text: 'City' }, - { input_type: 'select', name: 'country', read_only: false, text: 'Country/Region' }, - { input_type: 'select_owner', name: 'creator_id', read_only: true, text: 'Created By' }, - { input_type: 'select', name: 'crimestatus_id', read_only: false, text: 'Criminal Activity' }, - { input_type: 'boolean', name: 'data_encrypted', read_only: false, text: 'Data Encrypted' }, - { input_type: 'select', name: 'data_format', read_only: false, text: 'Data Format' }, - { input_type: 'datetimepicker', name: 'end_date', read_only: true, text: 'Date Closed' }, - { input_type: 'datetimepicker', name: 'create_date', read_only: true, text: 'Date Created' }, - { - input_type: 'datetimepicker', - name: 'determined_date', - read_only: false, - text: 'Date Determined', - }, - { - input_type: 'datetimepicker', - name: 'discovered_date', - read_only: false, - required: 'always', - text: 'Date Discovered', - }, - { input_type: 'datetimepicker', name: 'start_date', read_only: false, text: 'Date Occurred' }, - { input_type: 'select', name: 'exposure_dept_id', read_only: false, text: 'Department' }, - { input_type: 'textarea', name: 'description', read_only: false, text: 'Description' }, - { input_type: 'boolean', name: 'employee_involved', read_only: false, text: 'Employee Involved' }, - { input_type: 'boolean', name: 'data_contained', read_only: false, text: 'Exposure Resolved' }, - { input_type: 'select', name: 'exposure_type_id', read_only: false, text: 'Exposure Type' }, - { - input_type: 'multiselect', - name: 'gdpr_breach_circumstances', - read_only: false, - text: 'GDPR Breach Circumstances', - }, - { input_type: 'select', name: 'gdpr_breach_type', read_only: false, text: 'GDPR Breach Type' }, - { - input_type: 'textarea', - name: 'gdpr_breach_type_comment', - read_only: false, - text: 'GDPR Breach Type Comment', - }, - { input_type: 'select', name: 'gdpr_consequences', read_only: false, text: 'GDPR Consequences' }, - { - input_type: 'textarea', - name: 'gdpr_consequences_comment', - read_only: false, - text: 'GDPR Consequences Comment', - }, - { - input_type: 'select', - name: 'gdpr_final_assessment', - read_only: false, - text: 'GDPR Final Assessment', - }, - { - input_type: 'textarea', - name: 'gdpr_final_assessment_comment', - read_only: false, - text: 'GDPR Final Assessment Comment', - }, - { - input_type: 'select', - name: 'gdpr_identification', - read_only: false, - text: 'GDPR Identification', - }, - { - input_type: 'textarea', - name: 'gdpr_identification_comment', - read_only: false, - text: 'GDPR Identification Comment', - }, - { - input_type: 'select', - name: 'gdpr_personal_data', - read_only: false, - text: 'GDPR Personal Data', - }, - { - input_type: 'textarea', - name: 'gdpr_personal_data_comment', - read_only: false, - text: 'GDPR Personal Data Comment', - }, - { - input_type: 'boolean', - name: 'gdpr_subsequent_notification', - read_only: false, - text: 'GDPR Subsequent Notification', - }, - { input_type: 'number', name: 'id', read_only: true, text: 'ID' }, - { input_type: 'boolean', name: 'impact_likely', read_only: false, text: 'Impact Likely' }, - { - input_type: 'boolean', - name: 'ny_impact_likely', - read_only: false, - text: 'Impact Likely for New York', - }, - { - input_type: 'boolean', - name: 'or_impact_likely', - read_only: false, - text: 'Impact Likely for Oregon', - }, - { - input_type: 'boolean', - name: 'wa_impact_likely', - read_only: false, - text: 'Impact Likely for Washington', - }, - { input_type: 'boolean', name: 'confirmed', read_only: false, text: 'Incident Disposition' }, - { input_type: 'multiselect', name: 'incident_type_ids', read_only: false, text: 'Incident Type' }, - { - input_type: 'text', - name: 'exposure_individual_name', - read_only: false, - text: 'Individual Name', - }, - { - input_type: 'select', - name: 'harmstatus_id', - read_only: false, - text: 'Is harm/risk/misuse foreseeable?', - }, - { input_type: 'text', name: 'jurisdiction_name', read_only: false, text: 'Jurisdiction' }, - { - input_type: 'datetimepicker', - name: 'inc_last_modified_date', - read_only: true, - text: 'Last Modified', - }, - { - input_type: 'multiselect', - name: 'gdpr_lawful_data_processing_categories', - read_only: false, - text: 'Lawful Data Processing Categories', - }, - { input_type: 'multiselect_members', name: 'members', read_only: false, text: 'Members' }, - { input_type: 'text', name: 'name', read_only: false, required: 'always', text: 'Name' }, - { input_type: 'boolean', name: 'negative_pr_likely', read_only: false, text: 'Negative PR' }, - { input_type: 'datetimepicker', name: 'due_date', read_only: true, text: 'Next Due Date' }, - { - input_type: 'multiselect', - name: 'nist_attack_vectors', - read_only: false, - text: 'NIST Attack Vectors', - }, - { input_type: 'select', name: 'org_handle', read_only: true, text: 'Organization' }, - { input_type: 'select_owner', name: 'owner_id', read_only: false, text: 'Owner' }, - { input_type: 'select', name: 'phase_id', read_only: true, text: 'Phase' }, - { - input_type: 'select', - name: 'pipeda_other_factors', - read_only: false, - text: 'PIPEDA Other Factors', - }, - { - input_type: 'textarea', - name: 'pipeda_other_factors_comment', - read_only: false, - text: 'PIPEDA Other Factors Comment', - }, - { - input_type: 'select', - name: 'pipeda_overall_assessment', - read_only: false, - text: 'PIPEDA Overall Assessment', - }, - { - input_type: 'textarea', - name: 'pipeda_overall_assessment_comment', - read_only: false, - text: 'PIPEDA Overall Assessment Comment', - }, - { - input_type: 'select', - name: 'pipeda_probability_of_misuse', - read_only: false, - text: 'PIPEDA Probability of Misuse', - }, - { - input_type: 'textarea', - name: 'pipeda_probability_of_misuse_comment', - read_only: false, - text: 'PIPEDA Probability of Misuse Comment', - }, - { - input_type: 'select', - name: 'pipeda_sensitivity_of_pi', - read_only: false, - text: 'PIPEDA Sensitivity of PI', - }, - { - input_type: 'textarea', - name: 'pipeda_sensitivity_of_pi_comment', - read_only: false, - text: 'PIPEDA Sensitivity of PI Comment', - }, - { input_type: 'text', name: 'reporter', read_only: false, text: 'Reporting Individual' }, - { - input_type: 'select', - name: 'resolution_id', - read_only: false, - required: 'close', - text: 'Resolution', - }, - { - input_type: 'textarea', - name: 'resolution_summary', - read_only: false, - required: 'close', - text: 'Resolution Summary', - }, - { input_type: 'select', name: 'gdpr_harm_risk', read_only: false, text: 'Risk of Harm' }, - { input_type: 'select', name: 'severity_code', read_only: false, text: 'Severity' }, - { input_type: 'boolean', name: 'inc_training', read_only: true, text: 'Simulation' }, - { input_type: 'multiselect', name: 'data_source_ids', read_only: false, text: 'Source of Data' }, - { input_type: 'select', name: 'state', read_only: false, text: 'State' }, - { input_type: 'select', name: 'plan_status', read_only: false, text: 'Status' }, - { input_type: 'select', name: 'exposure_vendor_id', read_only: false, text: 'Vendor' }, - { - input_type: 'boolean', - name: 'data_compromised', - read_only: false, - text: 'Was personal information or personal data involved?', - }, - { - input_type: 'select', - name: 'workspace', - read_only: false, - required: 'always', - text: 'Workspace', - }, - { input_type: 'text', name: 'zip', read_only: false, text: 'Zip' }, -]; -const serviceNowFields: ServiceNowGetFieldsResponse = [ - { - column_label: 'Approval', - mandatory: 'false', - max_length: '40', - element: 'approval', - }, - { - column_label: 'Close notes', - mandatory: 'false', - max_length: '4000', - element: 'close_notes', - }, - { - column_label: 'Contact type', - mandatory: 'false', - max_length: '40', - element: 'contact_type', - }, - { - column_label: 'Correlation display', - mandatory: 'false', - max_length: '100', - element: 'correlation_display', - }, - { - column_label: 'Correlation ID', - mandatory: 'false', - max_length: '100', - element: 'correlation_id', - }, - { - column_label: 'Description', - mandatory: 'false', - max_length: '4000', - element: 'description', - }, - { - column_label: 'Number', - mandatory: 'false', - max_length: '40', - element: 'number', - }, - { - column_label: 'Short description', - mandatory: 'false', - max_length: '160', - element: 'short_description', - }, - { - column_label: 'Created by', - mandatory: 'false', - max_length: '40', - element: 'sys_created_by', - }, - { - column_label: 'Updated by', - mandatory: 'false', - max_length: '40', - element: 'sys_updated_by', - }, - { - column_label: 'Upon approval', - mandatory: 'false', - max_length: '40', - element: 'upon_approval', - }, - { - column_label: 'Upon reject', - mandatory: 'false', - max_length: '40', - element: 'upon_reject', - }, -]; -interface FormatFieldsTestData { - expected: ConnectorField[]; - fields: JiraGetFieldsResponse | ResilientGetFieldsResponse | ServiceNowGetFieldsResponse; - type: ConnectorTypes; -} -export const formatFieldsTestData: FormatFieldsTestData[] = [ - { - expected: [ - { id: 'summary', name: 'Summary', required: true, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'text' }, - ], - fields: jiraFields, - type: ConnectorTypes.jira, - }, - { - expected: [ - { id: 'addr', name: 'Address', required: false, type: 'text' }, - { id: 'city', name: 'City', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { - id: 'gdpr_breach_type_comment', - name: 'GDPR Breach Type Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_consequences_comment', - name: 'GDPR Consequences Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_final_assessment_comment', - name: 'GDPR Final Assessment Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_identification_comment', - name: 'GDPR Identification Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_personal_data_comment', - name: 'GDPR Personal Data Comment', - required: false, - type: 'textarea', - }, - { id: 'exposure_individual_name', name: 'Individual Name', required: false, type: 'text' }, - { id: 'jurisdiction_name', name: 'Jurisdiction', required: false, type: 'text' }, - { id: 'name', name: 'Name', required: true, type: 'text' }, - { - id: 'pipeda_other_factors_comment', - name: 'PIPEDA Other Factors Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_overall_assessment_comment', - name: 'PIPEDA Overall Assessment Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_probability_of_misuse_comment', - name: 'PIPEDA Probability of Misuse Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_sensitivity_of_pi_comment', - name: 'PIPEDA Sensitivity of PI Comment', - required: false, - type: 'textarea', - }, - { id: 'reporter', name: 'Reporting Individual', required: false, type: 'text' }, - { id: 'resolution_summary', name: 'Resolution Summary', required: false, type: 'textarea' }, - { id: 'zip', name: 'Zip', required: false, type: 'text' }, - ], - fields: resilientFields, - type: ConnectorTypes.resilient, - }, - { - expected: [ - { id: 'approval', name: 'Approval', required: false, type: 'text' }, - { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, - { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, - { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, - { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { id: 'number', name: 'Number', required: false, type: 'text' }, - { id: 'short_description', name: 'Short description', required: false, type: 'text' }, - { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, - { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, - { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, - { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, - ], - fields: serviceNowFields, - type: ConnectorTypes.serviceNowITSM, - }, - { - expected: [ - { id: 'approval', name: 'Approval', required: false, type: 'text' }, - { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, - { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, - { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, - { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { id: 'number', name: 'Number', required: false, type: 'text' }, - { id: 'short_description', name: 'Short description', required: false, type: 'text' }, - { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, - { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, - { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, - { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, - ], - fields: serviceNowFields, - type: ConnectorTypes.serviceNowSIR, - }, -]; -export const mockGetFieldsResponse = { - status: 'ok', - data: jiraFields, - actionId: '123', -}; - -export const actionsErrResponse = { - status: 'error', - serviceMessage: 'this is an actions error', -}; diff --git a/x-pack/plugins/cases/server/client/configure/types.ts b/x-pack/plugins/cases/server/client/configure/types.ts index a34251690db48d..aca3436c59082e 100644 --- a/x-pack/plugins/cases/server/client/configure/types.ts +++ b/x-pack/plugins/cases/server/client/configure/types.ts @@ -5,9 +5,10 @@ * 2.0. */ +import { CaseConnector } from '../../../common'; + export interface MappingsArgs { - connectorType: string; - connectorId: string; + connector: CaseConnector; } export interface CreateMappingsArgs extends MappingsArgs { @@ -17,8 +18,3 @@ export interface CreateMappingsArgs extends MappingsArgs { export interface UpdateMappingsArgs extends MappingsArgs { mappingId: string; } - -export interface ConfigurationGetFields { - connectorId: string; - connectorType: string; -} diff --git a/x-pack/plugins/cases/server/client/configure/update_mappings.ts b/x-pack/plugins/cases/server/client/configure/update_mappings.ts index 7eccf4cbbe5829..3d529e51e75614 100644 --- a/x-pack/plugins/cases/server/client/configure/update_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/update_mappings.ts @@ -5,40 +5,33 @@ * 2.0. */ -import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappingsAttributes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; -import { CasesClientArgs, CasesClientInternal } from '..'; +import { CasesClientArgs } from '..'; import { UpdateMappingsArgs } from './types'; +import { casesConnectors } from '../../connectors'; export const updateMappings = async ( - { connectorType, connectorId, mappingId }: UpdateMappingsArgs, - clientArgs: CasesClientArgs, - casesClientInternal: CasesClientInternal + { connector, mappingId }: UpdateMappingsArgs, + clientArgs: CasesClientArgs ): Promise => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { - if (connectorType === ConnectorTypes.none) { - return []; - } - - const res = await casesClientInternal.configuration.getFields({ - connectorId, - connectorType, - }); + const mappings = casesConnectors.get(connector.type)?.getMapping() ?? []; const theMapping = await connectorMappingsService.update({ unsecuredSavedObjectsClient, mappingId, attributes: { - mappings: res.defaultMappings, + mappings, }, references: [ { type: ACTION_SAVED_OBJECT_TYPE, name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, - id: connectorId, + id: connector.id, }, ], }); @@ -46,7 +39,7 @@ export const updateMappings = async ( return theMapping.attributes.mappings ?? []; } catch (error) { throw createCaseError({ - message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/configure/utils.test.ts b/x-pack/plugins/cases/server/client/configure/utils.test.ts deleted file mode 100644 index 41d62f5a9b91f9..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/utils.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { - JiraGetFieldsResponse, - ResilientGetFieldsResponse, - ServiceNowGetFieldsResponse, -} from '../../../../actions/server/types'; -import { createDefaultMapping, formatFields } from './utils'; -import { mappings, formatFieldsTestData } from './mock'; - -describe('client/configure/utils', () => { - describe('formatFields', () => { - formatFieldsTestData.forEach(({ expected, fields, type }) => { - it(`normalizes ${type} fields to common type ConnectorField`, () => { - const result = formatFields(fields, type); - expect(result).toEqual(expected); - }); - }); - }); - describe('createDefaultMapping', () => { - formatFieldsTestData.forEach(({ expected, fields, type }) => { - it(`normalizes ${type} fields to common type ConnectorField`, () => { - const result = createDefaultMapping(expected, type); - expect(result).toEqual(mappings[type]); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts deleted file mode 100644 index 24efb6ca54b3a3..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ /dev/null @@ -1,125 +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 { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; -import { - JiraGetFieldsResponse, - ResilientGetFieldsResponse, - ServiceNowGetFieldsResponse, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../actions/server/types'; - -const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] => - Object.keys(jiraFields).reduce( - (acc, data) => - jiraFields[data].schema.type === 'string' - ? [ - ...acc, - { - id: data, - name: jiraFields[data].name, - required: jiraFields[data].required, - type: 'text', - }, - ] - : acc, - [] - ); - -const normalizeResilientFields = (resilientFields: ResilientGetFieldsResponse): ConnectorField[] => - resilientFields.reduce( - (acc: ConnectorField[], data) => - (data.input_type === 'textarea' || data.input_type === 'text') && !data.read_only - ? [ - ...acc, - { - id: data.name, - name: data.text, - required: data.required === 'always', - type: data.input_type, - }, - ] - : acc, - [] - ); -const normalizeServiceNowFields = (snFields: ServiceNowGetFieldsResponse): ConnectorField[] => - snFields.reduce( - (acc, data) => [ - ...acc, - { - id: data.element, - name: data.column_label, - required: data.mandatory === 'true', - type: parseFloat(data.max_length) > 160 ? 'textarea' : 'text', - }, - ], - [] - ); - -export const formatFields = (theData: unknown, theType: string): ConnectorField[] => { - switch (theType) { - case ConnectorTypes.jira: - return normalizeJiraFields(theData as JiraGetFieldsResponse); - case ConnectorTypes.resilient: - return normalizeResilientFields(theData as ResilientGetFieldsResponse); - case ConnectorTypes.serviceNowITSM: - return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); - case ConnectorTypes.serviceNowSIR: - return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); - default: - return []; - } -}; - -const getPreferredFields = (theType: string) => { - let title: string = ''; - let description: string = ''; - let comments: string = ''; - - if (theType === ConnectorTypes.jira) { - title = 'summary'; - description = 'description'; - comments = 'comments'; - } else if (theType === ConnectorTypes.resilient) { - title = 'name'; - description = 'description'; - comments = 'comments'; - } else if ( - theType === ConnectorTypes.serviceNowITSM || - theType === ConnectorTypes.serviceNowSIR - ) { - title = 'short_description'; - description = 'description'; - comments = 'work_notes'; - } - - return { title, description, comments }; -}; - -export const createDefaultMapping = ( - fields: ConnectorField[], - theType: string -): ConnectorMappingsAttributes[] => { - const { description, title, comments } = getPreferredFields(theType); - return [ - { - source: 'title', - target: title, - action_type: 'overwrite', - }, - { - source: 'description', - target: description, - action_type: 'overwrite', - }, - { - source: 'comments', - target: comments, - action_type: 'append', - }, - ]; -}; diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts new file mode 100644 index 00000000000000..64e3e6f3eb225b --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/factory.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 { ConnectorTypes } from '../../common/api'; +import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; +import { ICasesConnector, CasesConnectorsMap } from './types'; + +const mapping: Record = { + [ConnectorTypes.jira]: getJiraCaseConnector(), + [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), + [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), + [ConnectorTypes.resilient]: getResilientCaseConnector(), + [ConnectorTypes.none]: null, +}; + +const isConnectorTypeSupported = (type: string): type is ConnectorTypes => + Object.values(ConnectorTypes).includes(type as ConnectorTypes); + +export const casesConnectors: CasesConnectorsMap = { + get: (type: string): ICasesConnector | undefined | null => + isConnectorTypeSupported(type) ? mapping[type] : undefined, +}; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index 4b6f845a961f21..b5dc1cc4a8ff9e 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -7,20 +7,16 @@ import { RegisterConnectorsArgs, - ExternalServiceFormatterMapper, CommentSchemaType, ContextTypeGeneratedAlertType, ContextTypeAlertSchemaType, } from './types'; import { getActionType as getCaseConnector } from './case'; -import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_formatter'; -import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter'; -import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; -import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; -import { CommentRequest, CommentType } from '../../common'; +import { CommentRequest, CommentType } from '../../common/api'; export * from './types'; export { transformConnectorComment } from './case'; +export { casesConnectors } from './factory'; /** * Separator used for creating a json parsable array from the mustache syntax that the alerting framework @@ -41,13 +37,6 @@ export const registerConnectors = ({ ); }; -export const externalServiceFormatters: ExternalServiceFormatterMapper = { - '.servicenow': serviceNowITSMExternalServiceFormatter, - '.servicenow-sir': serviceNowSIRExternalServiceFormatter, - '.jira': jiraExternalServiceFormatter, - '.resilient': resilientExternalServiceFormatter, -}; - export const isCommentGeneratedAlert = ( comment: CommentSchemaType | CommentRequest ): comment is ContextTypeGeneratedAlertType => { diff --git a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts b/x-pack/plugins/cases/server/connectors/jira/format.test.ts similarity index 71% rename from x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts rename to x-pack/plugins/cases/server/connectors/jira/format.test.ts index f5d76aeddf3130..edca4cf68250ce 100644 --- a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/jira/format.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; -import { jiraExternalServiceFormatter } from './external_service_formatter'; +import { CaseResponse } from '../../../common/api'; +import { format } from './format'; describe('Jira formatter', () => { const theCase = { @@ -15,21 +15,18 @@ describe('Jira formatter', () => { } as CaseResponse; it('it formats correctly', async () => { - const res = await jiraExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ ...theCase.connector.fields, labels: theCase.tags }); }); it('it formats correctly when fields do not exist ', async () => { const invalidFields = { tags: ['tag'], connector: { fields: null } } as CaseResponse; - const res = await jiraExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ priority: null, issueType: null, parent: null, labels: theCase.tags }); }); it('it replace white spaces with hyphens on tags', async () => { - const res = await jiraExternalServiceFormatter.format( - { ...theCase, tags: ['a tag with spaces'] }, - [] - ); + const res = await format({ ...theCase, tags: ['a tag with spaces'] }, []); expect(res).toEqual({ ...theCase.connector.fields, labels: ['a-tag-with-spaces'] }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/jira/format.ts similarity index 59% rename from x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts rename to x-pack/plugins/cases/server/connectors/jira/format.ts index 15ee2fd468ddaa..4caa23634d8875 100644 --- a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/jira/format.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; +import { ConnectorJiraTypeFields } from '../../../common/api'; +import { Format } from './types'; -interface ExternalServiceParams extends JiraFieldsType { - labels: string[]; -} - -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: Format = (theCase, alerts) => { const { priority = null, issueType = null, parent = null } = (theCase.connector.fields as ConnectorJiraTypeFields['fields']) ?? {}; return { @@ -23,7 +19,3 @@ const format: ExternalServiceFormatter['format'] = (theCa parent, }; }; - -export const jiraExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/jira/index.ts b/x-pack/plugins/cases/server/connectors/jira/index.ts new file mode 100644 index 00000000000000..9a2a00ac23b390 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/jira/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMapping } from './mapping'; +import { format } from './format'; +import { JiraCaseConnector } from './types'; + +export const getCaseConnector = (): JiraCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/jira/mapping.ts b/x-pack/plugins/cases/server/connectors/jira/mapping.ts new file mode 100644 index 00000000000000..8f8a914b4e091a --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/jira/mapping.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 { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'summary', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/jira/types.ts b/x-pack/plugins/cases/server/connectors/jira/types.ts new file mode 100644 index 00000000000000..59d5741d381b96 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/jira/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JiraFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +interface ExternalServiceFormatterParams extends JiraFieldsType { + labels: string[]; +} + +export type JiraCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts b/x-pack/plugins/cases/server/connectors/resilient/format.test.ts similarity index 76% rename from x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts rename to x-pack/plugins/cases/server/connectors/resilient/format.test.ts index b7096179b0fab6..20ba0bc3789343 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/format.test.ts @@ -6,7 +6,7 @@ */ import { CaseResponse } from '../../../common'; -import { resilientExternalServiceFormatter } from './external_service_formatter'; +import { format } from './format'; describe('IBM Resilient formatter', () => { const theCase = { @@ -14,13 +14,13 @@ describe('IBM Resilient formatter', () => { } as CaseResponse; it('it formats correctly', async () => { - const res = await resilientExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ ...theCase.connector.fields }); }); it('it formats correctly when fields do not exist ', async () => { const invalidFields = { tags: ['a tag'], connector: { fields: null } } as CaseResponse; - const res = await resilientExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ incidentTypes: null, severityCode: null }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/resilient/format.ts similarity index 56% rename from x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts rename to x-pack/plugins/cases/server/connectors/resilient/format.ts index 6dea452565d7c2..3e966d87686d20 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/format.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; +import { ConnectorResillientTypeFields } from '../../../common/api'; +import { Format } from './types'; -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: Format = (theCase, alerts) => { const { incidentTypes = null, severityCode = null } = (theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {}; return { incidentTypes, severityCode }; }; - -export const resilientExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/resilient/index.ts b/x-pack/plugins/cases/server/connectors/resilient/index.ts new file mode 100644 index 00000000000000..a946d0d7fa1c58 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/resilient/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMapping } from './mapping'; +import { format } from './format'; +import { ResilientCaseConnector } from './types'; + +export const getCaseConnector = (): ResilientCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/resilient/mapping.ts b/x-pack/plugins/cases/server/connectors/resilient/mapping.ts new file mode 100644 index 00000000000000..0226073711dfb9 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/resilient/mapping.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 { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'name', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/resilient/types.ts b/x-pack/plugins/cases/server/connectors/resilient/types.ts new file mode 100644 index 00000000000000..f895dccf652145 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/resilient/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ResilientFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export type ResilientCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/index.ts b/x-pack/plugins/cases/server/connectors/servicenow/index.ts new file mode 100644 index 00000000000000..e16a76ff5f79fb --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMapping as getServiceNowITSMMapping } from './itsm_mapping'; +import { format as formatServiceNowITSM } from './itsm_format'; +import { getMapping as getServiceNowSIRMapping } from './sir_mapping'; +import { format as formatServiceNowSIR } from './sir_format'; + +import { ServiceNowITSMCasesConnector, ServiceNowSIRCasesConnector } from './types'; + +export const getServiceNowITSMCaseConnector = (): ServiceNowITSMCasesConnector => ({ + getMapping: getServiceNowITSMMapping, + format: formatServiceNowITSM, +}); + +export const getServiceNowSIRCaseConnector = (): ServiceNowSIRCasesConnector => ({ + getMapping: getServiceNowSIRMapping, + format: formatServiceNowSIR, +}); diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts similarity index 74% rename from x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts rename to x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts index 78242e4c3848ab..2cc1816e7fa67e 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; -import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter'; +import { CaseResponse } from '../../../common/api'; +import { format } from './itsm_format'; describe('ITSM formatter', () => { const theCase = { @@ -16,13 +16,13 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly', async () => { - const res = await serviceNowITSMExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual(theCase.connector.fields); }); it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ severity: null, urgency: null, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts similarity index 58% rename from x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts rename to x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts index a4fa8a198fea77..9bf8c3e7e8b2e0 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; +import { ConnectorServiceNowITSMTypeFields } from '../../../common/api'; +import { ServiceNowITSMFormat } from './types'; -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: ServiceNowITSMFormat = (theCase, alerts) => { const { severity = null, urgency = null, impact = null, category = null, subcategory = null } = (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {}; return { severity, urgency, impact, category, subcategory }; }; - -export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.ts new file mode 100644 index 00000000000000..a94d72576d6e38 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.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 { ServiceNowITSMGetMapping } from './types'; + +export const getMapping: ServiceNowITSMGetMapping = () => { + return [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts similarity index 90% rename from x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts rename to x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index 1f7716424cfa9b..fa103d4c1142d6 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -6,7 +6,7 @@ */ import { CaseResponse } from '../../../common'; -import { serviceNowSIRExternalServiceFormatter } from './sir_formatter'; +import { format } from './sir_format'; describe('ITSM formatter', () => { const theCase = { @@ -24,7 +24,7 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly without alerts', async () => { - const res = await serviceNowSIRExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -38,7 +38,7 @@ describe('ITSM formatter', () => { it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = await serviceNowSIRExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -73,7 +73,7 @@ describe('ITSM formatter', () => { url: { full: 'https://attack.com/api' }, }, ]; - const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1,192.168.1.4', source_ip: '192.168.1.2,192.168.1.3', @@ -109,7 +109,7 @@ describe('ITSM formatter', () => { url: { full: 'https://attack.com/api' }, }, ]; - const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1', source_ip: '192.168.1.2,192.168.1.3', @@ -150,7 +150,7 @@ describe('ITSM formatter', () => { connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, } as CaseResponse; - const res = await serviceNowSIRExternalServiceFormatter.format(newCase, alerts); + const res = await format(newCase, alerts); expect(res).toEqual({ dest_ip: null, source_ip: '192.168.1.2,192.168.1.3', diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts similarity index 76% rename from x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts rename to x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 1c528cd2b47bfb..1c6e993d395696 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -5,23 +5,10 @@ * 2.0. */ import { get } from 'lodash/fp'; -import { ConnectorServiceNowSIRTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; -interface ExternalServiceParams { - dest_ip: string | null; - source_ip: string | null; - category: string | null; - subcategory: string | null; - malware_hash: string | null; - malware_url: string | null; - priority: string | null; -} -type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url'; -type AlertFieldMappingAndValues = Record< - string, - { alertPath: string; sirFieldKey: SirFieldKey; add: boolean } ->; -const format: ExternalServiceFormatter['format'] = (theCase, alerts) => { +import { ConnectorServiceNowSIRTypeFields } from '../../../common/api'; +import { ServiceNowSIRFormat, SirFieldKey, AlertFieldMappingAndValues } from './types'; + +export const format: ServiceNowSIRFormat = (theCase, alerts) => { const { destIp = null, sourceIp = null, @@ -83,6 +70,3 @@ const format: ExternalServiceFormatter['format'] = (theCa priority, }; }; -export const serviceNowSIRExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.ts new file mode 100644 index 00000000000000..04d9809bc8b996 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.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 { ServiceNowSIRGetMapping } from './types'; + +export const getMapping: ServiceNowSIRGetMapping = () => { + return [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/types.ts b/x-pack/plugins/cases/server/connectors/servicenow/types.ts new file mode 100644 index 00000000000000..500d1d22e3dcb8 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceNowITSMFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export interface ServiceNowSIRFieldsType { + dest_ip: string | null; + source_ip: string | null; + category: string | null; + subcategory: string | null; + malware_hash: string | null; + malware_url: string | null; + priority: string | null; +} + +export type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url'; +export type AlertFieldMappingAndValues = Record< + string, + { alertPath: string; sirFieldKey: SirFieldKey; add: boolean } +>; + +// ServiceNow ITSM +export type ServiceNowITSMCasesConnector = ICasesConnector; +export type ServiceNowITSMFormat = ICasesConnector['format']; +export type ServiceNowITSMGetMapping = ICasesConnector['getMapping']; + +// ServiceNow SIR +export type ServiceNowSIRCasesConnector = ICasesConnector; +export type ServiceNowSIRFormat = ICasesConnector['format']; +export type ServiceNowSIRGetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/types.ts b/x-pack/plugins/cases/server/connectors/types.ts index 98cbe9683546b6..2fab59037b1bdd 100644 --- a/x-pack/plugins/cases/server/connectors/types.ts +++ b/x-pack/plugins/cases/server/connectors/types.ts @@ -6,7 +6,7 @@ */ import { Logger } from 'kibana/server'; -import { CaseResponse, ConnectorTypes } from '../../common/api'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../common/api'; import { CasesClientGetAlertsResponse } from '../client/alerts/types'; import { CasesClientFactory } from '../client/factory'; import { RegisterActionType } from '../types'; @@ -26,12 +26,11 @@ export interface RegisterConnectorsArgs extends GetActionTypeParams { registerActionType: RegisterActionType; } -export type FormatterConnectorTypes = Exclude; - -export interface ExternalServiceFormatter { +export interface ICasesConnector { format: (theCase: CaseResponse, alerts: CasesClientGetAlertsResponse) => TExternalServiceParams; + getMapping: () => ConnectorMappingsAttributes[]; } -export type ExternalServiceFormatterMapper = { - [x in FormatterConnectorTypes]: ExternalServiceFormatter; -}; +export interface CasesConnectorsMap { + get: (type: string) => ICasesConnector | undefined | null; +} diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index bddceef8d782e6..ef5e0ebb5bbc10 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -10,20 +10,13 @@ import { AssociationType, CaseStatuses, CaseType, - CaseUserActionAttributes, CommentAttributes, CommentType, - ConnectorMappings, ConnectorTypes, ESCaseAttributes, ESCasesConfigureAttributes, } from '../../../../common'; -import { - CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - CASE_USER_ACTION_SAVED_OBJECT, - SECURITY_SOLUTION_OWNER, -} from '../../../../common/constants'; -import { mappings } from '../../../client/configure/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants'; export const mockCases: Array> = [ { @@ -485,79 +478,3 @@ export const mockCaseConfigure: Array> = version: 'WzYsMV0=', }, ]; - -export const mockCaseMappings: Array> = [ - { - type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - id: 'mock-mappings-1', - attributes: { - mappings: mappings[ConnectorTypes.jira], - owner: SECURITY_SOLUTION_OWNER, - }, - references: [], - }, -]; - -export const mockCaseMappingsResilient: Array> = [ - { - type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - id: 'mock-mappings-1', - attributes: { - mappings: mappings[ConnectorTypes.resilient], - owner: SECURITY_SOLUTION_OWNER, - }, - references: [], - }, -]; - -export const mockCaseMappingsBad: Array>> = [ - { - type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - id: 'mock-mappings-bad', - attributes: {}, - references: [], - }, -]; - -export const mockUserActions: Array> = [ - { - type: CASE_USER_ACTION_SAVED_OBJECT, - id: 'mock-user-actions-1', - attributes: { - action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], - action: 'create', - action_at: '2021-02-03T17:41:03.771Z', - action_by: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - new_value: - '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', - old_value: null, - owner: SECURITY_SOLUTION_OWNER, - }, - version: 'WzYsMV0=', - references: [], - }, - { - type: CASE_USER_ACTION_SAVED_OBJECT, - id: 'mock-user-actions-2', - attributes: { - action_field: ['comment'], - action: 'create', - action_at: '2021-02-03T17:44:21.067Z', - action_by: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - new_value: - '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', - old_value: null, - owner: SECURITY_SOLUTION_OWNER, - }, - version: 'WzYsMV0=', - references: [], - }, -]; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index 323b1b377e5555..0e92456b66c859 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts @@ -8,14 +8,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { - getConfigurationRequest, removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, deleteConfiguration, createConfiguration, updateConfiguration, + getConfigurationRequest, + getConfiguration, } from '../../../../common/lib/utils'; import { secOnly, @@ -52,6 +54,39 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); }); + it('should update mapping when changing connector', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration(supertest, configuration.id, { + connector: { + id: 'serviceNowITSM', + name: 'ServiceNow ITSM', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + version: configuration.version, + }); + const newConfiguration = await getConfiguration({ supertest }); + + expect(configuration.mappings).to.eql([]); + expect(newConfiguration[0].mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ]); + }); + it('should not patch a configuration with unsupported connector type', async () => { const configuration = await createConfiguration(supertest); await updateConfiguration( diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index 44ec24f688f201..fd9e8611db44ad 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -60,20 +60,135 @@ export default ({ getService }: FtrProviderContext): void => { expect(configuration.length).to.be(1); }); - it('should return an error when failing to get mapping', async () => { + it('should return an empty mapping when they type is none', async () => { const postRes = await createConfiguration( supertest, getConfigurationRequest({ id: 'not-exists', name: 'not-exists', - type: ConnectorTypes.jira, + type: ConnectorTypes.none, }) ); - expect(postRes.error).to.not.be(null); expect(postRes.mappings).to.eql([]); }); + it('should return the correct mapping for Jira', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'summary', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'comments', + }, + ]); + }); + + it('should return the correct mapping for IBM Resilient', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'resilient', + name: 'Resilient', + type: ConnectorTypes.resilient, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'name', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'comments', + }, + ]); + }); + + it('should return the correct mapping for ServiceNow ITSM', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'serviceNowITSM', + name: 'ServiceNow ITSM', + type: ConnectorTypes.serviceNowITSM, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ]); + }); + + it('should return the correct mapping for ServiceNow SecOps', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'serviceNowSIR', + name: 'ServiceNow SecOps', + type: ConnectorTypes.serviceNowSIR, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ]); + }); + it('should not create a configuration when missing connector.id', async () => { await createConfiguration( supertest, From 45ed0035b6272f1873cbf19472ec871a00c5f65c Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 10 Jun 2021 11:13:45 +0200 Subject: [PATCH 03/99] Allow navigating discover flyout via arrow keys (#101772) --- .../discover_grid_flyout.test.tsx | 20 +++++++++++++++++++ .../discover_grid/discover_grid_flyout.tsx | 19 +++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index 63d16af7de478a..60841799b1398b 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -143,4 +143,24 @@ describe('Discover flyout', function () { expect(props.setExpandedDoc).toHaveBeenCalledTimes(1); expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4'); }); + + it('allows navigating with arrow keys through documents', () => { + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); + expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ _id: '2' })); + component.setProps({ ...props, hit: props.hits[1] }); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' }); + expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ _id: '1' })); + }); + + it('should not navigate with keypresses when already at the border of documents', () => { + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' }); + expect(props.setExpandedDoc).not.toHaveBeenCalled(); + component.setProps({ ...props, hit: props.hits[props.hits.length - 1] }); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); + expect(props.setExpandedDoc).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index 3894127891041c..aaae9afe6531a5 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -21,6 +21,7 @@ import { EuiPortal, EuiPagination, EuiHideFor, + keys, } from '@elastic/eui'; import { DocViewer } from '../doc_viewer/doc_viewer'; import { IndexPattern } from '../../../kibana_services'; @@ -87,9 +88,25 @@ export function DiscoverGridFlyout({ [hits, setExpandedDoc] ); + const onKeyDown = useCallback( + (ev: React.KeyboardEvent) => { + if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) { + ev.preventDefault(); + ev.stopPropagation(); + setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1)); + } + }, + [activePage, setPage] + ); + return ( - + Date: Thu, 10 Jun 2021 08:08:36 -0400 Subject: [PATCH 04/99] [Security Solution][Endpoint] Adjust the Pending Actions API types (#101814) * Adjust Action Status API types * add lib/service to fetch host pending actions --- .../common/endpoint/types/actions.ts | 10 +++++-- .../index.test.ts | 0 .../index.ts | 0 .../mocks.ts | 0 .../endpoint_pending_actions.ts | 27 +++++++++++++++++++ .../lib/endpoint_pending_actions/index.ts | 8 ++++++ .../containers/detection_engine/alerts/api.ts | 2 +- .../endpoint_hosts/store/middleware.test.ts | 2 +- .../pages/endpoint_hosts/store/middleware.ts | 2 +- .../pages/endpoint_hosts/view/index.test.tsx | 2 +- .../server/endpoint/routes/actions/status.ts | 8 +++--- 11 files changed, 51 insertions(+), 10 deletions(-) rename x-pack/plugins/security_solution/public/common/lib/{host_isolation => endpoint_isolation}/index.test.ts (100%) rename x-pack/plugins/security_solution/public/common/lib/{host_isolation => endpoint_isolation}/index.ts (100%) rename x-pack/plugins/security_solution/public/common/lib/{host_isolation => endpoint_isolation}/mocks.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/index.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 33072e8df5cec5..937025f35dadcd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -6,7 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { HostIsolationRequestSchema } from '../schema/actions'; +import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema/actions'; export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; @@ -44,10 +44,16 @@ export interface HostIsolationResponse { action: string; } -export interface PendingActionsResponse { +export interface EndpointPendingActions { agent_id: string; pending_actions: { /** Number of actions pending for each type. The `key` could be one of the `ISOLATION_ACTIONS` values. */ [key: string]: number; }; } + +export interface PendingActionsResponse { + data: EndpointPendingActions[]; +} + +export type PendingActionsRequestQuery = TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/lib/host_isolation/index.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/index.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/host_isolation/index.test.ts rename to x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/index.test.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/index.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/host_isolation/index.ts rename to x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/index.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/host_isolation/mocks.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/mocks.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/host_isolation/mocks.ts rename to x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/mocks.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.ts new file mode 100644 index 00000000000000..e1feb8a14ab0bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/endpoint_pending_actions.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + PendingActionsRequestQuery, + PendingActionsResponse, +} from '../../../../common/endpoint/types'; +import { KibanaServices } from '../kibana'; +import { ACTION_STATUS_ROUTE } from '../../../../common/endpoint/constants'; + +/** + * Retrieve a list of pending actions against the given Fleet Agent Ids provided on input + * @param agentIds + */ +export const fetchPendingActionsByAgentId = ( + agentIds: PendingActionsRequestQuery['agent_ids'] +): Promise => { + return KibanaServices.get().http.get(ACTION_STATUS_ROUTE, { + query: { + agent_ids: agentIds, + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/index.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/index.ts new file mode 100644 index 00000000000000..e3f7c7c3be90e0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint_pending_actions/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './endpoint_pending_actions'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 4edbd5ab7e180c..3baa6580b36fbe 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -25,7 +25,7 @@ import { UpdateAlertStatusProps, CasesFromAlertsResponse, } from './types'; -import { isolateHost, unIsolateHost } from '../../../../common/lib/host_isolation'; +import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isolation'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 6548d8a10ce97f..98ef5a341ac9ef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -41,7 +41,7 @@ import { hostIsolationHttpMocks, hostIsolationRequestBodyMock, hostIsolationResponseMock, -} from '../../../../common/lib/host_isolation/mocks'; +} from '../../../../common/lib/endpoint_isolation/mocks'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; jest.mock('../../policy/store/services/ingest', () => ({ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 546116f82696b7..a1da3c072293ea 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -53,7 +53,7 @@ import { createLoadedResourceState, createLoadingResourceState, } from '../../../state'; -import { isolateHost, unIsolateHost } from '../../../../common/lib/host_isolation'; +import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isolation'; import { AppAction } from '../../../../common/store/actions'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; import { ServerReturnedEndpointPackageInfo } from './action'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 509bb7b4cf7111..aa1c47a3102d90 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -29,7 +29,7 @@ import { POLICY_STATUS_TO_TEXT } from './host_constants'; import { mockPolicyResultList } from '../../policy/store/test_mock_utils'; import { getEndpointDetailsPath } from '../../../common/routing'; import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kibana'; -import { hostIsolationHttpMocks } from '../../../../common/lib/host_isolation/mocks'; +import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks'; import { fireEvent } from '@testing-library/dom'; import { isFailedResourceState, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts index faaf41962a96cf..66bb51b459c12d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts @@ -10,7 +10,7 @@ import { TypeOf } from '@kbn/config-schema'; import { EndpointAction, EndpointActionResponse, - PendingActionsResponse, + EndpointPendingActions, } from '../../../../common/endpoint/types'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; import { ActionStatusRequestSchema } from '../../../../common/endpoint/schema/actions'; @@ -99,7 +99,7 @@ export const actionStatusRequestHandler = function ( const actionResponses = responseResults.body?.hits?.hits?.map((a) => a._source!) || []; // respond with action-count per agent - const response = agentIDs.map((aid) => { + const response: EndpointPendingActions[] = agentIDs.map((aid) => { const responseIDsFromAgent = actionResponses .filter((r) => r.agent_id === aid) .map((r) => r.action_id); @@ -115,8 +115,8 @@ export const actionStatusRequestHandler = function ( acc[cur] = 1; } return acc; - }, {} as PendingActionsResponse['pending_actions']), - } as PendingActionsResponse; + }, {} as EndpointPendingActions['pending_actions']), + }; }); return res.ok({ From f804585ec7d96d94c52cc9fafa545fe637e92209 Mon Sep 17 00:00:00 2001 From: SoNice! <62436227+d1v1b@users.noreply.github.com> Date: Thu, 10 Jun 2021 21:16:34 +0900 Subject: [PATCH 05/99] [FieldFormatters] Human-readable duration inconsistent unit casing (#101479) --- .../field_formats/converters/duration.test.ts | 30 +++++++++---------- .../field_formats/converters/duration.ts | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts index 72551f4b7b236f..9ea9919e757de8 100644 --- a/src/plugins/data/common/field_formats/converters/duration.test.ts +++ b/src/plugins/data/common/field_formats/converters/duration.test.ts @@ -130,11 +130,11 @@ describe('Duration Format', () => { fixtures: [ { input: -60, - output: '-60 Seconds', + output: '-60 seconds', }, { input: -32.333, - output: '-32 Seconds', + output: '-32 seconds', }, ], }); @@ -147,15 +147,15 @@ describe('Duration Format', () => { fixtures: [ { input: 1988, - output: '0.00 Milliseconds', + output: '0.00 milliseconds', }, { input: 658, - output: '0.00 Milliseconds', + output: '0.00 milliseconds', }, { input: 3857, - output: '0.00 Milliseconds', + output: '0.00 milliseconds', }, ], }); @@ -168,15 +168,15 @@ describe('Duration Format', () => { fixtures: [ { input: 1988, - output: '1.99 Milliseconds', + output: '1.99 milliseconds', }, { input: 658, - output: '0.66 Milliseconds', + output: '0.66 milliseconds', }, { input: 3857, - output: '3.86 Milliseconds', + output: '3.86 milliseconds', }, ], }); @@ -189,19 +189,19 @@ describe('Duration Format', () => { fixtures: [ { input: 1988, - output: '2.0 Milliseconds', + output: '2.0 milliseconds', }, { input: 0, - output: '0.0 Milliseconds', + output: '0.0 milliseconds', }, { input: 658, - output: '0.7 Milliseconds', + output: '0.7 milliseconds', }, { input: 3857, - output: '3.9 Milliseconds', + output: '3.9 milliseconds', }, ], }); @@ -214,15 +214,15 @@ describe('Duration Format', () => { fixtures: [ { input: 600, - output: '10 Minutes', + output: '10 minutes', }, { input: 30, - output: '30 Seconds', + output: '30 seconds', }, { input: 3000, - output: '50 Minutes', + output: '50 minutes', }, ], }); diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index c9a7091db84716..71ac022ba5e5cb 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -263,7 +263,7 @@ export class DurationFormat extends FieldFormat { const precise = human || humanPrecise ? formatted : formatted.toFixed(outputPrecision); const type = outputFormats.find(({ method }) => method === outputFormat); - const unitText = useShortSuffix ? type?.shortText : type?.text; + const unitText = useShortSuffix ? type?.shortText : type?.text.toLowerCase(); const suffix = showSuffix && unitText && !human ? `${includeSpace}${unitText}` : ''; @@ -294,7 +294,7 @@ function formatDuration( const getUnitText = (method: string) => { const type = outputFormats.find(({ method: methodT }) => method === methodT); - return useShortSuffix ? type?.shortText : type?.text; + return useShortSuffix ? type?.shortText : type?.text.toLowerCase(); }; for (let i = 0; i < units.length; i++) { From 65d3f49f000c766211a58b7c25f63d2fc2cf6df5 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 10 Jun 2021 08:36:43 -0400 Subject: [PATCH 06/99] address code review comments (#101764) --- x-pack/plugins/security/common/index.ts | 4 ++-- .../server/authentication/authentication_service.mock.ts | 4 ++-- .../server/authentication/authentication_service.ts | 4 ++-- x-pack/plugins/security/server/authentication/index.ts | 2 +- .../security/server/authorization/actions/actions.ts | 2 +- .../server/authorization/authorization_service.tsx | 2 +- x-pack/plugins/security/server/plugin.ts | 7 +++---- .../plugins/security/server/routes/api_keys/create.test.ts | 4 ++-- .../security/server/routes/api_keys/enabled.test.ts | 4 ++-- .../security/server/routes/authentication/common.test.ts | 4 ++-- .../security/server/routes/authentication/saml.test.ts | 4 ++-- x-pack/plugins/security/server/routes/index.ts | 4 ++-- .../security/server/routes/users/change_password.test.ts | 4 ++-- x-pack/plugins/security/server/saved_objects/index.ts | 2 +- 14 files changed, 25 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index 034c25758a1f0c..ac5d252c98a8b2 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { SecurityLicense } from './licensing'; -export { AuthenticatedUser } from './model'; +export type { SecurityLicense } from './licensing'; +export type { AuthenticatedUser } from './model'; diff --git a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts index ba73d3b196d2f8..9014e504b405b9 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts @@ -8,10 +8,10 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { apiKeysMock } from './api_keys/api_keys.mock'; -import type { AuthenticationServiceStartInternal } from './authentication_service'; +import type { InternalAuthenticationServiceStart } from './authentication_service'; export const authenticationServiceMock = { - createStart: (): DeeplyMockedKeys => ({ + createStart: (): DeeplyMockedKeys => ({ apiKeys: apiKeysMock.create(), login: jest.fn(), logout: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 946fedbeee04ff..79dcfb8d804b26 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -51,7 +51,7 @@ interface AuthenticationServiceStartParams { loggers: LoggerFactory; } -export interface AuthenticationServiceStartInternal extends AuthenticationServiceStart { +export interface InternalAuthenticationServiceStart extends AuthenticationServiceStart { apiKeys: Pick< APIKeys, | 'areAPIKeysEnabled' @@ -227,7 +227,7 @@ export class AuthenticationService { legacyAuditLogger, loggers, session, - }: AuthenticationServiceStartParams): AuthenticationServiceStartInternal { + }: AuthenticationServiceStartParams): InternalAuthenticationServiceStart { const apiKeys = new APIKeys({ clusterClient, logger: this.logger.get('api-key'), diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 4f82c5653baa9a..1e46d2aaf560ed 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -9,7 +9,7 @@ export { canRedirectRequest } from './can_redirect_request'; export { AuthenticationService, AuthenticationServiceStart, - AuthenticationServiceStartInternal, + InternalAuthenticationServiceStart, } from './authentication_service'; export { AuthenticationResult } from './authentication_result'; export { DeauthenticationResult } from './deauthentication_result'; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.ts b/x-pack/plugins/security/server/authorization/actions/actions.ts index d0466645213fa3..0234c3bc82042d 100644 --- a/x-pack/plugins/security/server/authorization/actions/actions.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.ts @@ -15,7 +15,7 @@ import { UIActions } from './ui'; /** Actions are used to create the "actions" that are associated with Elasticsearch's * application privileges, and are used to perform the authorization checks implemented - * by the various `checkPrivilegesWithRequest` derivatives + * by the various `checkPrivilegesWithRequest` derivatives. */ export class Actions { public readonly api = new ApiActions(this.versionNumber); diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx index c5adb3aff670e0..0777c231ecd89d 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.tsx +++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx @@ -89,7 +89,7 @@ export interface AuthorizationServiceSetup { /** * Actions are used to create the "actions" that are associated with Elasticsearch's * application privileges, and are used to perform the authorization checks implemented - * by the various `checkPrivilegesWithRequest` derivatives + * by the various `checkPrivilegesWithRequest` derivatives. */ actions: Actions; checkPrivilegesWithRequest: CheckPrivilegesWithRequest; diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 98f1335b534505..d0403c0f170ea8 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -37,12 +37,11 @@ import type { AuditServiceSetup } from './audit'; import { AuditService, SecurityAuditLogger } from './audit'; import type { AuthenticationServiceStart, - AuthenticationServiceStartInternal, + InternalAuthenticationServiceStart, } from './authentication'; import { AuthenticationService } from './authentication'; -import type { AuthorizationServiceSetup } from './authorization'; +import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } from './authorization'; import { AuthorizationService } from './authorization'; -import type { AuthorizationServiceSetupInternal } from './authorization/authorization_service'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; import { ElasticsearchService } from './elasticsearch'; @@ -156,7 +155,7 @@ export class SecurityPlugin private readonly authenticationService = new AuthenticationService( this.initializerContext.logger.get('authentication') ); - private authenticationStart?: AuthenticationServiceStartInternal; + private authenticationStart?: InternalAuthenticationServiceStart; private readonly getAuthentication = () => { if (!this.authenticationStart) { throw new Error(`authenticationStart is not registered!`); diff --git a/x-pack/plugins/security/server/routes/api_keys/create.test.ts b/x-pack/plugins/security/server/routes/api_keys/create.test.ts index ee28681adbd5f2..a86481b8016e8f 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.test.ts @@ -12,7 +12,7 @@ import type { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticationServiceStartInternal } from '../../authentication'; +import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRequestHandlerContext } from '../../types'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -28,7 +28,7 @@ describe('Create API Key route', () => { } let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { authc = authenticationServiceMock.createStart(); const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index 1000e79563b571..6a477d2600d3e2 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -12,7 +12,7 @@ import type { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticationServiceStartInternal } from '../../authentication'; +import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRequestHandlerContext } from '../../types'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -28,7 +28,7 @@ describe('API keys enabled', () => { } let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { authc = authenticationServiceMock.createStart(); const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 5acc5817bfb3e9..8320f88d1242a2 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -13,7 +13,7 @@ import { httpServerMock } from 'src/core/server/mocks'; import type { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStartInternal } from '../../authentication'; +import type { InternalAuthenticationServiceStart } from '../../authentication'; import { AuthenticationResult, DeauthenticationResult, @@ -28,7 +28,7 @@ import { defineCommonRoutes } from './common'; describe('Common authentication routes', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; let license: jest.Mocked; let mockContext: SecurityRequestHandlerContext; beforeEach(() => { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index c28c435217ef3a..d27ea5f6dca710 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -11,7 +11,7 @@ import type { RequestHandler, RouteConfig } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStartInternal } from '../../authentication'; +import type { InternalAuthenticationServiceStart } from '../../authentication'; import { AuthenticationResult, SAMLLogin } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRouter } from '../../types'; @@ -21,7 +21,7 @@ import { defineSAMLRoutes } from './saml'; describe('SAML authentication routes', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index e36ca1b9ab72e5..7a4310da3e4c7c 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -10,7 +10,7 @@ import type { HttpResources, IBasePath, Logger } from 'src/core/server'; import type { KibanaFeature } from '../../../features/server'; import type { SecurityLicense } from '../../common/licensing'; -import type { AuthenticationServiceStartInternal } from '../authentication'; +import type { InternalAuthenticationServiceStart } from '../authentication'; import type { AuthorizationServiceSetupInternal } from '../authorization'; import type { ConfigType } from '../config'; import type { SecurityFeatureUsageServiceStart } from '../feature_usage'; @@ -39,7 +39,7 @@ export interface RouteDefinitionParams { license: SecurityLicense; getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; - getAuthenticationService: () => AuthenticationServiceStartInternal; + getAuthenticationService: () => InternalAuthenticationServiceStart; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index e5f4381a204212..fba7bf4f872e7f 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -16,7 +16,7 @@ import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { AuthenticationResult } from '../../authentication'; -import type { AuthenticationServiceStartInternal } from '../../authentication/authentication_service'; +import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { Session } from '../../session_management'; import { sessionMock } from '../../session_management/session.mock'; @@ -26,7 +26,7 @@ import { defineChangeUserPasswordRoutes } from './change_password'; describe('Change password', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; let session: jest.Mocked>; let routeHandler: RequestHandler; let routeConfig: RouteConfig; diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 837b3c594d3968..364f639e9e9a3c 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -9,7 +9,7 @@ import type { CoreSetup, LegacyRequest } from 'src/core/server'; import { KibanaRequest, SavedObjectsClient } from '../../../../../src/core/server'; import type { AuditServiceSetup, SecurityAuditLogger } from '../audit'; -import type { AuthorizationServiceSetupInternal } from '../authorization/authorization_service'; +import type { AuthorizationServiceSetupInternal } from '../authorization'; import type { SpacesService } from '../plugin'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; From b2903e9f8fdccda6a9b6eebbd340f15324b8dc5d Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 10 Jun 2021 08:43:07 -0400 Subject: [PATCH 07/99] Small clean up with Cases plugin API (#101668) * Clean up public API of cases plugin 1. Don't use export * on index.ts files that define the public API 2. Add comments to the interface show they show up in the API docs 3. Export types that are part of the public API so they show up in the API docs. 4. Fill in information for the up and coming `description` and `owner` items in kibana.json. * Update returns comments to be more descriptive * update api docs * Remove kibana.json attributes, until PR supporting them is merged. * Change all exports to export type to avoid increase page bundle size --- api_docs/cases.json | 1173 +++++++++++++++++++------ api_docs/cases.mdx | 3 - x-pack/plugins/cases/kibana.json | 12 +- x-pack/plugins/cases/public/index.tsx | 11 +- x-pack/plugins/cases/public/types.ts | 30 + 5 files changed, 943 insertions(+), 286 deletions(-) diff --git a/api_docs/cases.json b/api_docs/cases.json index bc92995dff6e91..cccf5de2710e28 100644 --- a/api_docs/cases.json +++ b/api_docs/cases.json @@ -34,21 +34,9 @@ "text": "CasesUiStart" }, ", ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.SetupPlugins", - "text": "SetupPlugins" - }, + "SetupPlugins", ", ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" - }, + "StartPlugins", ">" ], "source": { @@ -117,13 +105,7 @@ "text": "CoreSetup" }, ", plugins: ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.SetupPlugins", - "text": "SetupPlugins" - }, + "SetupPlugins", ") => void" ], "source": { @@ -164,13 +146,7 @@ "label": "plugins", "description": [], "signature": [ - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.SetupPlugins", - "text": "SetupPlugins" - } + "SetupPlugins" ], "source": { "path": "x-pack/plugins/cases/public/plugin.ts", @@ -199,127 +175,676 @@ "text": "CoreStart" }, ", plugins: ", + "StartPlugins", + ") => ", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CasesUiStart", + "text": "CasesUiStart" + } + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 38 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.CasesUiPlugin.start.$1", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "public", + "docId": "kibCorePluginApi", + "section": "def-public.CoreStart", + "text": "CoreStart" + } + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 38 + }, + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "cases", + "id": "def-public.CasesUiPlugin.start.$2", + "type": "Object", + "tags": [], + "label": "plugins", + "description": [], + "signature": [ + "StartPlugins" + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 38 + }, + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "cases", + "id": "def-public.CasesUiPlugin.stop", + "type": "Function", + "tags": [], + "label": "stop", + "description": [], + "signature": [ + "() => void" + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 92 + }, + "deprecated": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [ + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps", + "type": "Interface", + "tags": [], + "label": "AllCasesProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 13 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.caseDetailsNavigation", + "type": "Object", + "tags": [], + "label": "caseDetailsNavigation", + "description": [], + "signature": [ + "CasesNavigation", + "<", + "CaseDetailsHrefSchema", + ", \"configurable\">" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 14 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.configureCasesNavigation", + "type": "Object", + "tags": [], + "label": "configureCasesNavigation", + "description": [], + "signature": [ + "CasesNavigation", + ", null>" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 15 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.createCaseNavigation", + "type": "Object", + "tags": [], + "label": "createCaseNavigation", + "description": [], + "signature": [ + "CasesNavigation", + ", null>" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 16 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.userCanCrud", + "type": "boolean", + "tags": [], + "label": "userCanCrud", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 17 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps", + "type": "Interface", + "tags": [], + "label": "AllCasesSelectorModalProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 23 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.alertData", + "type": "Object", + "tags": [], + "label": "alertData", + "description": [], + "signature": [ + "Pick<{ type: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CommentType", + "text": "CommentType" + }, + ".alert | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CommentType", + "text": "CommentType" + }, + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; }, \"index\" | \"rule\" | \"alertId\" | \"owner\"> | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 24 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.createCaseNavigation", + "type": "Object", + "tags": [], + "label": "createCaseNavigation", + "description": [], + "signature": [ + "CasesNavigation", + ", null>" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 25 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.hiddenStatuses", + "type": "Array", + "tags": [], + "label": "hiddenStatuses", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CaseStatusWithAllStatus", + "text": "CaseStatusWithAllStatus" + }, + "[] | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 26 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.onRowClick", + "type": "Function", + "tags": [], + "label": "onRowClick", + "description": [], + "signature": [ + "(theCase?: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + " | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.SubCase", + "text": "SubCase" + }, + " | undefined) => void" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 27 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.theCase", + "type": "CompoundType", + "tags": [], + "label": "theCase", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + " | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.SubCase", + "text": "SubCase" + }, + " | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 27 + }, + "deprecated": false + } + ] + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.updateCase", + "type": "Function", + "tags": [], + "label": "updateCase", + "description": [], + "signature": [ + "((newCase: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + ") => void) | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 28 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.userCanCrud", + "type": "boolean", + "tags": [], + "label": "userCanCrud", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 29 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CaseViewProps", + "type": "Interface", + "tags": [], + "label": "CaseViewProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + }, + " extends ", + "CaseViewComponentProps" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/case_view/index.tsx", + "lineNumber": 63 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.CaseViewProps.onCaseDataSuccess", + "type": "Function", + "tags": [], + "label": "onCaseDataSuccess", + "description": [], + "signature": [ + "((data: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + ") => void) | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/case_view/index.tsx", + "lineNumber": 64 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CaseViewProps.timelineIntegration", + "type": "Object", + "tags": [], + "label": "timelineIntegration", + "description": [], + "signature": [ + "CasesTimelineIntegration", + " | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/case_view/index.tsx", + "lineNumber": 65 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.ConfigureCasesProps", + "type": "Interface", + "tags": [], + "label": "ConfigureCasesProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/configure_cases/index.tsx", + "lineNumber": 55 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.ConfigureCasesProps.userCanCrud", + "type": "boolean", + "tags": [], + "label": "userCanCrud", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/configure_cases/index.tsx", + "lineNumber": 56 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps", + "type": "Interface", + "tags": [], + "label": "CreateCaseProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 34 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.afterCaseCreated", + "type": "Function", + "tags": [], + "label": "afterCaseCreated", + "description": [], + "signature": [ + "((theCase: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + ", postComment: (args: PostComment) => Promise) => Promise) | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 35 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.caseType", + "type": "CompoundType", + "tags": [], + "label": "caseType", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CaseType", + "text": "CaseType" + }, + " | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 36 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.hideConnectorServiceNowSir", + "type": "CompoundType", + "tags": [], + "label": "hideConnectorServiceNowSir", + "description": [], + "signature": [ + "boolean | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 37 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.onCancel", + "type": "Function", + "tags": [], + "label": "onCancel", + "description": [], + "signature": [ + "() => void" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 38 + }, + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.onSuccess", + "type": "Function", + "tags": [], + "label": "onSuccess", + "description": [], + "signature": [ + "(theCase: ", { "pluginId": "cases", - "scope": "public", + "scope": "common", "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" + "section": "def-common.Case", + "text": "Case" }, - ") => ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.CasesUiStart", - "text": "CasesUiStart" - } + ") => Promise" ], "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 38 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 39 }, "deprecated": false, + "returnComment": [], "children": [ { "parentPluginId": "cases", - "id": "def-public.CasesUiPlugin.start.$1", - "type": "Object", - "tags": [], - "label": "core", - "description": [], - "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.CoreStart", - "text": "CoreStart" - } - ], - "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 38 - }, - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "cases", - "id": "def-public.CasesUiPlugin.start.$2", + "id": "def-public.theCase", "type": "Object", "tags": [], - "label": "plugins", + "label": "theCase", "description": [], "signature": [ { "pluginId": "cases", - "scope": "public", + "scope": "common", "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" + "section": "def-common.Case", + "text": "Case" } ], "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 38 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 39 }, - "deprecated": false, - "isRequired": true + "deprecated": false } - ], - "returnComment": [] + ] }, { "parentPluginId": "cases", - "id": "def-public.CasesUiPlugin.stop", - "type": "Function", + "id": "def-public.CreateCaseProps.timelineIntegration", + "type": "Object", "tags": [], - "label": "stop", + "label": "timelineIntegration", "description": [], "signature": [ - "() => void" + "CasesTimelineIntegration", + " | undefined" ], "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 92 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 40 }, - "deprecated": false, - "children": [], - "returnComment": [] - } - ], - "initialIsOpen": false - } - ], - "functions": [], - "interfaces": [ - { - "parentPluginId": "cases", - "id": "def-public.Owner", - "type": "Interface", - "tags": [], - "label": "Owner", - "description": [], - "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 42 - }, - "deprecated": false, - "children": [ + "deprecated": false + }, { "parentPluginId": "cases", - "id": "def-public.Owner.owner", - "type": "Array", + "id": "def-public.CreateCaseProps.withSteps", + "type": "CompoundType", "tags": [], - "label": "owner", + "label": "withSteps", "description": [], "signature": [ - "string[]" + "boolean | undefined" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 43 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 41 }, "deprecated": false } @@ -328,148 +853,100 @@ }, { "parentPluginId": "cases", - "id": "def-public.SetupPlugins", + "id": "def-public.RecentCasesProps", "type": "Interface", "tags": [], - "label": "SetupPlugins", + "label": "RecentCasesProps", "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + }, + " extends ", + "Owner" + ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 22 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 20 }, "deprecated": false, "children": [ { "parentPluginId": "cases", - "id": "def-public.SetupPlugins.security", + "id": "def-public.RecentCasesProps.allCasesNavigation", "type": "Object", "tags": [], - "label": "security", + "label": "allCasesNavigation", "description": [], "signature": [ - { - "pluginId": "security", - "scope": "public", - "docId": "kibSecurityPluginApi", - "section": "def-public.SecurityPluginSetup", - "text": "SecurityPluginSetup" - } + "CasesNavigation", + ", null>" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 23 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 21 }, "deprecated": false }, { "parentPluginId": "cases", - "id": "def-public.SetupPlugins.triggersActionsUi", + "id": "def-public.RecentCasesProps.caseDetailsNavigation", "type": "Object", "tags": [], - "label": "triggersActionsUi", + "label": "caseDetailsNavigation", "description": [], "signature": [ - { - "pluginId": "triggersActionsUi", - "scope": "public", - "docId": "kibTriggersActionsUiPluginApi", - "section": "def-public.TriggersAndActionsUIPublicPluginSetup", - "text": "TriggersAndActionsUIPublicPluginSetup" - } + "CasesNavigation", + "<", + "CaseDetailsHrefSchema", + ", \"configurable\">" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 24 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 22 }, "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "cases", - "id": "def-public.StartPlugins", - "type": "Interface", - "tags": [], - "label": "StartPlugins", - "description": [], - "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 27 - }, - "deprecated": false, - "children": [ + }, { "parentPluginId": "cases", - "id": "def-public.StartPlugins.triggersActionsUi", + "id": "def-public.RecentCasesProps.createCaseNavigation", "type": "Object", "tags": [], - "label": "triggersActionsUi", + "label": "createCaseNavigation", "description": [], "signature": [ - { - "pluginId": "triggersActionsUi", - "scope": "public", - "docId": "kibTriggersActionsUiPluginApi", - "section": "def-public.TriggersAndActionsUIPublicPluginStart", - "text": "TriggersAndActionsUIPublicPluginStart" - } + "CasesNavigation", + ", null>" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 28 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 23 }, "deprecated": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "cases", - "id": "def-public.StartServices", - "type": "Type", - "tags": [], - "label": "StartServices", - "description": [ - "\nTODO: The extra security service is one that should be implemented in the kibana context of the consuming application.\nSecurity is needed for access to authc for the `useCurrentUser` hook. Security_Solution currently passes it via renderApp in public/plugin.tsx\nLeaving it out currently in lieu of RBAC changes" - ], - "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.CoreStart", - "text": "CoreStart" - }, - " & ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" }, - " & { security: ", { - "pluginId": "security", - "scope": "public", - "docId": "kibSecurityPluginApi", - "section": "def-public.SecurityPluginSetup", - "text": "SecurityPluginSetup" - }, - "; }" + "parentPluginId": "cases", + "id": "def-public.RecentCasesProps.maxCasesToShow", + "type": "number", + "tags": [], + "label": "maxCasesToShow", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 24 + }, + "deprecated": false + } ], - "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 37 - }, - "deprecated": false, "initialIsOpen": false } ], + "enums": [], + "misc": [], "objects": [], "start": { "parentPluginId": "cases", @@ -490,20 +967,36 @@ "type": "Function", "tags": [], "label": "getAllCases", - "description": [], + "description": [ + "\nGet the all cases table" + ], "signature": [ "(props: ", - "AllCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + }, ") => React.ReactElement<", - "AllCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 47 + "lineNumber": 52 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component that displays all cases" + ], "children": [ { "parentPluginId": "cases", @@ -511,13 +1004,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "AllCasesProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 47 + "lineNumber": 52 }, "deprecated": false } @@ -529,20 +1030,36 @@ "type": "Function", "tags": [], "label": "getAllCasesSelectorModal", - "description": [], + "description": [ + "\nuse Modal hook for all cases selector" + ], "signature": [ "(props: ", - "AllCasesSelectorModalProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + }, ") => React.ReactElement<", - "AllCasesSelectorModalProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 48 + "lineNumber": 58 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component that is a modal for selecting a case" + ], "children": [ { "parentPluginId": "cases", @@ -550,13 +1067,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], + "description": [ + "UseAllCasesSelectorModalProps" + ], "signature": [ - "AllCasesSelectorModalProps" + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + } ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 49 + "lineNumber": 59 }, "deprecated": false } @@ -568,20 +1093,36 @@ "type": "Function", "tags": [], "label": "getCaseView", - "description": [], + "description": [ + "\nGet the case view component" + ], "signature": [ "(props: ", - "CaseViewProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + }, ") => React.ReactElement<", - "CaseViewProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 51 + "lineNumber": 66 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for viewing a specific case" + ], "children": [ { "parentPluginId": "cases", @@ -589,13 +1130,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "CaseViewProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 51 + "lineNumber": 66 }, "deprecated": false } @@ -607,20 +1156,36 @@ "type": "Function", "tags": [], "label": "getConfigureCases", - "description": [], + "description": [ + "\nGet the configure case component" + ], "signature": [ "(props: ", - "ConfigureCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + }, ") => React.ReactElement<", - "ConfigureCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 52 + "lineNumber": 72 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for configuring a specific case" + ], "children": [ { "parentPluginId": "cases", @@ -628,13 +1193,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "ConfigureCasesProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 52 + "lineNumber": 72 }, "deprecated": false } @@ -646,20 +1219,36 @@ "type": "Function", "tags": [], "label": "getCreateCase", - "description": [], + "description": [ + "\nGet the create case form" + ], "signature": [ "(props: ", - "CreateCaseProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + }, ") => React.ReactElement<", - "CreateCaseProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 53 + "lineNumber": 78 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for creating a new case" + ], "children": [ { "parentPluginId": "cases", @@ -667,13 +1256,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "CreateCaseProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 53 + "lineNumber": 78 }, "deprecated": false } @@ -685,20 +1282,36 @@ "type": "Function", "tags": [], "label": "getRecentCases", - "description": [], + "description": [ + "\nGet the recent cases component" + ], "signature": [ "(props: ", - "RecentCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + }, ") => React.ReactElement<", - "RecentCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 54 + "lineNumber": 84 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for showing recent cases" + ], "children": [ { "parentPluginId": "cases", @@ -706,13 +1319,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "RecentCasesProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 54 + "lineNumber": 84 }, "deprecated": false } @@ -6138,7 +6759,7 @@ "label": "Comment", "description": [], "signature": [ - "({ comment: string; type: ", + "({ type: ", { "pluginId": "cases", "scope": "common", @@ -6146,7 +6767,15 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } & { associationType: ", + ".alert | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CommentType", + "text": "CommentType" + }, + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { associationType: ", { "pluginId": "cases", "scope": "common", @@ -6170,15 +6799,7 @@ "section": "def-common.ElasticUser", "text": "ElasticUser" }, - " | null; version: string; }) | ({ type: ", - { - "pluginId": "cases", - "scope": "common", - "docId": "kibCasesPluginApi", - "section": "def-common.CommentType", - "text": "CommentType" - }, - ".alert | ", + " | null; version: string; }) | ({ comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -6186,7 +6807,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { associationType: ", + ".user; owner: string; } & { associationType: ", { "pluginId": "cases", "scope": "common", @@ -6333,7 +6954,7 @@ "label": "CommentPatchRequest", "description": [], "signature": [ - "({ comment: string; type: ", + "({ type: ", { "pluginId": "cases", "scope": "common", @@ -6341,7 +6962,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } & { id: string; version: string; }) | ({ type: ", + ".alert | ", { "pluginId": "cases", "scope": "common", @@ -6349,7 +6970,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".alert | ", + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { id: string; version: string; }) | ({ comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -6357,7 +6978,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { id: string; version: string; })" + ".user; owner: string; } & { id: string; version: string; })" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/comment.ts", @@ -6374,7 +6995,7 @@ "label": "CommentRequest", "description": [], "signature": [ - "{ comment: string; type: ", + "{ type: ", { "pluginId": "cases", "scope": "common", @@ -6382,7 +7003,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } | { type: ", + ".alert | ", { "pluginId": "cases", "scope": "common", @@ -6390,7 +7011,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".alert | ", + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } | { comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -6398,7 +7019,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; }" + ".user; owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/comment.ts", diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 0f9cbe5364b630..00714b3217fca8 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -32,9 +32,6 @@ import casesObj from './cases.json'; ### Interfaces -### Consts, variables and types - - ## Server ### Classes diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index c59800aaf9bcba..4a85a64c7e03a8 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -3,11 +3,15 @@ "id": "cases", "kibanaVersion": "kibana", "extraPublicDirs": ["common"], - "requiredPlugins": ["actions", "esUiShared", "features", "kibanaReact", "kibanaUtils", "triggersActionsUi"], - "optionalPlugins": [ - "spaces", - "security" + "requiredPlugins": [ + "actions", + "esUiShared", + "features", + "kibanaReact", + "kibanaUtils", + "triggersActionsUi" ], + "optionalPlugins": ["spaces", "security"], "server": true, "ui": true, "version": "8.0.0" diff --git a/x-pack/plugins/cases/public/index.tsx b/x-pack/plugins/cases/public/index.tsx index e8589152b7ca8a..a33bb14cd78c1a 100644 --- a/x-pack/plugins/cases/public/index.tsx +++ b/x-pack/plugins/cases/public/index.tsx @@ -12,6 +12,11 @@ export function plugin(initializerContext: PluginInitializerContext) { return new CasesUiPlugin(initializerContext); } -export { CasesUiPlugin }; -export * from './plugin'; -export * from './types'; +export type { CasesUiPlugin }; +export type { CasesUiStart } from './types'; +export type { AllCasesProps } from './components/all_cases'; +export type { AllCasesSelectorModalProps } from './components/all_cases/selector_modal'; +export type { CaseViewProps } from './components/case_view'; +export type { ConfigureCasesProps } from './components/configure_cases'; +export type { CreateCaseProps } from './components/create'; +export type { RecentCasesProps } from './components/recent_cases'; diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 2193832492aa22..2b31935c3ff970 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -44,12 +44,42 @@ export interface Owner { } export interface CasesUiStart { + /** + * Get the all cases table + * @param props AllCasesProps + * @returns A react component that displays all cases + */ getAllCases: (props: AllCasesProps) => ReactElement; + /** + * use Modal hook for all cases selector + * @param props UseAllCasesSelectorModalProps + * @returns A react component that is a modal for selecting a case + */ getAllCasesSelectorModal: ( props: AllCasesSelectorModalProps ) => ReactElement; + /** + * Get the case view component + * @param props CaseViewProps + * @returns A react component for viewing a specific case + */ getCaseView: (props: CaseViewProps) => ReactElement; + /** + * Get the configure case component + * @param props ConfigureCasesProps + * @returns A react component for configuring a specific case + */ getConfigureCases: (props: ConfigureCasesProps) => ReactElement; + /** + * Get the create case form + * @param props CreateCaseProps + * @returns A react component for creating a new case + */ getCreateCase: (props: CreateCaseProps) => ReactElement; + /** + * Get the recent cases component + * @param props RecentCasesProps + * @returns A react component for showing recent cases + */ getRecentCases: (props: RecentCasesProps) => ReactElement; } From e0f817c8547335e67204bbffc21bc4822c32cf46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Thu, 10 Jun 2021 14:45:38 +0200 Subject: [PATCH 08/99] Cleans comment state when submiting and when init the store (#101861) --- .../event_filters/store/middleware.test.ts | 37 +++++++++++++++++++ .../pages/event_filters/store/middleware.ts | 3 +- .../pages/event_filters/store/reducer.test.ts | 19 ++++++++++ .../pages/event_filters/store/reducer.ts | 1 + 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts index 5229e4078eb0dc..530a18cd8a3120 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts @@ -22,6 +22,11 @@ import { EventFiltersListPageState, EventFiltersService } from '../types'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { isFailedResourceState, isLoadedResourceState } from '../../../state'; import { getListFetchError } from './selector'; +import type { + ExceptionListItemSchema, + CreateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { Immutable } from '../../../../../common/endpoint/types'; const createEventFiltersServiceMock = (): jest.Mocked => ({ addEventFilters: jest.fn(), @@ -208,6 +213,38 @@ describe('Event filters middleware', () => { }); }); + it('does submit when entry has empty comments with white spaces', async () => { + service.addEventFilters.mockImplementation( + async (exception: Immutable) => { + expect(exception.comments).toStrictEqual(createdEventFilterEntryMock().comments); + return createdEventFilterEntryMock(); + } + ); + const entry = getInitialExceptionFromEvent(ecsEventMock()); + store.dispatch({ + type: 'eventFiltersInitForm', + payload: { entry }, + }); + + store.dispatch({ + type: 'eventFiltersChangeForm', + payload: { newComment: ' ', entry }, + }); + + store.dispatch({ type: 'eventFiltersCreateStart' }); + await spyMiddleware.waitForAction('eventFiltersFormStateChanged'); + expect(store.getState()).toStrictEqual({ + ...initialState, + form: { + ...store.getState().form, + submissionResourceState: { + type: 'LoadedResourceState', + data: createdEventFilterEntryMock(), + }, + }, + }); + }); + it('does throw error when creating', async () => { service.addEventFilters.mockRejectedValue({ body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' }, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index a115ee9961de42..c1ade4e2cadeca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -57,7 +57,8 @@ const addNewComments = ( ): UpdateExceptionListItemSchema | CreateExceptionListItemSchema => { if (newComment) { if (!entry.comments) entry.comments = []; - entry.comments.push({ comment: newComment }); + const trimmedComment = newComment.trim(); + if (trimmedComment) entry.comments.push({ comment: trimmedComment }); } return entry; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts index ed665135317af4..5366b6dcf155aa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts @@ -88,6 +88,25 @@ describe('event filters reducer', () => { }); }); + it('clean form after change form status', () => { + const entry = getInitialExceptionFromEvent(ecsEventMock()); + const nameChanged = 'name changed'; + const newComment = 'new comment'; + const result = eventFiltersPageReducer(initialState, { + type: 'eventFiltersChangeForm', + payload: { entry: { ...entry, name: nameChanged }, newComment }, + }); + const cleanState = eventFiltersPageReducer(result, { + type: 'eventFiltersInitForm', + payload: { entry }, + }); + + expect(cleanState).toStrictEqual({ + ...initialState, + form: { ...initialState.form, entry, hasNameError: true, newComment: '' }, + }); + }); + it('create is success and force list refresh', () => { const initialStateWithListPageActive = { ...initialState, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts index d69efb689c8779..28292bdb1ed1cd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts @@ -86,6 +86,7 @@ const eventFiltersInitForm: CaseReducer = (state, action) entry: action.payload.entry, hasNameError: !action.payload.entry.name, hasOSError: !action.payload.entry.os_types?.length, + newComment: '', submissionResourceState: { type: 'UninitialisedResourceState', }, From baef85575dbf8a1a6fcd91b191aeaf2d102569e6 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 10 Jun 2021 14:52:24 +0200 Subject: [PATCH 09/99] [Lens] Do not use dynamic coloring for array values (#101750) --- .../components/cell_value.test.tsx | 11 +++++++++++ .../components/cell_value.tsx | 7 ++++++- .../components/shared_utils.tsx | 18 +++++++++++++----- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx index 67255dc8a953ea..aa8c6ffb26d170 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx @@ -192,5 +192,16 @@ describe('datatable cell renderer', () => { style: expect.objectContaining({ color: 'blue' }), }); }); + + it('should not color the cell when the value is an array', async () => { + const columnConfig = getColumnConfiguration(); + columnConfig.columns[0].colorMode = 'cell'; + + const { setCellProps } = await renderCellComponent(columnConfig, { + table: { ...table, rows: [{ a: [10, 123] }] }, + }); + + expect(setCellProps).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx index a6c50f00cb77fd..14b8c8a36b431c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx @@ -13,6 +13,7 @@ import type { DataContextType } from './types'; import { ColumnConfig } from './table_basic'; import { getContrastColor } from '../../shared_components/coloring/utils'; import { getOriginalId } from '../transpose_helpers'; +import { getNumericValue } from './shared_utils'; export const createGridCell = ( formatters: Record>, @@ -37,7 +38,11 @@ export const createGridCell = ( if (minMaxByColumnId?.[originalId]) { if (colorMode !== 'none' && palette?.params && getColorForValue) { // workout the bucket the value belongs to - const color = getColorForValue(rowValue, palette.params, minMaxByColumnId[originalId]); + const color = getColorForValue( + getNumericValue(rowValue), + palette.params, + minMaxByColumnId[originalId] + ); if (color) { const style = { [colorMode === 'cell' ? 'backgroundColor' : 'color']: color }; if (colorMode === 'cell' && color) { diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx index 92a949e65c67ea..815da92d9fca97 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/shared_utils.tsx @@ -8,6 +8,13 @@ import { Datatable } from 'src/plugins/expressions'; import { getOriginalId } from '../transpose_helpers'; +export function getNumericValue(rowValue: number | number[] | undefined) { + if (rowValue == null || Array.isArray(rowValue)) { + return; + } + return rowValue; +} + export const findMinMaxByColumnId = (columnIds: string[], table: Datatable | undefined) => { const minMax: Record = {}; @@ -17,12 +24,13 @@ export const findMinMaxByColumnId = (columnIds: string[], table: Datatable | und minMax[originalId] = minMax[originalId] || { max: -Infinity, min: Infinity }; table.rows.forEach((row) => { const rowValue = row[columnId]; - if (rowValue != null) { - if (minMax[originalId].min > rowValue) { - minMax[originalId].min = rowValue; + const numericValue = getNumericValue(rowValue); + if (numericValue != null) { + if (minMax[originalId].min > numericValue) { + minMax[originalId].min = numericValue; } - if (minMax[originalId].max < rowValue) { - minMax[originalId].max = rowValue; + if (minMax[originalId].max < numericValue) { + minMax[originalId].max = numericValue; } } }); From 49ea272240cefe6689d675ebac5ca303a4650d67 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 10 Jun 2021 14:53:25 +0200 Subject: [PATCH 10/99] [Lens] Memoize operationMatrix computation (#101745) --- .../dimension_panel/operation_support.ts | 38 ++++++++++++------- .../operations/operations.ts | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 504aa0912f9cc4..7a7297e77bcf28 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { DatasourceDimensionDropProps } from '../../types'; +import memoizeOne from 'memoize-one'; +import { DatasourceDimensionDropProps, OperationMetadata } from '../../types'; import { OperationType } from '../indexpattern'; -import { memoizedGetAvailableOperationsByMetadata } from '../operations'; +import { memoizedGetAvailableOperationsByMetadata, OperationFieldTuple } from '../operations'; import { IndexPatternPrivateState } from '../types'; export interface OperationSupportMatrix { @@ -21,17 +22,16 @@ type Props = Pick< 'layerId' | 'columnId' | 'state' | 'filterOperations' >; -// TODO: the support matrix should be available outside of the dimension panel - -// TODO: This code has historically been memoized, as a potentially performance -// sensitive task. If we can add memoization without breaking the behavior, we should. -export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { - const layerId = props.layerId; - const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - - const filteredOperationsByMetadata = memoizedGetAvailableOperationsByMetadata( - currentIndexPattern - ).filter((operation) => props.filterOperations(operation.operationMetaData)); +function computeOperationMatrix( + operationsByMetadata: Array<{ + operationMetaData: OperationMetadata; + operations: OperationFieldTuple[]; + }>, + filterOperations: (operation: OperationMetadata) => boolean +) { + const filteredOperationsByMetadata = operationsByMetadata.filter((operation) => + filterOperations(operation.operationMetaData) + ); const supportedOperationsByField: Partial>> = {}; const supportedOperationsWithoutField: Set = new Set(); @@ -59,4 +59,16 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix operationWithoutField: supportedOperationsWithoutField, fieldByOperation: supportedFieldsByOperation, }; +} + +// memoize based on latest execution. It supports multiple args +const memoizedComputeOperationsMatrix = memoizeOne(computeOperationMatrix); + +// TODO: the support matrix should be available outside of the dimension panel +export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { + const layerId = props.layerId; + const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; + + const operationsByMetadata = memoizedGetAvailableOperationsByMetadata(currentIndexPattern); + return memoizedComputeOperationsMatrix(operationsByMetadata, props.filterOperations); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 437d2af005961f..fc70be257c8ad9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -93,7 +93,7 @@ export function isDocumentOperation(type: string) { return documentOperations.has(type); } -type OperationFieldTuple = +export type OperationFieldTuple = | { type: 'field'; operationType: OperationType; From bde81c2090ddc1c8b52f35746e7744ec9bb48d8b Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 10 Jun 2021 15:17:52 +0200 Subject: [PATCH 11/99] [Expressions] Create expressions function to get UI settings (#101317) --- .../canvas/canvas-function-reference.asciidoc | 27 ++++++ ...s-expressions-public.expressionsservice.md | 4 +- ...essions-public.expressionsservice.setup.md | 9 +- ...essions-public.expressionsservice.start.md | 9 +- .../common/executor/executor.test.ts | 26 +++-- .../expressions/common/executor/executor.ts | 3 +- .../expression_functions/specs/index.ts | 26 +---- .../specs/tests/ui_setting.test.ts | 78 +++++++++++++++ .../expression_functions/specs/ui_setting.ts | 94 +++++++++++++++++++ .../common/expression_types/specs/index.ts | 3 + .../specs/tests/ui_setting.test.ts | 79 ++++++++++++++++ .../expression_types/specs/ui_setting.ts | 65 +++++++++++++ .../common/service/expressions_services.ts | 33 ++++++- .../expression_functions/index.ts | 24 ++++- .../public/expression_functions/index.ts | 9 ++ .../public/expression_functions/ui_setting.ts | 20 ++++ src/plugins/expressions/public/plugin.test.ts | 2 +- src/plugins/expressions/public/plugin.ts | 12 ++- src/plugins/expressions/public/public.api.md | 4 +- .../public/services/expressions_services.ts | 19 ++++ .../public/{services.ts => services/index.ts} | 6 +- .../server/expression_functions/index.ts | 9 ++ .../server/expression_functions/ui_setting.ts | 22 +++++ src/plugins/expressions/server/plugin.ts | 3 +- .../server/services/expressions_services.ts | 19 ++++ .../expressions/server/services/index.ts | 9 ++ 26 files changed, 560 insertions(+), 54 deletions(-) create mode 100644 src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts create mode 100644 src/plugins/expressions/common/expression_functions/specs/ui_setting.ts create mode 100644 src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts create mode 100644 src/plugins/expressions/common/expression_types/specs/ui_setting.ts create mode 100644 src/plugins/expressions/public/expression_functions/index.ts create mode 100644 src/plugins/expressions/public/expression_functions/ui_setting.ts create mode 100644 src/plugins/expressions/public/services/expressions_services.ts rename src/plugins/expressions/public/{services.ts => services/index.ts} (87%) create mode 100644 src/plugins/expressions/server/expression_functions/index.ts create mode 100644 src/plugins/expressions/server/expression_functions/ui_setting.ts create mode 100644 src/plugins/expressions/server/services/expressions_services.ts create mode 100644 src/plugins/expressions/server/services/index.ts diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 67210c9d77057e..272cd524c2c200 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -2893,6 +2893,33 @@ Alias: `type` [[u_fns]] == U +[float] +[[uiSetting_fn]] +=== `uiSetting` + +Returns a UI settings parameter value. + +*Accepts:* `null` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|_Unnamed_ *** + +Aliases: `parameter` +|`string` +|The parameter name. + +|`default` +|`any` +|A default value in case of the parameter is not set. + +Default: `null` +|=== + +*Returns:* `ui_setting` + [float] [[urlparam_fn]] === `urlparam` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index 6ba0f0feb82b3a..9afd603bc48694 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -70,7 +70,7 @@ The actual function is defined in the fn key. The function can be \ | Method | Modifiers | Description | | --- | --- | --- | -| [setup()](./kibana-plugin-plugins-expressions-public.expressionsservice.setup.md) | | Returns Kibana Platform \*setup\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | -| [start()](./kibana-plugin-plugins-expressions-public.expressionsservice.start.md) | | Returns Kibana Platform \*start\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | +| [setup(args)](./kibana-plugin-plugins-expressions-public.expressionsservice.setup.md) | | Returns Kibana Platform \*setup\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | +| [start(args)](./kibana-plugin-plugins-expressions-public.expressionsservice.start.md) | | Returns Kibana Platform \*start\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | | [stop()](./kibana-plugin-plugins-expressions-public.expressionsservice.stop.md) | | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md index a51f3f073d5181..991f1f717d5f56 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md @@ -9,8 +9,15 @@ Returns Kibana Platform \*setup\* life-cycle contract. Useful to return the same Signature: ```typescript -setup(): ExpressionsServiceSetup; +setup(...args: unknown[]): ExpressionsServiceSetup; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| args | unknown[] | | + Returns: `ExpressionsServiceSetup` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md index 766d703a0729da..34d33cacabebb6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md @@ -9,8 +9,15 @@ Returns Kibana Platform \*start\* life-cycle contract. Useful to return the same Signature: ```typescript -start(): ExpressionsServiceStart; +start(...args: unknown[]): ExpressionsServiceStart; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| args | unknown[] | | + Returns: `ExpressionsServiceStart` diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 6175c9e170a906..3c24a3c24e01bc 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -52,12 +52,6 @@ describe('Executor', () => { executor.registerFunction(expressionFunctions.clog); }); - test('can register all functions', () => { - const executor = new Executor(); - for (const functionDefinition of expressionFunctions.functionSpecs) - executor.registerFunction(functionDefinition); - }); - test('can retrieve all functions', () => { const executor = new Executor(); executor.registerFunction(expressionFunctions.clog); @@ -67,12 +61,24 @@ describe('Executor', () => { test('can retrieve all functions - 2', () => { const executor = new Executor(); - for (const functionDefinition of expressionFunctions.functionSpecs) + const functionSpecs = [ + expressionFunctions.clog, + expressionFunctions.font, + expressionFunctions.variableSet, + expressionFunctions.variable, + expressionFunctions.theme, + expressionFunctions.cumulativeSum, + expressionFunctions.derivative, + expressionFunctions.movingAverage, + expressionFunctions.mapColumn, + expressionFunctions.math, + ]; + for (const functionDefinition of functionSpecs) { executor.registerFunction(functionDefinition); + } const functions = executor.getFunctions(); - expect(Object.keys(functions).sort()).toEqual( - expressionFunctions.functionSpecs.map((spec) => spec.name).sort() - ); + + expect(Object.keys(functions).sort()).toEqual(functionSpecs.map((spec) => spec.name).sort()); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 1eea51a0e1ec45..a307172aff9737 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -19,7 +19,6 @@ import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; import { ExpressionAstExpression, ExpressionAstFunction } from '../ast'; import { ExpressionValueError, typeSpecs } from '../expression_types/specs'; -import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; @@ -85,7 +84,7 @@ export class Executor = Record { const executor = new Executor(state); for (const type of typeSpecs) executor.registerType(type); - for (const func of functionSpecs) executor.registerFunction(func); + return executor; } diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index 9408b3a4337120..20a6f9aac45674 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -6,31 +6,6 @@ * Side Public License, v 1. */ -import { clog } from './clog'; -import { font } from './font'; -import { variableSet } from './var_set'; -import { variable } from './var'; -import { AnyExpressionFunctionDefinition } from '../types'; -import { theme } from './theme'; -import { cumulativeSum } from './cumulative_sum'; -import { derivative } from './derivative'; -import { movingAverage } from './moving_average'; -import { mapColumn } from './map_column'; -import { math } from './math'; - -export const functionSpecs: AnyExpressionFunctionDefinition[] = [ - clog, - font, - variableSet, - variable, - theme, - cumulativeSum, - derivative, - movingAverage, - mapColumn, - math, -]; - export * from './clog'; export * from './font'; export * from './var_set'; @@ -39,5 +14,6 @@ export * from './theme'; export * from './cumulative_sum'; export * from './derivative'; export * from './moving_average'; +export * from './ui_setting'; export { mapColumn, MapColumnArguments } from './map_column'; export { math, MathArguments, MathInput } from './math'; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts new file mode 100644 index 00000000000000..fb2c87588a4d47 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../../../../common'); + +import { IUiSettingsClient } from 'src/core/public'; +import { getUiSettingFn } from '../ui_setting'; + +describe('uiSetting', () => { + describe('fn', () => { + let getStartDependencies: jest.MockedFunction< + Parameters[0]['getStartDependencies'] + >; + let uiSetting: ReturnType; + let uiSettings: jest.Mocked; + + beforeEach(() => { + uiSettings = ({ + get: jest.fn(), + } as unknown) as jest.Mocked; + getStartDependencies = (jest.fn(async () => ({ + uiSettings, + })) as unknown) as typeof getStartDependencies; + + uiSetting = getUiSettingFn({ getStartDependencies }); + }); + + it('should return a value', () => { + uiSettings.get.mockReturnValueOnce('value'); + + expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).resolves.toEqual({ + type: 'ui_setting', + key: 'something', + value: 'value', + }); + }); + + it('should pass a default value', async () => { + await uiSetting.fn(null, { parameter: 'something', default: 'default' }, {} as any); + + expect(uiSettings.get).toHaveBeenCalledWith('something', 'default'); + }); + + it('should throw an error when parameter does not exist', () => { + uiSettings.get.mockImplementationOnce(() => { + throw new Error(); + }); + + expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).rejects.toEqual( + new Error('Invalid parameter "something".') + ); + }); + + it('should get a request instance on the server-side', async () => { + const request = {}; + await uiSetting.fn(null, { parameter: 'something' }, { + getKibanaRequest: () => request, + } as any); + + const [[getKibanaRequest]] = getStartDependencies.mock.calls; + + expect(getKibanaRequest()).toBe(request); + }); + + it('should throw an error if request is not provided on the server-side', async () => { + await uiSetting.fn(null, { parameter: 'something' }, {} as any); + + const [[getKibanaRequest]] = getStartDependencies.mock.calls; + + expect(getKibanaRequest).toThrow(); + }); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/specs/ui_setting.ts b/src/plugins/expressions/common/expression_functions/specs/ui_setting.ts new file mode 100644 index 00000000000000..8e352e12d49c59 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/ui_setting.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { KibanaRequest } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { UiSetting } from '../../expression_types/specs/ui_setting'; + +interface UiSettingsClient { + get(key: string, defaultValue?: T): T | Promise; +} + +interface UiSettingStartDependencies { + uiSettings: UiSettingsClient; +} + +interface UiSettingFnArguments { + getStartDependencies(getKibanaRequest: () => KibanaRequest): Promise; +} + +export interface UiSettingArguments { + default?: unknown; + parameter: string; +} + +export type ExpressionFunctionUiSetting = ExpressionFunctionDefinition< + 'uiSetting', + unknown, + UiSettingArguments, + Promise +>; + +export function getUiSettingFn({ + getStartDependencies, +}: UiSettingFnArguments): ExpressionFunctionUiSetting { + return { + name: 'uiSetting', + help: i18n.translate('expressions.functions.uiSetting.help', { + defaultMessage: 'Returns a UI settings parameter value.', + }), + args: { + default: { + help: i18n.translate('expressions.functions.uiSetting.args.default', { + defaultMessage: 'A default value in case of the parameter is not set.', + }), + }, + parameter: { + aliases: ['_'], + help: i18n.translate('expressions.functions.uiSetting.args.parameter', { + defaultMessage: 'The parameter name.', + }), + required: true, + types: ['string'], + }, + }, + async fn(input, { default: defaultValue, parameter }, { getKibanaRequest }) { + const { uiSettings } = await getStartDependencies(() => { + const request = getKibanaRequest?.(); + if (!request) { + throw new Error( + i18n.translate('expressions.functions.uiSetting.error.kibanaRequest', { + defaultMessage: + 'A KibanaRequest is required to get UI settings on the server. ' + + 'Please provide a request object to the expression execution params.', + }) + ); + } + + return request; + }); + + try { + return { + type: 'ui_setting', + key: parameter, + value: await uiSettings.get(parameter, defaultValue), + }; + } catch { + throw new Error( + i18n.translate('expressions.functions.uiSetting.error.parameter', { + defaultMessage: 'Invalid parameter "{parameter}".', + values: { parameter }, + }) + ); + } + }, + }; +} diff --git a/src/plugins/expressions/common/expression_types/specs/index.ts b/src/plugins/expressions/common/expression_types/specs/index.ts index 70427f8b337d89..c990d74672fcc4 100644 --- a/src/plugins/expressions/common/expression_types/specs/index.ts +++ b/src/plugins/expressions/common/expression_types/specs/index.ts @@ -21,6 +21,7 @@ import { shape } from './shape'; import { string } from './string'; import { style } from './style'; import { AnyExpressionTypeDefinition } from '../types'; +import { uiSetting } from './ui_setting'; export const typeSpecs: AnyExpressionTypeDefinition[] = [ boolean, @@ -37,6 +38,7 @@ export const typeSpecs: AnyExpressionTypeDefinition[] = [ shape, string, style, + uiSetting, ]; export * from './boolean'; @@ -53,3 +55,4 @@ export * from './render'; export * from './shape'; export * from './string'; export * from './style'; +export * from './ui_setting'; diff --git a/src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts b/src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts new file mode 100644 index 00000000000000..a0a3d0d396b6e6 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { UiSetting, uiSetting } from '../ui_setting'; + +function createUiSetting(value: unknown, key = 'something'): UiSetting { + return { + key, + value, + type: 'ui_setting', + }; +} + +describe('uiSetting', () => { + describe('to', () => { + describe('render', () => { + it.each` + value | expected + ${{ a: 'b' }} | ${JSON.stringify({ a: 'b' })} + ${null} | ${''} + ${'something'} | ${'something'} + `('should render "$value" as "$expected"', ({ expected, value }) => { + expect(uiSetting.to?.render(createUiSetting(value), {})).toHaveProperty( + 'value.text', + expected + ); + }); + }); + + describe('datatable', () => { + it('should use parameter name as a datatable column', () => { + expect(uiSetting.to?.datatable(createUiSetting('value', 'column'), {})).toHaveProperty( + 'columns.0', + expect.objectContaining({ id: 'column', name: 'column' }) + ); + }); + + it.each` + value | type + ${null} | ${'null'} + ${undefined} | ${'null'} + ${'something'} | ${'string'} + ${['123']} | ${'string'} + ${123} | ${'number'} + ${[123]} | ${'number'} + ${true} | ${'boolean'} + ${{ a: 'b' }} | ${'object'} + ${[]} | ${'unknown'} + `('should determine $type type', ({ value, type }) => { + expect(uiSetting.to?.datatable(createUiSetting(value, 'column'), {})).toHaveProperty( + 'columns.0.meta.type', + type + ); + }); + + it('should put a value into a row', () => { + expect(uiSetting.to?.datatable(createUiSetting('value'), {})).toHaveProperty( + 'rows.0.something', + 'value' + ); + }); + + it('should put an array value into multiple rows', () => { + expect(uiSetting.to?.datatable(createUiSetting(['a', 'b']), {})).toHaveProperty( + 'rows', + expect.arrayContaining([ + expect.objectContaining({ something: 'a' }), + expect.objectContaining({ something: 'b' }), + ]) + ); + }); + }); + }); +}); diff --git a/src/plugins/expressions/common/expression_types/specs/ui_setting.ts b/src/plugins/expressions/common/expression_types/specs/ui_setting.ts new file mode 100644 index 00000000000000..aaea92e89a8325 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/specs/ui_setting.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Datatable, DatatableColumnType } from './datatable'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; +import { ExpressionValueRender } from './render'; + +const name = 'ui_setting'; + +function getType(value: unknown): DatatableColumnType { + if (value == null) { + return 'null'; + } + + if (Array.isArray(value)) { + return value.length ? getType(value[0]) : 'unknown'; + } + + if (['boolean', 'number', 'object', 'string'].includes(typeof value)) { + return typeof value as DatatableColumnType; + } + + return 'unknown'; +} + +export type UiSetting = ExpressionValueBoxed<'ui_setting', { key: string; value: unknown }>; + +export const uiSetting: ExpressionTypeDefinition<'ui_setting', UiSetting> = { + name, + to: { + boolean({ value }) { + return Boolean(value); + }, + number({ value }) { + return Number(value); + }, + string({ value }) { + return String(value ?? ''); + }, + render({ value }): ExpressionValueRender<{ text: string }> { + return { + type: 'render', + as: 'text', + value: { + text: + typeof value === 'object' && value !== null + ? JSON.stringify(value) + : String(value ?? ''), + }, + }; + }, + datatable({ key, value }): Datatable { + return { + type: 'datatable', + columns: [{ id: key, name: key, meta: { type: getType(value) } }], + rows: (Array.isArray(value) ? value : [value]).map((cell) => ({ [key]: cell })), + }; + }, + }, +}; diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index d57c1748954abf..a8839c9b0d71e1 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -19,6 +19,18 @@ import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; +import { + clog, + font, + variableSet, + variable, + theme, + cumulativeSum, + derivative, + movingAverage, + mapColumn, + math, +} from '../expression_functions'; /** * The public contract that `ExpressionsService` provides to other plugins @@ -269,7 +281,7 @@ export class ExpressionsService implements PersistableStateService { const executor = this.executor.fork(); const renderers = this.renderers; - const fork = new ExpressionsService({ executor, renderers }); + const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); return fork; }; @@ -318,7 +330,22 @@ export class ExpressionsService implements PersistableStateService) { + return getCommonUiSettingFn({ + async getStartDependencies() { + const [{ uiSettings }] = await getStartServices(); + + return { uiSettings }; + }, + }); +} diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index b83c92f5d1a96b..93353ebbb51b61 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -8,7 +8,7 @@ import { expressionsPluginMock } from './mocks'; import { add } from '../common/test_helpers/expression_functions/add'; -import { ExpressionsService } from '../common'; +import { ExpressionsService } from './services'; describe('ExpressionsPublicPlugin', () => { test('can instantiate from mocks', async () => { diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 2410ad87413129..37d519ac451336 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -6,9 +6,15 @@ * Side Public License, v 1. */ +import { pick } from 'lodash'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { ExpressionsService, ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; -import { setRenderersRegistry, setNotifications, setExpressionsService } from './services'; +import { ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; +import { + ExpressionsService, + setRenderersRegistry, + setNotifications, + setExpressionsService, +} from './services'; import { ReactExpressionRenderer } from './react_expression_renderer'; import { ExpressionLoader, IExpressionLoader, loader } from './loader'; import { render, ExpressionRenderHandler } from './render'; @@ -51,7 +57,7 @@ export class ExpressionsPublicPlugin implements Plugin) => Record; diff --git a/src/plugins/expressions/public/services/expressions_services.ts b/src/plugins/expressions/public/services/expressions_services.ts new file mode 100644 index 00000000000000..388af81629c31d --- /dev/null +++ b/src/plugins/expressions/public/services/expressions_services.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup } from 'src/core/public'; +import { ExpressionsService as CommonExpressionsService } from '../../common'; +import { getUiSettingFn } from '../expression_functions'; + +export class ExpressionsService extends CommonExpressionsService { + setup({ getStartServices }: Pick) { + this.registerFunction(getUiSettingFn({ getStartServices })); + + return super.setup(); + } +} diff --git a/src/plugins/expressions/public/services.ts b/src/plugins/expressions/public/services/index.ts similarity index 87% rename from src/plugins/expressions/public/services.ts rename to src/plugins/expressions/public/services/index.ts index a700e54d77e190..db473037a0a4a6 100644 --- a/src/plugins/expressions/public/services.ts +++ b/src/plugins/expressions/public/services/index.ts @@ -7,8 +7,8 @@ */ import { NotificationsStart } from 'kibana/public'; -import { createGetterSetter } from '../../kibana_utils/public'; -import { ExpressionsService, ExpressionRendererRegistry } from '../common'; +import { createGetterSetter } from '../../../kibana_utils/public'; +import { ExpressionsService, ExpressionRendererRegistry } from '../../common'; export const [getNotifications, setNotifications] = createGetterSetter( 'Notifications' @@ -23,3 +23,5 @@ export const [ getExpressionsService, setExpressionsService, ] = createGetterSetter('ExpressionsService'); + +export * from './expressions_services'; diff --git a/src/plugins/expressions/server/expression_functions/index.ts b/src/plugins/expressions/server/expression_functions/index.ts new file mode 100644 index 00000000000000..eb36291c613c67 --- /dev/null +++ b/src/plugins/expressions/server/expression_functions/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './ui_setting'; diff --git a/src/plugins/expressions/server/expression_functions/ui_setting.ts b/src/plugins/expressions/server/expression_functions/ui_setting.ts new file mode 100644 index 00000000000000..0d3d5b1c1f997b --- /dev/null +++ b/src/plugins/expressions/server/expression_functions/ui_setting.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup } from 'src/core/server'; +import { getUiSettingFn as getCommonUiSettingFn } from '../../common'; + +export function getUiSettingFn({ getStartServices }: Pick) { + return getCommonUiSettingFn({ + async getStartDependencies(getKibanaRequest) { + const [{ savedObjects, uiSettings }] = await getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(getKibanaRequest()); + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + + return { uiSettings: uiSettingsClient }; + }, + }); +} diff --git a/src/plugins/expressions/server/plugin.ts b/src/plugins/expressions/server/plugin.ts index 2c638d496ece62..2e45daf6e0f8c0 100644 --- a/src/plugins/expressions/server/plugin.ts +++ b/src/plugins/expressions/server/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { pick } from 'lodash'; import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; import { ExpressionsService, ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; @@ -24,7 +25,7 @@ export class ExpressionsServerPlugin environment: 'server', }); - const setup = this.expressions.setup(); + const setup = this.expressions.setup(pick(core, 'getStartServices')); return Object.freeze(setup); } diff --git a/src/plugins/expressions/server/services/expressions_services.ts b/src/plugins/expressions/server/services/expressions_services.ts new file mode 100644 index 00000000000000..914745436f1518 --- /dev/null +++ b/src/plugins/expressions/server/services/expressions_services.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup } from 'src/core/server'; +import { ExpressionsService as CommonExpressionsService } from '../../common'; +import { getUiSettingFn } from '../expression_functions'; + +export class ExpressionsService extends CommonExpressionsService { + setup({ getStartServices }: Pick) { + this.registerFunction(getUiSettingFn({ getStartServices })); + + return super.setup(); + } +} diff --git a/src/plugins/expressions/server/services/index.ts b/src/plugins/expressions/server/services/index.ts new file mode 100644 index 00000000000000..c239c4efdb02cc --- /dev/null +++ b/src/plugins/expressions/server/services/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './expressions_services'; From 1fe7840ad92f5af7b1f2c1df25717ce88c2ed69c Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 10 Jun 2021 06:44:44 -0700 Subject: [PATCH 12/99] [so-migrations] Integration test fixes: Use default distribution for cleanup fixture creation (#101698) --- .../archives/7.13.0_with_corrupted_so.zip | Bin 49885 -> 43069 bytes .../integration_tests/cleanup.test.ts | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip index c6c89ac2879b2e674c1d613e6de0271632db9ebc..44f2fc9ba19eb54fc22c883437beed8f00a8c4c4 100644 GIT binary patch literal 43069 zcmd43WpEtJvMwrSW|l_Gj21JqWHB??VrFJ$W@ct)28)?xvBfMsd#`g3?042a_uc#F zbwp0|L`-MZm-%&7Wld!&NP~jG0R6e{;zpJJar18<;6Qjl#s-cC45}*7K%m?hz53i( zz4}hBut30I_rO3vU}(R8sPMOs!~U zQVrHWsX_Z^HF^$?2973wY18|yt>(Y))E|UUKySYdWBUvpoc8-0#&6&_f1^#$j^4=B zh7s5UxA%? z0t45cCg7Rq?aNO|R_X@=c1H@I3gjm+3=Wd)^wX6z1yLUS?!ggau?L2q znh3I8X(5I#RuSODd(9)~@&(JI=K2r@KhDF-iO+#UKFWkmdK7jbP~kteD)k%#jv8ag zwGMXZC?@#JjemjtW4iVx$C}iC1B?52V42we9qk_r{*nJHwDD#;`g79KR0h64yih}# z5NMMVLH5N0=3?}*Ql&%_Jf+cYM13$&8H*6qKYIxg?fn1U6x|TiS7FpS86ZjgFb&Y6 zd}RFC#M90M<=co;o}~pVCV4kxkgViAzV+A$>+jEq|KRL@fF0P8Qic8PRfB)!+23Ft zEdJrye}(n@FV7laWTt0lq^s?vr6kY%zq}d`8D=NIgi68;8H$bFmU?RXz`iA69FB7^rE3>{5$pCpy z+L{}b9h8mi;a#U|sAvzN6%?e=nHC16Zl-AQn%blC|-sbZURF)bu=l9^~bJ<%f z{j&40DI|pLBn`g<*W{(e)y4Vc1qHRGg(0(ebmpb$+G|qdhSE<5+DBj?<^dZMXvga% zrtc-Er6(wzC+j6t{W&-{SNy*Z=@ea@X zfM$)}gMsjtEz}mD-b5im*qNk({QYgQ(x@gV~=@ ziU?=9y+v>=E;%Z~AWH^Xg`_Y(Dk}UsNFoyM^Y-TTmxpFseauk*Q?LIFZF!3LyD-1W zulrlD|EAZ!hqiy?|DRD!=D+VP{r?vKd0mjU2+6uz* z!t#4<{!W{!@xGUvY;Z@PUp{e#!qgjs9abNJr{|Nl~r1B6%Ao)3w*QH0Ob{C zk&6x9iyzzo(G_MPF(LsKHd0oWCSfsMdJ^i|lVK-H%S?vJ3;#|3Jfr`8`b*rq@gn~x zg$mG=WK>VkE{vkm_`w)sCBoL6z@3p|coZlS& z)%@1<)k_6c4Z|k^hz__8gClWB3AQnom|8pQU>#NjR|R1q3MXpfO5Bgj0$Fp<6he7uZyHp#RvTG zePByQChn5}5`@L@r=idw!DMSNg&#@akuj@`tzPs4Os>k!iJ7a#vszT|QBD(bdX@t! z0x{y5X=D@ALe5JO5XOeyL0B5_>LCL8^>WLkfTJ(o%ABjUuptBZq_WF=S%V(j{Tfk0 ztjM#03@&gO^+Z$y@)T&sNF38~r$RNp=6f-Hjl)*9_R{EK{+bs-#hzcnE{0u!b++KA z-Qw4wI2yQ2Q4{bL!#=i2(BGhp83wxOwi0MNeMc1p>tVm5(l%}x$AGA+v$y{%I@S%Q zYp|~~jU+BKhUOI77fl?35=(a&&SD&OiJMTX{!pkN6|C3L*R9k5#j3Co4y8uWPz)J3 zD}Y;7aZrAV|J57PkD^#ItRINxnx4{F<~Ox9s%(M&x-`;EhO9qSIiopbArYXB3L|+(RJb~cwV_K*wWf~| zsUZ0a%^vvL(4V#t-WFh=Lkb7ypZoj;k;z{G8x~77j;;m85(Cpz6P7Kg`01_l+<)N+ zDi=M$8rpWS9a~^5vrwvcKgVZF^-T85_q*zIC!JsdYaRQdGDyVE^>}*&+{3k$9Vw_D zjN1p@0Y&kFJXG21YCk$ZzWMR(E`lm}4cBs?KAVM(pdQsJMIW7y_YG1RZ;OBdZAei->3K94&gs(wyS!7Dcqi zWcLfBCnPD;!7qc^ah>ioJN=om@>Gb)A72bN=i@1+C+Lus(dZ@`><@I9mJznox_f@6 z)eO_;jJ-YK@KO}I3)(3-g%s?*Z_Wj$o|n>Hjd{H%m^I2uV^r&D29+?#^Af>(?{;5nrDi=m(9*Cyv^GFGa41;6aNb;!b6;>6eu+KZ5cb}n z#h$XE!P}x}$Np+7U8(1L((x0-=}5(-qySL)5j6c!96!^n;qBr5?ev4ch*dlU?J{H4 zS~rvGm4{&h*}CItd-}-b$~sYRt*ya^DEZ;DH1CE#jtQ4L#HCZW6)64$^F#`a&85t&UKy68+kNr&wV72jnQ4^_cl~bEul5>9 zOC{6cQx!~(+p8eVHyX#8`Im#?`lX}jOdiwMu=1@$7Nh83Q1KoZPqV2O+;GiHx^(+X zv8x}1_z|C`X}9~sy%PZ#?S@8TEB9?1)bzd=aHDgySIoD!0O&Gjz2kGbmsewj#A8S?Sy-CptYmQ-$NURa>C zSB1b8C$dk(1k|+JPwxmFW1-7MxjSvqjj<$?MtOI?v{L>z*(%Ki#59rZ40XgU6U%6R z65wp4F_G>r^(W4sojAXGaXw4uylmeL2;cK-HBUsYw(LAl7~C4YJB~${Q6-y)q;Go5h2JiP<-k?~V?7Oa}i`d@`;0S6rer`FlY*v~}`2Q6An^8!4~Q zdw32YBOee!Kp;neVAMBWf{33K04{zKQb~fGv42V=|GlQvO#KW2iH-g=2eO7qpT5;^ zNZW^ppQAk{S{@bFW+a)KB|R8pS3Y>bv$ULk5gpTIgz|DPAA7 z?`jm{jseu8vi*|ZE||23I_uM%KN=``PZT9UkjJN3E!GjtgMwyu=rARa*6F6 zR<+q{GIsJ$sFo4N~DNh0b^wL3#rew4x&Dib+gx+%u%201 zwurT2)NY%-Vjwh^_BQKI~F;GsC+c-GKBt zFcqsW`k3^3*IAD?__2)gFlnU5aFLFE$@g)_O~IX>&~ZXt zf3~<{aIW$i#5=b$v5aw1*+bsqgV3-N&kYa2S!wq<0j|lL$Qk&@yQg>MHZ7+pP08O!02WNXFXEEk?kg<)FfkOJRUP3>|QJ;(U_S&6izzKs}N4) zYkH2lFFl}$FPi-GEV)o5?0MSMW$!Rr-=U^rs4LD197_K%sJ^%b1a_=-bU=LtEPnT` zGafJYg%rD1bbxsP zAXn983s=`nJN}i%QUElI6xkQumFq0iRF5#YAcDbyd>D+D2z)nHbG>V-rUw#>O`!oogn?=kaT8v>|7rCuvPD*Y;-VE(h|)FsaR% zhXcZA$p4rD{_LKdA-tKc&;kMR@%*dd?a%JXU&Gtq?4AU9KzgDrH$AqoG{^?2YDhZL z2{}rq>!<)>E+FYZ0RVpZVhtS)6@ney6VsKkjM2bO0DuU`ceG_vXPFJ9Ow9O|nXvuA zx`Y)5IlGA4BCl;wBcqjTkLT$u9bmc4$GcZ(=&8vpt?B0!hbwv?n$%-;%v&jgvd9`m zA!IU)hsE%8s3#L_O+vlqiTVNuB~!5rc_00tLsyrtdzbdza>@SPiv~UUdsUPCn-jL0 zE+Qboz1s zCV-BQ2|rLtd!D}Ki+OzEUDY2VmkpERE0AoGQ)xPQTJbq$d3ZQDJ5an4A7Q!?TdPq< zEb&aoY3WM4ky(4E>XC6Hy_ZV6SUEFZ^+!|oWF)mPM81&d>`@LcuScOLeShuhOD&gF z1Yjh1O;rdxMOb}Rbz~RQEQ~F2*nIij9F12`HMLvL{ksanYau;=*if%vWj1-eccb~&sV}v~Yj4r8$HZ>`jjLYA z`K{zG4joRyaDQDaq?y1qtYrt*eOXfVfT4_XEjKdLIcmNVDW!*KQ#ynF$%}wQq{vSk zy|70pz9HNzbOMT;gW-y=Drg-Tm>u`tdM@g4V_TX%SSm$%5-MR5in7B7=RS~plIX^S z)0hyNIw1>Ps2r!^xH4VD_8%_LblqHk);BWL?wHVKa+ zRrUGH5jDTQ|I#;08<$W~+BTrZrPjeEi|6K(Yu!9ROzN!W&<(Q*=fJd%w(-Dys#8Dk zN(E^9Lk?eildVua7yGS+AR2`?#tL~=z<+%(7?uf+^wUGO$JDCh((MXtyaPM56^HN~ z-$gOp7}s>^Z2J05EkAwh3+hezJke~2@b@_j&rWP~^hTa(|Uc{;GF zD7q*tM?8G@I80e|YRQwdu8G+F|mGPbR4lRe@ zy^BKmSyzkj1VX1`7NnZGPRw4->riQn&Jt-2Mg#)$QqankqQoXF!NexL>6DGWttEYrYhjAd*Wb=R!>1Pyjc&nT74{)g@}u6`Dz1%>`lbF0l_4E6(;b9ObqtS`x| z)J=Yu{wCCtc-TE#JaIrUpjg!8${kNUhXYB~5c&L^&xSOZ|B0XFjjzTB_^lo>;Oke< zr61wmacbUiV3ga^s*khGO&7lNMO0Xh%Er>Hx zg}O~(IiHsxd+Wf}QbWeGQ>nSsU-2Zue!9m*8r!mXD1d$hL2;+3oJW)s?&UX<=zlrzWR@@Ilk_ z!G?aa!@7m@aO1&7WaZ&#oo)2yFx9mlFmsqH4b${<1Ld z$0*n|ndQV9dc!hZSr6-(!BtTy!z1WBGJH~3*-8S13hlDmmDNr(um%CesN*7(=?R|i zk_v%~_G7{gaeFo^rmd8Mlwj$>x6y%^^dc>}0+Y;uE(wmM?W{9bf#Da?=H(u0vX+8m z$+|RvH985&quyx?TEllxPb(!TVz#Wt&ipsUsYj>Aocr>o}^QW zBtUdKb=;t*@5$y7w^@8kLtQR{-g1FJUwYZ5GawO~5VD0T)soZ~ zXo5sgPcAqFgu{=7q4~@G$^!Wr(Pdf*1^}sK?ly)}>?3Nul|tT?kT(xCkUwr`H!6tp zWc|%TkRLknSGv{gdyn{hRNz(x2a5&;)`iZEYDYVx&~C(M_B`>Wfdg-{6M%k%p1cYW z=x@!w2Gob-(@YJ4Ta$gXzCsr>|#|m@vuV2d!0uA3g^}?LA5hN2!numy1 zKJ(?0UwfKL6ul%((RgbcLATM_RsKaKxqU`09n#5bfh8ibna39U>O?WQpCc8p;a~wp zSd02v9R}>g*9L^vOBDi}xS(aFWI$H$-~`MWa#*rr3Wo$Z(60~n>H7*JagzjfNv2Sz zHcZv*EO6B4>?{xFrikR#9m6>RGWugaS7bQ`xb7o+5MKpa!5)te0X$-mQBDWRL{ic7 zo6?u64kdncT|INgcL-q|c({IX2Y)G|XlW#LUVEg~mai%u1G-)$Ny1$aa)aWGZiDIy zsBmrdz;jIC^fDNfaMI=3Xdtr7Wb=K6A}FPU4(SZQ;bp?*VuvBrq2;#x%3(rutSOs{ zZM8Y4RUR5YT2dj-KaN~kv*$D< z!>Y9wSc-$nqp64k!2zX^E&eAN11rd1u1YCJ45pi(*qdX0OsF;Z_D((sYPkxOeFL#> zIX7a=kxwvK561j?OX}9}Ub;-h5VA+>l9!J@U=Gh5%|dO{pTzia zhbhETJ*b>PiYLo5Ve*{t0-;@Jd(x zkuQhnk%h3y`o!h&bB?O~c!Jh(93b4_d?b!IHlw#Br79`@ZWv&w@;1Q}FK02iV%v)7Jt-;6I(ChIEYBKI8gz;o5n%`2+kP3t#0u0=pbuP7 zBl?UZ6A(FX=fLnx?Zxuj?i0rtOi!?1!A&w(QFdf(Es9xT7dph6U(Er$dp*5?j;dzW zqbP%;$L&BZ036>(sOp9J{mzNqju~;|dRb4%YB#0qqcH@+(7FSB+h7!~MGnke&=26$ z`gm4bVC}_j^&J#esYE9vr{kz>-H8+0n~;U3<2nOa^5VZ>9pp2SAJ4-5+L`dGZwcT1 zf^|DpiyDH_Tl!8t#?=kij4g6~#rWJyt6n{1n>iV;PI?0aa!_;J@A+}z*lPI!e|WiV zI6Ldg3?gytV^%Ud`Z)+MPuP|1{HoyrnUTct6*wcR(T1XJ;0KVku{kj|H{eknpw8WQ zv#sxwV{)R>*^XZxZA7ruuXze^LcihTjaOoYbTeCmo6~h4W}Cuy~A+CC`zX{i2wr_VteY>Z%Z) z2GZ>t+?q>XnU#=d+3J)+ENv2XSq{gQ_GgV?8G1qS$=1%c!{=rK2%kZ(c*Ru7(# z%Cc$vslnaZJsgkec;&O-$-T!MisIS#1)%Q+V*qle#HrW!kWizm!T_&CljEB!3W04{f0|Xk7nS*h&KKHeBuLCdXu!D6etXmmm+aP7D#ng$=z!g!rb*tno+=>L2 z`FP#|+#JR3>tuJ1Pe(CT$(XVOFW##8^z|Wc_S*0f$c;@nbks7S$|ZDWuH-9QjWWDw zyVfTM`V4gSM0E?E@sx+l*tl%_R1%Ru0|ZwY+&a1hsj8b>ckB_Wy^-Tf4rt;7^72FdPb)oG z%ZbE^1EIz0^1Q)44@6dj@MX-QH3=mW6+RRZW>NZxwy@{`Dfr9Cj{+VX=?XHSgRW8b z)aVg?5*<%C+Pbze9HE}{8@(rC_p#hI2LS>%1!%FT*ui{_Z^E>qp|R4Wb>m zx+X8JxH{_$^a>E&2F}rMO+K7u6mnAGHJS*riE$H^f%Qu-Mj7SIhVK#FD!||td~4XD z*tZP93)5dBRWjmDsq0Cy+n4n*ledY!O7TzSinqa5L$djHY%hjb*UXa}E>z$MKkE!t z6^+-8NWKZ}R#g&q?CZB_!_4z#jbk-(S6N0&NOuKLFoKYbOx}SAJt^)rw8Lzk_ae0h zgKds8cGZR`W<#4INoczsMZG#)#@JE1$DoZ%$NcSAC%moyT&ZMY(l3?`Lm#qFo)jkc zUX?JKvS&_RXw1&uL)AxtVFgq>sea=IGR#&T<7Asfd*2_$N?AUp+;!7LRc{SgFEvbV zb4e0mGX)v0^!Pgra4wieh`?^@%i;=^qm)>D0Bl~iNseFN9W(7EL?YyNxC=2pZI``l zk!N49>LFfb@jY-j!Rf4zV2E%RaR(>8Mlk_gSczt}-*v9IErYdoorU+dVJbn-TR>b; zNUDGdox?)-riX`h8u7UqU5~vzB_Wrn^vV9xQ#@4v53&xS8)2`s3{`*a&OM zgjt})=Lr_jo*FnxD9kI#qnaNlDp0s2-BeG8^fCZAQwCAP&03e`G{%4j+204I=J zXr2^pr1o*QR!V02ap!g@{4h~yxqwaua${F!yEKN=Gc^j7mWX8I_Fo;iJubVCSx;Tu;4&?7 zT-6NRy>>2pydDUGms>Kvn}bX@1Z{@$weTQcz@emMpzoGmqa>#N!P|8qeo@$}un8Z{ zenUcC6K&$%dZBL(7T$w_Fzwai*y%izK8c-c!8{sTpiMnHJrNIOw|pKkNUnAf2=+C9 z<(YJ-(t*GpU^a$kdRlM{Fvf+->pBydwA%rDI6`{;Zptj!N6mbMWptr`u9XI%44SZi zGp7e?u}>RqS@Venuxa>KFbs1-8$;BZZm?&)7|Ks=Vh{BlF;YHpt9VXRW5%Wx3PCe! zbXOGak=>#gyR49@5S1Na&da~Xp%`=Z7qDg?CV2p}Dbi0g-$_Nx%FEb*ap71e-;ld8 zAoz0vovZvc*C6QXTHIeP$eL|JI2e)jm%@_+(AEeA%lz;ubgAu)o?njcEBqBFhZJta zvN-7;RRF(ECWBZzJ@;OmB~aAC3ox`Nnb*ULfcRJ=V;JLt8CbAGC<3?;=cBRN;-YTV z&o+^;*g{Wb4c#NAaJ+Caj5d-{f2jCs1a=}GKmIUOnE$0>|D|O)DBH!U1M3dfyR-1b zV}g0N*sGe>_udxi2Vgf;Xp-WHq_Hu4!8b9R)ixB_B^Ev;F3ij~4KHZd`@9yDt&dRv zi2Cu{6Ax+$s>fh)A!K`d$AjD z)Q3HPClV`gW^8gq;?SeS!D$fGpmS6nAop`Z6psvWy}-a1ojuOS*s|Gsc(C4W5RhCe zb*oR<0dcfL^aq6)3XJpI#bF+xw8L&g`s1W0Epl%n>M8aNHD<>`V5-O;@6^@f4iUZ0 zU<1$3T{~--v6^%b#G~V-pU5kiLQC0su~@^4@Lgh3v7i9%0k+8%Ou+~Bo;qA&QADBY z1fRSuk(tx3|tEH*5u68#vXUYdXYptF>?AzWu4+mW@zD#}rX=ig} zO573++8;>x-5!`}ODyPb5)td@@>x_@W{3KlQB;dIh7i~*V;JrFBwHLb;BM0tp(3 zmlKvCpI9!*8Op$f`e1`ZxtQe`FC9K18B|z8E1y$dVmxIB#RFt0E-&n7CstTHlKnzQ zpC*tV^Sl$g@n#0(F%MHuPBHTE9-or9(GhRz@6!RURAY|8#;3lW`wRgrC|qEb3ARt+Vi+*70z`=`of+WS1rW1u=mc_5?&F}mu`XW-b*1H!7RcO!M1jm=Dwr6m zh@Oma87n2%4X9fR#SCo#u@3qKV=Cf)uRwWh5i9k8vlV20>M$=TqN_n9MhBvASoJY* z0r*)8TTkA;Ae4)Rzko-1Vy9Robabia2fwqHYty80D@r{TNd2sk(c-6z8LvV8p-OJ{ z>#OTCPOXjD>vZ#+P+Pf3ULPYP7_+=LR6ccq6K3~Fytjl~<}b*%y_irh+(M3ytr4;{ z5VL4~PN?DFbIT(g1Ai`pT*iA4mURYN;&WPa#?XByeF6hB9wx#SF1VJM0rgDQh$&uW z;N%+Q4$PHHRIC>$xEP^|x)OU#_r;X*Fr)PZS;v$WFOg|&tH&Yy~LaXG|E6*Nzp(a6m zl`|86sUZqxyJKj&vnpOZrKTN!;Rki7Y)7>&t5m*w(Kz!QeI>%@35iM{_x@gu_YhlR zG5)+{h`RNFXZ|B*miXO*=rI9HjEF24v8^Py%bd87$fQh7f>bih1KNLCbeJ)8q)bre zd*|mu;)<)!h*N-uwIKGbMIU_7z7M;H6MFSfr^ks>7grrqbmx5oBEUu($JoZpkFJN; zTby2`J8|X5N1$H?qZ(#0;0|&2h2r#z!;jmh@8OPPhvfU482A>z0|{C$s1`7gVMgxk zq335V6e>+q)fHEW@X4lt|Dk1`m)wA(yaUJSRY3rH!=n*x{|W;D7PhwH5Y}bAXkTj% zhA)d+v;WNLbqt68HRY=21+S3ClkxpU(+2XDa$bMYa)l+f@#Ygj8?>R9jT3f_(cGvU zja{QfP`46kXJRr;*G3_14t|LXh^*3L!&%zL>?dL&N&>0vn5p-4bGcax3Omb@!PkJnb8q> za*AJwb&tqvcVg{+g3oc?{>^dbo8P2@%tGKMnJG7cF~h3215l}7OE1JKQA`H}P-Yg| zdSfKB8g_Sc^F?BF0}LM>OPw@3jREkUs9Rr z3-^M$IcdD~U~MydLa61=g)0zZ$=obEYl@|RN^%r-teN(Mos3#1rsu0}MjN15Vki%2 z)xI~eWh$Xme0@^W*rBGKz3c3WrF*GXr$wTC_& zM73lRc$@VL8td9Ki}(tFwS<@tOS~XTK;%{h3&37Y%ZWMXVT0#nF@m2HMkw$RF6xY+ z9sc=5L$!%VmcZxhFxAEVO?<)THkNOOS>1gLQA_Hbyxs zAnPPHpQCPD9&ZJU`QhPf!-fzDTV#UIFN3A)MQh4Unh@nYXF_Hx9F?e7!#V5AC$BWG&CU$)^em3~U?SR<1RK#cs*HJs(=;iL< zVP^tk$1X5zrwF3`NN5Bd=^UaKb7GfVoXodyoY56q+=I=A=t-w6I0147|8L~R8zTYx zAFJlh1OoeaL@*uPrL{)$vV}PIggMbRZnW^9Zmi&2Luj79-!I6W!@k<5vEjl)uhk=&Iys8+dJxR8m~L+mri$-99xFjXyD3?rE*n!B>F2CtEnjGG zK>~sJ%?CVfG)cm!R)5m~B4s*#!g-&Vp`2MmNd2!bRv%Ef837I)H|WIVmK+@#%CRdS z8BW%8e*6Vdaw=#+zF4rC0j!wDRRZ&x5#c!8XO#mZim?4M=8p2h5^aOoF7iFqlkQU+ zlF>W^htegS759uBGrT4+b%)GL1(`e9;g9$`w9N>`gE}ivHf;@hvj^7G)Aef$vN_PbP*k! z*KyzntNGAF=!B^VwZkAy$&u+AjLM(U&mh0H!)_V0%cQd>+rPjOu9r`9485YhtaD_1 z?pz*xlsMqxIld&7DqE#wW)S^&cCzIpv7OsZ97tXMN#Q9c-_lS)N$k#z6ob0XW{JGH z#{+-7S#&+j`MF7RZKsM`Z_V5+IhhAc*?zNjZ;10 zHTMf$j{f2s6TgI1@nvNnr=BWaF^qmb3Q=vw26%qCv-dtbt?9Sl^g6TlHFRnR1 zBQIKsP=23}ZD?<*p@!yjOM+X;n6Hz^Iu&W5EgfwvV6yJH7Bu5_!oV#)jCc6gK>jj9 zxZvisxbGNFJN9`)Z#)xLAi3t)SMSi`r<<(Uxr7*>pok474Yo)@DAW30SGJwx5B3yZG3^NirI#spl6qDug4zM zt{Y6{;yYrd&lNBq=EX|gmMu3_P-Pdf$jb5*oI=-dgsB;5oy1vRu8#5 z^qn1z8&M{GNYLBz0f?P($&wF%=i_FDZRc3+sGBj!iWk@dKdmr}dy6VvP)DFHDYI?P zU8Z{qT3zS-gF24&xcw5rFTp#Gdqy|Pw-kBsF&J74T2U?e2M)CRAF!pwVsf~k<;hwy zO6|4u%kL?>FU50vyADYcTU(-qu~Ge3UC=`OMleNt>s~oXefrdZ=&scC3wEhW>a6kW za{RUGU1~_^JtvqjMA;59@q)}+CKQcxv4&kr1JSJEJrj&EYl0*9xMEe`z`VD?Nb$o1 zgZ7OlRMBd$cx7(Bd@wvx{Um|H1SZApTM5X4*%q2iT?v^0A2Fvle%{($$s)hgF0j%s zX9d5}#I)P=3cX@tS^@2NvrBCarEAqaR7FU3$ucn{8$N|t#My)t%Hiy{;`BZz#C zv>fi}4QX1{IGBTe-gDm$&?%sR6R~-%(XaF=Ij~Smj5h0IpTRE;XcQm&@u*$HMi4~1 zn-L3mC)mm^UB4?AG^T!HFcp!-TVwK7i0UBO4$cF6x>=v;1L+<>K=)uf^iHk6H+i~b zgy8Cq{|?hEXjP*IOJiVQgTNZ*rZwkoYfYkm%JAZ#7112t> zBu6r^*dXoH_2Hd5E>G-s&pf_!>L_8y$Y1Oa-+^*J)OZOW%cUZZGS`$mId<9wpO#>t zYrJlb%tXmAL~S^?V^G>}0`rNllmno7EK|JVS?DlSpdq5;y-*zE$5+}4tn3D|6*WKO zm+-5cBaFo}z%~0a)qFAHmhAP|XCGNX=gVyIh~~>7VM+e`KWmN4Bq6BEZ=@zP4f-{Y zGmJw@niBOeF@nAW@^Lj&r|he;b&H3!u()4qY4&)suKkX226nPOiU zA6nV-lgYma7mYzvarpu571n+<2PFik%EVd8Ha69~(+Wsa>63NJkoACo14BC~;kv}j zV6kNw`O-?qn`Nj{B|IZDBzKvB+quX~h6Q#hLLa7x_0aipo-xas(3^WJ2}He68=!hjVq5Da^h67BE7zKO zBf5vsG#u=Y<@$(OF*;#X3$v}51Dq32N+nMja3pGL@B)8%9ntD`ur}vg`vk}2q{+Zk zYQrZ#TN76qcl&kPG;LkGcF5AInG?ZGLst%iH%j@^MvM4;c#i7&81K=Cj(S>8r?Y$K zGuG`gY3O&<|cp^sBq!QkmTPf-Hjdn{H4wj80q{*o*SDHbaxMy@xC<&C>#!jr(v8 zw2eb8Z0pBB_AAZhz4$IRlhfqn`NgX0qKj_Ts0*zPKi1ZC4ON{Y6|Z8ocYRVj_w-Bq zeak68%SM~2_~Y0>n=L?3LShSppgA|sgM&9 z6B8-cqWF#wiSffMX1?65t$2i-1TYwF>ST%(X(KX*HWZ9OO&kfTWEDA{IISJeWaykA z_x!12=Ula)vCxSkpBJjuwfU`yBQ%$gW=v9&x6qy=+=oL&rHl9b5Gs{>l{-78YO2yv-(Sq*1!hPV*w~ksYpV6i9(QSpd54}yY6hQAqxhMc z$if+%ilt!#?LUn1bYxMd3s@{h$joT{zt2;Y$>r=tDM|^Z1SA@);E{#a?H zW!~c&gfx^JG$||F=MmFh3@#iUsnO<^?xdU)cl-!2UNHbA2)KZ|BStdd4yX2a!)$aT z9Z$mD&H7GZ@7B*P*)6Q!uFl@$DR^zB8gycHbdM;DM;23V9_xSIJs9^3Ubm`Z#WrYK zD{EJsF*LlAB6pbQT2~mjTA<=q+ehi6X_Db+3|x5*pBodMjxt9t(9kBe9z6bn_NHr1 zNKF)A_@JtTsL!VcHY%2hG}i`KzqX93jHw|FaH88a?pNbnG_@C5$o#$DfpgPqyc_Kcb|e87Qw5eXdxP;(zvj% z>z~(E*tyVAxUruirDb&jtaD`$;C$JT+1>s84)0)s*3Hy9Y(KlgPd~}enT-Hm$~~qF z2!<`6#_5D#`}th$q|{y|@UUevb)EsIT);KUE+G#W<^%kGb?GAg%W7`Ayu2K~NeiS3 zUaN453_hYIh1zdg72hOoss4dHWlbG*OG$I9(ct%{Q?Z4~(2HfAX9f4d{0e%-O>^)e zgm=FU1GXmgtgLKm3nN!?4Os{4f#l+&(`dG{^k#{?>Y>wgh%r_2#U?eku&^NNa-=p+bD_LR%e?>fpIb{FlV)#>v^XUicZ@DXS+>&iXl zq)Mv0T5YS#^+rSKqem|gWtD|pQd(t;PwcPhS`${>9JjcbuOQq;^Ue(0TUeAQa5q}% zO%fJqW=6vIka zJ7#8Qn2*T#C)AUl(Dp5Hrq7Ceu9=2;kB=E${k(;kgS^=t_8j6+qv*FmKkwM=>N=-c zp7nIJZ$QS{>Kx2I_g7}jkBenI?H%?&Gw2%CajR528!(OSEk~ zKlT%X_)BKYe7co~Z$G7L_?D-KayCA1FyZLi0zZ{m?#e9mc>2UpKPS+x0ANtJ?0~_{ z)WU-pm^FH>ma?#EAcL`W8DjmP%jE|%N4Vk&kNb$(-PfNn2>f?1pZ_>4{a5jdKe@KQ zGiCn$BKVth85esCM-zH$8zZZKWXlNu&Sd^0|CD?EUk_nR{$z3cpDP3~{jUt^zd3{b zkJ=CaTN%=OK>vTN5KvbZRM&>hqScv~r)x*f9V#U!9W*}_c{2;dj7UGmASG)zAu}aO z*Qp>nalJH4vHqTaCr{?kJRJ8-1g5kM=<``ZGUp3 z|HEDXC_?hzyX!A~6!HI+2I&8+&*HnWh0*^iN0RD#qWwE{HcT4M3U(&! zN^0V^YV2yJ%qq%Ks)B#j7m@m(TKSWI{qMy6eZ%tqv6cT^%;ZmM{@+0QE3fQNW2Lw! z=gi^OK)nBFjs2s33f*tc|5d}_PqTmIpWY)c6*mP`HN0pdxgd%4J|Q}G)x7xB z_%TrhsBc7xwqWH7g+%o_@hP>!&K&2+P?kfm2eicD$x2l!wXBo@;Y#PF>WJs1EUr0p zFiO^RMfnG%^GPBXPa)h5+^?_R%O2aWURMPZ5d4Vo{#3n&b>zHW1FqQw56r;OtgL`50b4Hk~TnAxL+ZG)PHzOE=O82#O#ep-6WNNGTwobf+|eigcGs3L^F1j~EXqqI|yR zygxkmf#V;0&Cbrw%_Ar4*7j7o71dw_ccAF39!;c~jkTh$=Q^Xvg!>RjQ)l>{B=uG{3E0OynGzJ{4z>o!KdNLwnBv0(eL@ZY8k24 zel=j9C;lFHv@z*@($?TCiLYLLTisif42F$|vvv!63AT5WbavcsS{HqsWDM^9GEPMJ zB9V-$D$?sqt|$sT+`Xj~wz~{u5}ogr4DbXWEA{tzS~bWn_cVQ&s7F>D38D+>RZfiV z>Wb-Dw%6vLb{42yloZJ3TdgH_FCJ|x$Xwv>Z|4=0Hz=8n% za6U8VtBQhcA?Xl5f{UdVHTf<51+kxO{d!+8J;9`1C)+CDpxXm(aGm_4AiO$H)5Bm27Mu%tYbl-dyj%*TzunRL+NTljTmre5?xJhFqrUczq^9_}W{ID1^zMQ+lyXUT$F%VeB$CqV?{w zyufhfmB?^m&sXG%dfh~k`PIBLt;1=4A*EP~i6+|I8C&;)ri(uJrXdq#? z_^2+uc+E+bInEm#yEDW8yh?ZL?bwrBDpA+ClGr~CC+-hYT?jbPG8EM9tY-B#ESq83 ze?it7nT}(ydNsmCrYMpcdG5gtsPH)y|Bjs=CP<=p3T^Y%1%dQdpJq(#bBVnj6W~6S zU3-swQBuH6x^ue@CC$%|Z>_vCSo?01%ZvOyDfvqGP;(;J@qM?xz`Ey4qywE+P%bZ? zZ|M$k<+xs#U93;4S|s`uNfXrvInWl_!Oth~vFjODg^lvgm$|TKyo6Zyp%i17Mej!` z^u4=obOj;QR0g^V70rSPT}!!W!C3abim^;U6jq}VA}MmXHwWy^_($Hgv6A9=_eq>f zM+$@4!!d=RbEsipI|lCKlR`Cb)LxS@D#J~B2+K_vOg8+Wrbwr|S7d1704y{fj|zYT^oPc}J z7PdAF)+PpyCXPRY5l9BmcPtoz60lzhb&vyD9WjDvr@j*EltgA=W0}p)5m|If$Q{@;aUU}JOwv~;SRib@eN>Ivb=U4XFX8Vwa^ z>zb2N1q?_7%6<14DX~ned8C9%G*R{Q;&09h{9z#3|5;=pOE+=-If@`<9<$b;D>RUw z3~%)+L-(zp{uQxns`#iCg!s6pz@OIJv*m`~5a zL{a14!a`A#(pW>n(cRt@4^OM8k`SBRVr<@wGBH>0qjA2o_(A7qhgvyrWUYD4dA!0o zoFxiu3bO)LjhLqRSpPfD*pai5#7`n!xEbq4C&*R0}d$VIk zlh0zW;K$+N5#ZtC$H!@E#4#5&#m`eb&Jf8mR3lMMXro3sPqaQBwwu%YzGK7vZ^Vg}jkWD*(TQR00CA#1`9<;=#F_WLeT?)sEtn(k-&d$}+g(K+ zqL-OGzoE1ip1QXFjr@&y&o*_9-+T?Ae9OF=Rf*Huhm-}JG^9XF7Qob-n))H_`^FnX zI7s33Q(|Yahoi-LAYDo5M93i_9H1y;-j>S>8WA#p!ZH>_8JI?Kk|OS>yugzc1h7oW z+=f)GwM&`k69;qeAmtgIySCi10vPE~Ccv5*sT~`ZQR=4ZQbGqX9S=Y4ui^2)m}5Sv)}zPty0zVq(8#vAa9=PXo29aH1DYee^Eok6^nS&--QMx z-zEaQh8Gk-O>)Md|AV-S?MEW|n>_F9qU}$-RXpNGUumllP>o^H^8nT=R}2+j_+6-j21# zsJSa^jM3UAFV_(u-z6#A#n{^2L1(}o-X@#p&(ujLCnNnpo}QuYVYf^GQ(K2D1EW+2 zgMJ^!Gy~1m4!W*3`G-sloib=atSFqD=RP4KOZ78sxUgh1uXjozUB&6@>SyWNa^ysecU*hbIsnLpPJlx(_4ZC%LT20oYY7_+=;dI4-uKuK_Q=(a(}e|0D9KitWmvf#g_u&xBi@_<+v4a&rTj3MqwFdW9n zVZ>*&g$nDI_j}3Y^%6d{HQHFD(`+9uG8w*@Jp-!VG!_cWN$8adL}U$Aa8$m^obv$B_-n^9&p_MPPM>nnhI|o?hW*=Y?H#XLQNZXU-9I zG*~N2v3Vu{I)_kUxGNO6L=L7uP$;b33tWtbhJcVl{*RgLAr;RF4y997Rt7^DGbxs# z*qg*3O1wXv1`*x|cIL5;e{nXAAeu33TCeGXT&Ld+oEtBEzl3k(tfs8aBJ#Vk-dvj& zW^Zo1i({BJG1&x-6aIjzK{#PAeDJw8x6q_Cr|peV_c0H)<<9tt{Dml^sX_86yNd*& zY(Wlc=xLSB(3kezF|`Iq?Y4)3+b6L`laebjRdUr4U8MTF_j2detrx*Vve<89! zJiMT=RZ;}xY6yA=<$a4wuMsxheHrFNb0;kMlu;7g62FP7}%PzE*fpfBq{P)y?PkU@_&C)*9l z7rAfeRYZ+WY)felTv^Dxoa>LTmgdlxqS%q&yed~P#s2PPOQ$OIhj~dYIYv9AWq)4Cb`*DD*T`YUrL&b@*oEr*V2AcTIR_->}NKemWH zda40M09Pk&SSKM*^^#}@yV88g=8%vjV|ZPuLN(0%=gq<;WpR&qB0=USZAjSk5xDu5 zo2t|d(06*s5Q1yPB`m!z=*KUmJ1ID+UFI%y;w^2>U3`{@P?wr=L8Qtf>)v@?;_$)H zx?CEfj=6d?#x!3toXj;{S50``cN+4XoLt;-7vyyUTb!B0<9Tc9XV%w6xZb?t$i1s? zhAO@5fsY|6+qMYd0#~woi4r-+Ni9(aX1Oo^;D+fEjjldDO*WoDVpERdu6K#!bmY0$ zGH@_M&$+XBMJ45VafU;gXOQx`R)lxjx?Uvf58Q{>vM=D6BGVHMXo@ONb6&iD zD|9V;6^ZeN(;mUbe4s^O&1WN(j@7WhfREQ0oS!}yYN%dxgi&}n2 z36nKUiW~xodOf6iKrOpTl|8N__AR@_>ork0n-CmGHQ^0pG>r=mcWroMsWvVBlR|DR z2%vB2y(Mbl6XtDVv0Q@Rpc$QaulTqvUjKkwW)|XlyFEBVEI8FK-ZYO6jUv zI|csdSRZPL;#a6W*;08i!g7Z6w7N9PunFw0W!Zx&S9QGTSEy7fBSMVL$Jj7-i!nas z!?dRrCEdQj=i^o0n0Cq3l_HtMIb0b2+5JLfo?LPk=y`Ee$?1Z$z-QCEDXJgtr6SeQ z!jHaaX?!2qWP>=In9=R}BnNF{uLg4=ca`teh|qy&S>NSVoTFLsO1)DKEWj$b`d?hZ zPgbszT!C7*ErA&3Xsqr5$)#M(t0I_M7AGDCn?b_3)B(q-#1IiVEDdaHVSwu;Nv<72RZJ>L*DDKvwmMUfU8fgdg1^Ln%Zop%0f)#%w=O-4p*@?#^j zU1-Z(<)da4gY>!z8*ud@GbqQ{JGlg=2W+R?n zpT*YF?}55(Zq8fiHlbkX(C0yrBDKidTeeiMFxN^Lr_+jaNOg6&UQ%92dnxugEz<$j zPClj;@DMxSI$0- zx;KkvYhcEqw@-462XzCN7;O}e4}L-yVxk{ojGp@zA`K_wB((LTMc0St7mzfwBGQA_ zb9rGeBSD~i6q)O2(B|_=fj~0byRna5au3o&^8J3>l&AJ$fVjb1Cs&YxlMvh_*Q~L;dP~!4j1P99 zlClOZGh32z9_bC!Q+t+MMk#1IkO$(G?-_|lD14>1yS59x}HMh*Ex zh6`npvqzvyuFk^mNWAfU*LFdFTG zWm+;8o^-wzHsLMG$BVYlA)}nU*kzl)Jiw(q=i$iRtVu6>O`Nqo4n{oyz$F!D(Dr!A#gG#;lDR6s#*E0Ae0JT5AE#-*x5Vg@mOk99FGVoSJv#s;0R%I zBTDzs(uYLQI+?LPJwxl;h%mk(`!47qzI`5gd}v~VifIZ$r9HICWDXqU9joyt!ToVB zOnRFFJ%Gqk?dPhct`sIfz!jP_xgUT(=^HhG;os8hI1ahehptqV8z^)Loso9}Pz7mVQFm13}`$RWdQayGhusba*E zu4JgNR5E?@vP4UiW4ZTImtXCq9rP;SG~_fksgG=Tw3gnkNAa`%FKZPcH_J6VK9a_L z?zs{ngVNd{rZm72;@)X6L4AL-d~B8fcE0a0&hEz~#E;5)p~E!hLAO z&_d^zT&{s6JY0kUq^SN|+9VzB9z9b2MUz+4w052NwQwytVHYlU#jQ^*E$ony;oZ*?dgGz@1V?R>FaFfwawTv55kiGwLUu>nAwfVQ$o*oZ%&!;QfTZ1{;#{?L`_r|mn>9IwF24M{ z-DN!1lT&Lxsyfegl-1cKpo=M%6Aw6Va+E18zU|Gfyj^~wIcu|8$lX={^`&3~#t`J^ z6Z+bP4*gJS@x>DFfLB;P^fc56zy0WW5MgHSF1kIQXA*NEw)lP8EW6G{sE`~Oh)Fn< ztsTT=y1PsGOd4hFngmny&x5Y8I9!ONjB};%@FL)(e9LH1^(Nz%ZJicLusF7=66FO( zq!HA#C84)TSo+MgjT^qjg6;0>?60`GZB^+@X_Q-XogXdDu`oUntm$Ol^ucYRILThG zY+}T}Vy{G|h-9S;gTU`Ij#ElzX)zA@mZ;qeKIhpuq|WdQ2U)Gx2xW$Y<RPQ)IVTU%OFzA6^Y-y=ASdvXGrx zFE%uqFT^>644tz?8a7Y*krUQXlLYzW%KI6L0hf!P$C@m_UA%pdWh+d+Kt}%HqE=m# zPlh;(p~M5Z#Jrcc!#L})Q*ADg3Non882h|>_K|Y6~mwai-Djg=%ppm zmnvA$5dshNaJKun`F7{^6JIDvigaF$*8Grvu)9twv<|Niiv*Fj74ft@Ua;&rD#~^Q zH&3()JuK|!q{MAb|7udCMBl-Q(u6K$H>k9h4M-fb`=7@ohvi)Fb23G;8F(g-wQYCA zdwxEsNgA&F0{wH10^06@M@`J30@f%q|HUzYwtEg818BPkv`#s8@Ybm&YyEAF((_Ce zK`?{-N~?ovx?cf{g^olHI!ACk`;G8sISl*Scdjk3GdG`B2~yq&T7mSUh7aK;@w$^l zO9~^IfO;>-ANnzp{hh1AlM19r#gYOfs0^5c!`oS4^a5S5~UxN_iANMZR<=o)5aVKgnx@Znec#+>#L!xb`w3 zYCtv*N!#g`hn6ZgLaFD%org@98d$XYWu*rkOl7!`o^gaZ`_y>3yXJ+*S(p_q!i8|_ zy}Oc8oCzHatvxQ%_{$28_fa)ahWk;9t}a|b+s95MR4N2J5-K z$u5kajz5u}K_F?d63~_@)*WR#(SS&}QL~DK>mzD?ZquuryN5!}*Mc2TVk-(LPZL>uwH{?v_jht)`UA)QjqVkzoYpU|th!<++ zlz{KH_VSqB+`$bCiVzCAs`j9Wd$~27&6{-O$&m31GzxeIj?q;T^N`H~Yzmygr5myQ zPDvFA)WfpOOkG*d-~3%iMF=X5VuT*g~gv5v#I`SJet&deY(xe;*& z4|8=^Zk*T-mlS?lzgE#(c*CZPi4P~Yb|A74Z!1^{iO^P|J}dRj@@U3Nq>hy0O0;H8 zw4<`aAbK68we2&Ei;-N#Eyle0+#<05(Q8JPisG8kYY%Flv4!K#tHhHOHLWhJ6X6Ks z4R4kW?tN|ywY>~iIh9Yoox9dsI$3kY_7%g*8?Wus-@mSDkXvgEqnV@|ucKV;h8`Cqo|8dJzrdBhj?-3-Q=98rwY_PZBzEJi zuJ2HZd&0}D;nWJkT~UYbn%hhGUr>I!CeSLTL&`|~`-@^{F2`W~-yJGKeLtD`KX#}n zs*GX!+Li3=FODg=_m{zZ-Nrtw#EP+5UHfj`V+Y z(4nD4fk8L=Ht1^SQkDMknYp(ro1Zwr2L1S*hZ8R;h)XCbi^~{YR~D0JykDbObjUxP%$-y}pM&_@ZCA0|#p_)E`a<-z%2G3SZugk_Jp z9iFj5Sb35AWMM{%~5f_F#)s+3A@VG7N5rt6G(N}~O4c*vrCaHz>Ly_le zi#QQqK7*4SYQ5sWRuBQ>3EjC&Q`}`o6ED)d{@R7k*YgSrriQhK zttRKqtM6}CzM2_hsW4;DcPp@TF(4X@9FDaz1UOatXF@Z%i>4H=*_Q4*=k_Hvtj@$D zHoyvxz3a^1F}$^^q-(9MYoe;xKUDU%I(DJCN#RqKd^G&kCyT1(N?C#{Syb%_FwtWZ z`uWA1BitDC!T9vC))sd14@@5Fku4{M#Vd&u%>+o{KsOn@XkXdxc%Kou(Uv`awtARknQsJV`oJcSdxW@I@8Pw%i6_>O$wD(f`5 zC~Ld$2Yen#e@q8qig7WU#vR49oSdn9`JRm%E=#IYdwL+p`}_M&Cl-mHFj2l0iJvg}FKNC7mE0e+DO})HLHqk@)A9+bcL=!S2d2 zLp-ra{KWo#FV#Mbd!UEE{}_rufDYBnrKc8&pAPsP`A3mB2>h>$#FjlrI58S z1Kk7jho$p}Rsa$bL;nf-sYT*{FC=)8I7rUI6XwdXBJrp)#lZ#N*$L9>F5=b9Rn6_u zU^mj>bEvh{w~f^({xFay7KxuIGFXu~NI=kc%vygYsGx+xH~w1EB2XVQ_NPkaQ_%E& zRw|#6P5fJ>vXUvPes8;6n+fQl>RaAFUU(P2&7!hP5U~kw@zSRNPdC;3DX4fk9JeCH zdtE{1YMZ=w^Ry_1lS0FV&;4KVHvC}@gjOKhk_`Ef^HactENKfW zumqiyA>qjhF&N1Sp>YYiA<41o_2wku@fv7)aq0;%QTWLkT45KHW6|}(V&j3g&||b~ z)2&*yzuikA*3*iMy<{3o5Se(*oS1lW={=e)esbjUhZrE0PKb}i(TmUui;0bnL)S~t z!cC6uHm?m2)dXI%S3&HJRgYEH!pBC(oV3J7&ySC?iNjRFe%}iFj5u=w^uT+Mf_^E1 zMH198D-Ri%QuG8iT}M#=P$PDVqQCl(!%f!{d`PF7o;jfyZnEu-W%*4N^jA7VZ==>5 z1^xM-f8dZ({*+P%;~hZA*%)1KKl;&^qHLu7MaW63286v*O6^5PKfN_+^7CGuQ(#X`wRn-NlMjl?lh-x4#;TNAU$Z#>PetK2Hx zW5N>Xcpb`w7xiR;?sa^oX)fi0;>5TJIf`x++T_fr?jA)``}QzvKHZ{tmYy88Yv|To zjn3ms@Ug86JhHmV^!K`S=-vY~oF3T9%57AkC~ORa7!0nXGl5%!+hhL{1dtCFd|C}gl z0;lcn+Th?;)+$*C?Y8sXcf`)uTo>oq1o>0?6%Wd*P#2!tso%ko)>5`rD0Gsn?Vy_= zZCN)xpa!@%Yt3VPS9YhQ>|YcZI7V4=h?@ z+EF~Aw3V7^G@lSDKYJBR|AK5VI=#aVPJzILDJL@n?=ob<{JD4W=OdL`gHh)Z@G6r( zb#7kRt|*a3xPw1f{@hTlb@+;@oB9LUZWtnm*wCU6xV<;H1l#k5qn_X5b(u?E^4M)B zpNyunk{w7hQ?}>#eY7cmV-uz40$q~jW5iv%f`o|`;)`2!aIJdvjguliS|RU){ig|n zSvj#{=2+Ca9dKy^31Q%N5LZ2)-i&H+_i&+s#*BcQZ+6vBg2~0F~G zClyyyCvt_Y;kVx0P#)2I#`PZ+Or4_j!n?>UKJKE&Qb9ruQ!GRHL+(RkNM%D$$0^@N z@>ZEu;x>tr>a8Xhx!UqfnO3=vNpGyWcAUE{glJOpXb@c1PzAJC6th<}Y5CXZj@b`knjeCepf0OK{9|M5{)`R$B>vIqjj`^28+Du4M+6p90G?DeLrF zc398r`u4Kt=G^&g#jj~9k8ba2+7yQ_T0g#Zr7vPQr`)ZLIg;CNq>*Mbdu$oXeX#f5 zP^>4$FbkgfivPxTj7Rz{yN+8$H?HOJj3GOl3qnnoeRRB8N5wqK6gg`f%a*`P{Z?vR6&A#hPNb-QLgoHN%NV zMSnBZXX<{Ux9_d{f34L z@j)2$r?G*P0ia%32xt)Z%0D!Pe55JC?@fXDTpa(ZLGEKKz4J#J^f}rfXdeTRG5*(- zQ9=4=caQI3VjgL-{dkiP`2-*!L=NU46`fp3=^0RRxK(jPPgn%sR5 zUyQW@fa}0VdPGA40Nc&)0InO@SeTkPIx!sHjt5|_{22x`(5Z?<6W^CQct>D1PJjUh z`kR_g8D~&0iVD?b6oDRS0w2+l<`scQ&VSbnkXijx4~oj0(1n4v zK++0m=#(yiEa~XO_28BijPm;~{PeZJGxn#O6A0w!)9>IQg+O-xJ3Z-roezh-IAuP7 z?Dgp5%isVZSSJB|EBgMEy&k&W;~|dt$VgX-{2l<@#RC|>r7i>cyF;%8>cmmjgCZn~h8G zFmU{ol>SK57-Gl7{@(P79A5z9Z}>F85kX!9n9Vpm>zCV=yH*J3l^e6hDL(E?}ZN7IKWIS8{j0vvs z-!RJ@4)x)j`<373J2WVen#%lZcfUdZa***u{in$DD~k-cJR!1Yln4B{;B@={mD=UI z5TNn6BloXDeBVhw%=&(FZ4`V&ex-T&4h|ZT)w6?tm*Q|gAJD>I2y&Fl1zeCQ`7>+b zyC6UI?EfUo;S4!GBJ4+Ir4+D4Iubxn@qx@t-#6p`g#0sAKFZeuu7ww8ME=I!0>Gc> zj(*0(qnst+@T`EFK1;{Hp9=u^zbT>ww?Su)i#=`J-_(zEg!141m&WQgbV>@N^ zf4#g94o?N_ik!vzet&j!B7k4-#{$aiBpSph-`H!!D1b#$* zP&(A$csBTB3)2(VLqCJ?(RxvE74SNpO$NQg>e16xaC~ifbOBiEd;7cU#gV$98+CEW)LxVf6?V}68Vz+t$k8$_a6u~myCC2Tz!POToR!B%=JvlT1#AI$BJ!d0 z2f3u9wOHU<7(OF1*b3}KfAkaoXtfeJ{^dJo>p6h`cPAkr#UHKL0f&F#bvF28DfnLu z!O>C{a2ZJMo=t}Dg)RRc5{~8|82)F&KbC`>;*ySUz!Uz+gFr4R=(r5OoHzr$JZ1YG zgmCn{8C=T~ry%?oQQ-9XPgsZ3`PZ{zaIDaPV;Viyn`4^$?Wr*=@K^){gb47DlNeye J0a}EB_rjKmY&`l;1y;|J%!<|Lt-cTVoT)|DqA8e_8|V|7wKcA7*&|zt$rCPi9d5*^Hi} zlYx`TKdkZny|q6cLIC`+=C*=8kn;N*+Hc3P|HhggBMY68smWjA;{9#($NwI#`tEFE zx=W>u%#2hEi=E6ItvJ1m)V&Nf#iVr22>_5B+pR?gosW8n^@M)k_z@*vFt|^*FbC*F zH{i~scVjS!nZP+8id)k3R{0w&3f!^$)D$4(4v3;-Ake$qfGBtQUKN65QUta{kf5d` zKuA>qjwUMkWVr7xXaa{yxwm*Fx2^K_>#XuG!r&}q-F|hLQ|U4*z<=Gu|8Uhn*XMZn zx2svdUB&+!R~=2vtW9j39Q7ED{&M$E42A#C?oKIWW@P{P?czjFPtU{4kq+$qB7bB9 zE$2IWI8UW1lg!bM{+>Pnu$Yey!6*!hegwUs66NsjKVoF?i4P^f^;rN|L;0Nxwa1is z7&{|Z@;TAkm|#Kznk5{nF1i8;d$cbxH75Z}FGW*1Gd*Y6#^$?~zP>qSMxwelQchZC zrlx#C22i326XUKV9W50Qn6;Org#ujxA-B4tX@q)+xxI<9n2K1geSTG|y@ZsqT~NE= z4q)^sJx14LHIZEu{N+VH#Ko|qGh0SbowhoTz?OU zDNXGJ^@OqC;eqH=&CsssZcCJoj8MFj+LKm{bbyzUoT8)!tJ~{^+8r`p^HvnV(&8qdB z{vE%6w%GrF{Br(F{Oa`mh2M98qGNv$Y67bvzLH$v3`tP=7e{$_IpvoPlrV-w4rN^Q zkr^b2Pz^+(`gDNbIA3xM=~Kp=NqYyfpaO0eoHqSWp8naYAkn=H@V~KZ^n3iRr;HqQ zj^_Uk-M{nn{C}bl?H>@{OHWOi{@-@#JX9P8I0`c(k)vN)e0(HjcUnI?qed^kI$;1e zbuEoTH4Q60X?NH#BWZW;KsN#F^!VTpa4LnrplbYm2!8jtFGwABvLSl%*`eoK0JP?b z#tCNaG5Q%P8mU>0ihA1MkMuADPO_5lqjsU$~e*H{^tnphZ`7}yvZSn3S*!`PZ9q(4!GPnFpabTUUN;9-gGb`sw6 z7f_lf7BZ6zo8R*{29P>8-jkH+$d`BGo_7G$)X+nKu@dzd1pK=vOv(Q##7z6vwD2GR z06gITDjRbAMw8>eigBZg&UYLYlHFEfKIM>UUF2^a?>DKwWq?NA9{-iv(9|LT{(pbe+A|ynN3pTo4n%nnV zRBQ9vi_Sjla+Jlv9=6UMABM`okfg=KurfNWNP1lXg^6*y>ZX&dRaMwT*o)Ts`M&}= zEC^(Ygf@JXIR^x{#cWohh8Iz&nNXo*@lPO2D^P*a3+mYbdG}A12ENe{fvs+@9@wQ4DNbr)J zB0)fGi3x(_pG-!>2?wbNe>rF;CP-#rw1ke{qLVR@xMcKfPA^TzN<*sjU2m(Qlf@fo zeiLA4^~*_)3&8K-cLYHW?_nc`d@xEn1SAV|B)7h?VS!)@K+01W01d&_?QYh%2g5#{ z#&+9$M5I_I4D&+-haf{PquEuxQl_1^?(V*c>)ao(c;IRfLc;7X147M}#yY2@z3mT8 zkNQy-_aGS_M7S;qQAS5D0}AC=!eWfn%ON74y6bcH>~D1f^6LPVJk0qT_^oYbK5s=y zBWLBUZ%ZDbNZ)Kt2KR*X@VTXrLuKv`$_-nzww2R&ViB$SeGoTXggfnNSZGmW+w1J; zar;|_TDfWJSy$F(yW7#)QfVYPRU?>JOSN~p_tD9StL0D>f~T3=k=Ir0^iO{G^Tn&< z?W!btJnc_=J3&hhR}aUfCj~j|ul@sM-zD1fCmnu3I_vb74wkpYzCANN8lTq(dW+-x z&Gw(dzBIaRO(y5j$*bMIWXqP7O-JdA3Qs(a8rvn*nshn**&=0Rc`O77=58iF$q_Ji znyJ0MeY@IX+L#bQ*|8xx^K+efbo_uCv-n~|iNOB7i|_Wv@_J=p@~8Jlx8n9#$R!t8 zUjibswW;JA=05lw5x$vNsHPz6-dSPUzpgiiLoy%ZR!#NtpYNa4xy~=E9NJfux=Rrn95#L z&fIKw#*ad&ekuZgddea|F0);!QQCKqyLn||MW3q$Y9b6zk3*s-vjq=0{o|U z|G(>D)^2Z=6!)2xgqUR_sSB9On1G^@ohPG(74rj#H(-IKqym?ZF8Xiw9Y^E+(GqtB#aIKz=!W&6`Mb;_dklw-?ZMzo{(PHi>+6#rt!wZ zf?LU`)V1{-y-g&x_5=f3nIK5(wOK2Wko)m031lfOV^PM#U>Sq$M&$O7LA2+z4JPK4 z@NG3omlw1%iZ=d2ehyl0SGqV5B(pQOUCecfGjpEXcUM0LQdrEUUQ)ikzPyho#+Cz` zCY%{#QBtIES*0lQ2l}=&8L5Ciy>5s3Y3aNV^4l8v=Im^b-e7Bxx_OyzE=)+HBhZrj zeJWLw3lcQT-L9c-V^c}wytV8qJZ)}vyZa90+@>DxPQRZn3J%WC=1VC?7KSuvt|AZ0 z$$m{TScUVKvu8co3syI=ohQ3grAD#Vel-+tPSfh2 zOxz?#(VIHZ3@_wTyY};c$at%(E;H`ydAGbfCO97+xHzz~4n-g*QafTKYGFT8Vs&2d zPe$(MZz4is0O`qmGB{$BF1A>V-8)BMAr%Dk?emW(MSm_XH>6FIUy;D=be!n7?Ucsv zN}Ilaj&v$;&2uIKr?88os@#%AX^>mYjy^w|Y3*4J>ydAhRhw)|;>o%*oK~8D#ox{} z*#6|ndA}(C%y>7v4W28kT^_xCH|FC*5eh|_2_FFGB2`IDw!_2RJcb-%i6SFJOYFoh zC&q4WZcG{0Tyjobaz-n+{?WGXr6Yc~BidUKB;>$Cw>h`bv{t*jXYeX2cwccZ5icaM z7~@K{V1eCBD8U`N8|i?f)NZGMA0d?|As;2B`EF+*0XhC1^<7s`{>D;40eaxg4ldzU zTV?AW8x1yUl;u}YSXiR+&FhjO0XXy+bc#04Lg9CqOwEpJkg407&!8u&@i(3bwOf`J z(hC}8F=fehF~0U!0S+{W0$!AyTu_n4auqK4$l8(srN}UY>RhS#+;hZ63vqwPcdQ*N z27B)!;bm}PLlPlxU&1YFH&i*Vo^@O_;X1QVI}sJ41gvCx+{N$IFm-KkrR~tJx zNOWW<;mwP=wuK?AWD=-W*X9X)M4|SiS%&fu=FjHxkZ%u7ZtGBK1jHFgO1udOL!bj% z`<*!mj&Kc+U<~hyyA!=7R3VLF`T6mkTO4V}LN9on4yZoBW+X*IQ%CIpQcm9IX9qac zI~N6RfhrN6?FZpaL*Qn;7C264ST&75do14yB?O=(siuSB|+oS7^m} z0RY73Wi9^XHo>r#yrSuC*F!d`4+5DVp?4l&xB z`_3{t|DbZf=zaA#g`n$3fc662YC(x*9=jdr3-6hvkFZ=+;+f}AjdY&^@B{P%!w{pm z)((E<+XToNnSRc~Lzj)$a4dSVpy|1}Uu>?z@>57*qTRK=01~l~#D+Wf%I2nP{rPfQ z!;MA_U$NxYYEKeSI_!^`alBI(ze)z%*jwZ@ecpJCm?rv?FkQicLgWwd@Tm*?kUeWl zV8a>m=j(!!uAeNhRjX$*;42XLT5r5QD&eBneSjUh_ip*mXB0=u$|?bUI1K1@@PjO~ zv6txmV+woSsQj)A2h63N%S_JCZ{giF$%^t(UO-4w5~>~4Jv{gVq{A$PSn|o=Sz6*G z2U*xw2@X~L(06mI;PsO{NG8AO!yLhB%PGRg#vRW>ChR1$#8QbfVLT!z#OPCm2ABJb zkHeHGru9tD@Sh+b>dPc&hmH}_9-RUzaf!@iavLh1Q%G(NYKlHv0 zDox44*<_G#0E}sT9`isgVd~8eH|vMu!fFhfa-HnZQF)nWo~5VgY#6{a^qRwCyRIrh z^EIlah&f))vtc@B_Z8CoQU&~snCb+raqztri{?T9gRZ72M5sojhp||S5{=xnxBmU~ z*$)NH9%<`E0t4s$Z4i)=A`)v4;>f1C2+X(j25m-V;ig52S_pWYGu$ut$urOk@yKduUh2j^MyMR@R*HtWYmeWD&VXP>ex! z;qv#BnE+={JG!WBf{q}2+WDQWZ#T99c@QOMF>tS{ab3wFco3w(ubYV3u<1}A+zBw4 zT>jzZVn{)8e$A>Zvg@QP&LCBR%ITr!7l#CJFBH>L0uUC__;8Yhq8Dre;X+aE*sI4_ z-*FPSOr~@Eh`Ug%?5L%_3v;zKG!u`x=h!U_gz$jRvT@!NI_V8p{fMjrnbUcUI}6&L&%OK7ksp zyh$XvYcj6MCK=_9TYV>_^or{cmsJ*r3=G-+WRQ%P{1uN?EYYuhm{foW%0CXMw1yix z!c%>XRZ9n=^y>m|h13aarv*T!FP;Hue^KA#p2R~T*PYF;`#MH8mzg-z4?~j+I9lvy(Wugp zJX@D3Fs)@y!w;@;dWUmufYMD9{WHnH1+BTBUtl#hrY)J(sJ&KaJX2P#=WT#Ao@qS8 zm-{t6wd9y}k9+0ldfv~!hL-`lz;0NRVAA`;b1_ySSKsCU@OB-tQVf@-xUc|8qsVu zVROA9BN4(Lowk9z%X{l zxSp3e!bv0@P;X|6?l2v5fXRq(@}}OG=Im2FeZ||3)o_cH#oup9a_O=_xhT&_*kapc zrE?*=CK8$mG1R%rGJAY7FA;3WNsLbOu1gq=L$m2;P{E(4eYcg_{U{5PVF8|ln>eau z2AmtptZ5R4qk0m4^#PX=x85As)BF@uEMH4ou)woo{W){gD7_*#+4e2`$Zz;3B)1{P z)JQj;iaUlW*BF8GBGyPnI~s32R%JJsU@iFE8$Mevt9~oqyOAK~;JyVkzo9vH!Nwu^ zI}N}>^-sd7?T~(-i}9$IaVTk=4iHeiP2F2H6{FGMsfY_QH%f3fFaXUe3q; z71=xtwnlm3rOG$B(YrZ^?y-YaXF5^NUM7xDs{K+2~RAULjsQlC~79tq55w_iV~V&(75p2c?Q*b5oiJ~-#(B=VqiiC z7?5tT(JUd;Y9VHD;q_|1nX^Jkp_F#ni<)v_{SiFp za6GW29g{1k3hT~OhV2nACM>0lv}H6QZ7|Kp0-*}kw;1IPc8+zIN21)u05A`RE0b|e zvg^}tj8h#rO`vcJb%H4eZg@m6%E1ADDZUyC&qG+#SaP<)doJbo>DD9^=I>Et-KcdMdtPbx*SGa8xIJ~KjGsgv-=*X7J zBVn4Y4RByxCj!Smu@dtIa~?lJsGoDxVoq{4z+*?y*HkCXYQUFfL6C$1bf-V85U3IK z1#5>+OiFT7-Qyw$SAz+&M3W${4kho7W1@lm<1CU@AOf~=Q!vD-Zb=nL;|)EY#TusN z3E^?eEP;vOV+B#4D%=_4@FUznA5CBk!Wah=@NBU}ap(zfA~c}{WeeW?4LwoJoLtrZ zYt%LuRm6?e5n8cTPDLn|;bH_tL8U8r-*?Omwi3VxSZ@oQSY)9=6*Vb~|<+0n2Qv)IymySMOh|x-yah^ia;BQ18s?RqxE4ICyfBp8R_toXt2+RDJJT?A9F0nkG6=n zWbBp6vfMAsb0v}`p|M&ciN)sAH3;M%!$l1FVgd-Xt7DxbR^ohs_GwfG=KiA{Jqh+! zQFeL^e7(p@wH>?1IJLm56AmRUvEDsC2*MgPB+BuHkS-Btt{bp2d_<(=Vkk7c?fk?w z<5rg4@Z+2n0o*o&r~I7gcWtf63B|Q8v-?Jqkp{yOLe1Ml8P+>|QH-Txu@2COc5!~g z6|5T+q9}3%&}$FG7Bo0MAS1#gArYZVDERiV(QYF5;&BP%SCCK0&`}Alh-q|O8zsD< zP{O?dz$xUmg+)(XyS>inHX{L$c+%Eo0-*tQa<@*N0g4@+i;!1_Sm-T`>Ybnt49anG z=SCLctx)!|Es@hzdZO5@W|awB_;H=Ol-kHDMo22v4LwWF!X1w)3^@!pWh#fkQ+q<8 z-ww~>a4s0q-A#cW!E16=5zT8FM57%tWz^pW^hpQ|1;|)BeCCi*6y$cLA47!+Q(hQG z>0EDv-jwFd&IPOIP?r$G=Tq)zw%CN+1;bR%JR;5mZP| zZ%;zK6EH+MkT&x5u1oX5lLg?OU*8QWNYMPd`6)s+wpgBn_`mrFJ(43()}knVcfa(X zPhHu?cCQf*Ctw=m<@etVkkSgwumj*D)zdZVfW>qzf!Wbw5FQU8lmP`~ocsY=Dqf!} z83}Uuow=qP*pbtUBcHPC#QvFb$sSQVq<5gYHxt0Rd; zwp%je9(M53ilfs#BJ{D53Gtoka2fG-i%(O+=v>i&eo)W9n2{>T)_{g>rReOs|QiJfp?_XmH7ga*E)DXmpbi<5%Z(o@2Lh(rucA% zYNmJwO=7U^FcLEy=S^`~T7+M;G2r$&I&ref89f7$4X>R;<%YFoTR2FcuM>(_klRxU zazRE*Nephba8&>Y+%SGJNI3y5(6brY8mC%5Ad5Y|aFwC=L0rCToO>1x<;Mfy!HH^! zbnxPb=0qxRX`r-wUC^G}133i0Am>Ir6uLT%Fhkbi_ot$*DcS+mMv!;!kTF5P-;e0e zmJ{oZqlmzXU=Haesl(`r*J0mFazM?cBSr@*s1@{QPK-cNeZ5w&l|-AP3^sMBEjA5e zx0HqXLih?2B4RyAb1dHSc04OM{VVx#RZ)qAOq!k_`;#)3P56&=(HN#m7ztmb?aR@+P!3 zQTy%sF%1)+7C46ELyO1eXCIQ$43qVSAWa&}hGS4XIh<1>Jj^#>DvxFrYf@5SctoV5 zKUN!$1;x35)V>FQSY_{rjxzXebizCf;ce}9HCvRK?^U$nF^Q!J&~9;f@gCsDlk(|Y z5LzO12sfT@iW|e=V2bv(_o%>)XI%T46G!yBtqZ@y4C_4~A$H_(f0KF3+s&nNu?FdK z04X2{PJcuiZ?11_yVQOFs=#q}uM094BM}hv#GrJ5dI`AIhO^SJ z+x(I=UuZgm4t-T~ZGAM+pI+lyw#{O*dojbZmx|IWEU+{GbShKimv1$a)OKV+Kxx;^ z7vWCqDN^D4{ijX$oQ6S23FRTh3fx49c{AeKkDexcv5lxPc>jmxpdu^FO^{RQEK7VM zOyKVPtTL>iuDfotX=Lw-lQ{h;$dFICj9VQ3SHPU(=dK|RvBIg;)k4q7ctLY=(nwL5 zJD%>RA9GV-NGW~P>Ss9`T2q1Jel7NV+3F0_=hqawFU5fn`9I~?OXk?7=)lab>6F1m zy%Z@^JqQLS=JOx46D&MV>0~;-X@r|MO!LG`53^{eonT3cLAyoR&G5^r2|PgN1Vor( zdSyhh=aD?f&Apy@wdg{5)_zt8FvQT8Z{87F7DiBg;*DU{_mVC2CwR~s}iU-;mRxvp@Svk7mSG2#n6ZTfbG0dN~`t7tB4wE z&;zQcK?{FJLgV~_-e+hw?$q*qEv(qyMo3b^+scW;$*dezb^rC3_I3K<_W)b@&=San zZ~7-*E2kCRGk)rP=&F|J55R{t;l)`f1gpdtkdnr3R7zZgZ|Zm?)?k+CGS&U|YPO+e z`)PRlQLT8z(Sk?2+AYB;8f9uDy}usJ^)sR^%wRNORMSCE_v`&C1Cmx}Y;xPKp_rA@ zVDjKI#8G{wZWzG?dEzKTo87z3^<1I^XCXfW|MB!<)PjR{4M5Ov9?o$Yvp-G z$ja-y2)04SvFMEMzqxRF>{2zklLWQdz}c3O2l!z5ZbNBQ2@|#=r@^Nc4v#4H=jFza zpWOXG^vw1}%*K|Tsm!zbjNCo8J0)f|&G8MmDZ>v?aU-@6FnAz$f_#&^qXt1x){OwsYAZ?l8FdK_@&HI~5EnMLtYANp||v zA$L|{fy1m4FroYmjp+h$%9A2!gieoyk28n?yD{MxViI=|3lmGr4W=BTS=nbZ#F{JI0u*wE3t7~(=+Ts`e){SU#wCt! zg9wu9=s2EXX509vo58qy%6>1H_E47r5t-p!2;VfC0o*;WP4_@}rpM0SZ=NZ452X}4 zE}v+ozkG>x)VDV}8KYLi>*`c3eKM1TpC?{#IC(>0>yQFd9prX2$ZRBGtk*vH4 zT@}znKu0dzEqU3bRpCuvxR_ z%Y&cG2JUkq`vxF-UJkH)4PEpc8rR4MZA#;g%qQ#U6%!0hC-CNQ7+2zh<6BN22n;GV zE@CT1#Iw|C#m+e%t4Eo}Wz6p27&fOt@6*G$dN&U4HJ zQu66~$_#@c3U4(vZM=xjDJRdyT=-Sf?$qNef52uPrUpWPoekKEqpOk%R5!884}BYE zL01o0Tqg0+uPE5X9IXf6yu4%@S=q3GxB?Wyei1JpN=`LWZrbxEXvD zf^1?ce|(Zer#rZ*FCYS=r|&_}X-8MUr6Z@Q_FHa?BaujUCfHT%oOQMIhds88UzK>( ziD#t_q&#$Vr_;O@SK5-n2%j#L92~Z_a{!afX~N7WJvXli&-Hs6lPf7-8dX7(1AmnnMCW0%kY_+#kP%tc$5$h)R4AS_9w> zo|(sgqSbKcq5zXZ$&Y4cv85ZiqGH`9*^tQPk(Q@KYOKOrxow1Mbr)@f^Xy>6-NQuSm%Y=s;QOJliK? z&cOf;lJQ!I6b|1+XgU&q?4DaX=nX#$CO|{O}p8nXEPonw&ViROR|}% z1T@3WK$>U+XJk=ERrre-s?4j(`S3#nyYq+VBVo_(TE}1e=ojI>0N%5%C8}h0Z@Q3 zMZk1kfXS3JClEeZ!y9MU5(ODn8Az3_>gxbV>McL$CVgw*&o-sJj1_h1moa0mz!K3R zPbk?A41Sp^0kggQf#N|?gd&I5fU%JYdg!Tgp)%0|pnBS?Hc8v*H0?^bl4Toqq`Bt% zEmWm5W3S)XJGCi3bWoyXM>xbQYzUc`J-5n+HFJ@(!^ao-a&NFHZ--}+1`L&5q3>AvwAjH0^|W0+B9gLHf#CKf>L}96t>n-Q zU#~Vn{4<;4I6#3=ju@=B%zNIDRV?}jk7hG_@f{1zk=kO_;vK{C_+ z=ewJxZlN4)BC8YR;AvjRBG4fR8q~%<9^|hl`^GD{IRk@g&f@zhB3#ZS*LR~3N{JYO zM9tjD^v^t)b$q?cobNcxF8oqU-3rIlxTkT)0mcmrkH!-7keTCn*HZTY(3{sidfv)b zd(}qwxd22KfceF@Xb%!MCS5j-e)szDvz z6xP`C)a@bA_QS5zs^DeEqz_ZC-F%5`Pbj0Ccm1p~)9kVbv8H*gC=OD?Q4a@s-{^Pu z4M!H1FbYVfDuV{h3vGL5mJ>37MXMUVF`%~2F@nvI6GKh(2V_uML|2Dw@Td(DtUR&; ze0i^|h*NKsGZ4ke@Zu`0S&j$Ee(`Evr%K>q#?Kh zBr(0m`t$U4}vA7nw6ezZ%d9vcj?>t~v_Mm&Rclp%$q^wt{KPH<46Ocp5(JZHsj zt=l6HfH5=)8$PqHeVjJdTw9KKF~iHW9zYikG*LH~;_HBrwO4h=_In1@p)3(PzhNb$ zuR}p=LorhzwU+o2%&wrfhdjKjv;5#&)+`csSWRkfY=UfI&w+!3; zYR^iQ9_5{yJJny0jwnvG`P_nC1$9wBppx-F&qvW0Iq9Pm6YWlsBGY&5Fo#-_N z7o%qF^x!kLX~owp6J!(>gTHeP(%@*LfUd+$27+yQ0x^A}Tta!a@DZMb6`+SB?(wwD zW8S%Kk9P(x@HQT-#G^~tTC__*XVl8YtqubtZP&kzDW|p3{6%8b0 zIa8+B8_C-%L0LCk<0Dv-mQLut$<{2z!%WEV4yacXqg^u>tJb8%!F-}NAw?s5X9QQu z@T+hrx0}^2DoY)ZCf*m;(?F4%xo@X0K4Iau~$ic3GAS z(X|n_=%E;JsstZ$hkNpFubzJWv15R1{?=!O!~ShR=2hE zFfN$;5znw{d7=No2>1-Q`xN-V9TfbbUj}+cOtSCV#uvtx*msXIC%`0gF#I%RTrG~I zLl-)%qOen83t!Z*c5MMZ^G#eJao!EoM%6hm`e zK}B=!m!;KaO}~4AX4PZM0jo1^j7fy!&vs9xnw9phj;6w?GrC!}1~D7;=K89#Vh?jC zv(q(wIVFv|XrJMsn6{yz*Zd;qn4y%m2lguJi!#b;8|c)929{Lk1!@!Q6gI2X*&R)^ zGddfZ$Hof!Xb{b%ib{M>Ps#3L4KeVu}s&=lhglb@03N4(R+?tAByu6L6 zi*RnNL!!{px!hK#VNY79yeduc&eB@4mDk4RUT$BwuvBxl{#CL@z{av{Yom@2)oeu{ z&8o~2y1WG1Y-?8u&w{?}vc0l@vfALJH2xTayTz*LKK;n5LapkRy%NGBojHA0uuR}4 zJrQJ5c6&PfEdDk4fxZ`W1j}9NB_g3ro4z3uBY)J!6*Ibw6vz5@kf%6`Cma}#f#p)Y z7e1@XonKWJbRP6TA*d|ok~nIv=A{wW=mUISqsCg*D4uLZ0WYm1y}Pq_>lJ~|*Ba_O zdMPG#(u29e3wVRqf=&q5i%nXf1EK`q&`!E$ya|xJ>7u~fZ)DJ>bc+Y{xqm-s@)Ddw z&SnzwlO>@{C+rDZ_WXOt*VV*o6quZoS)QQMBkH3a4aUNQTeV(f>%O;`kz+DLpRcX{ z?TN$LUY{aUFN)QLre>U8Q%kO$`6mRgv)7MPVr@g~?0TN*V1c|d!M>HTKx5zxkp}wE z`yP!QTVjS~mdbcLgFT97OD%~4jK(*^P8xQJG8RvX4WZ&@Bpa+!Csv&nH>A18FTiYc zx~mnq&n=Rfz%f@n2H70+t8TEh_Wh4|zh@-&-YZxNg#+#hji5mbaS3b$UVirOAgjtb zU{rO%;aH0e5yd?oeb>GB2K%Ujq0{Sj@FM_i=;SFR`mtqlFrcA$7+Rm7*RV%MVu|u# z;|dj`^4arSPj^S*CT1zgQoJfU<*Lo7;B9QpOXX*(oW%Lm2nbrPj}_}D6|3q>YkL}< zlr1K*&OgaF4%Qd;Pj#;7dyh2A^(}cKE%Z9C0cl8$5sfTq0lNGEO}T5A;P z(=b$rEsQAr>oNdEAA7{8s?vVBspagbMOs~yOkK@$gLFveGDA_jbzDj3JWZNNeG{ap zBndnQDwG{w+u_}qYy!DK=^e8Tno8yY*bzG{zrDjgRaw9$`|5U*7&b3d=d&DQzN@0a zEpkiCYBbyz?v(SDoscQH3o&j1pOM6V8Hb4UAo#rfBAbG4vG{C(iE-&+8sV^SVCOUHg8 zv~F<4FbuY5{H69SXMfjmu0l+E)3FZ-=PQU4N^nkkPvmGF6wA)FNtb5^$(`>|^*b6Z zX3Kta53g1eOenw2xi(*{;jylwvWgG{a>;t2Rl^K{Tn|N;YZCi2l z!-M6S+SwcIrh;F|0;ThOpk}`U>A?_Z2^<;9P1fh9&3AiJoaq_i;DtbCz>u`+YCbDMSN&!}C zwMElN5FKfk;60SgwS(M-KXBB@kmu5|7)(ZnL@F-X~Mz^3@JJPJ1*B9prBDH0!Fc{(<>Wp)y^v*cedlbv5 zfTXvuy*1C>{PHIR$#HW&k%^0{F6~WJpLEub41V33LV!S%tKec7j0~zDOuh2^=>Wj; z<8Ugxse?Gsa;J+@43lPjM_oePdXp>1X9d^Ucc|KTu;F{U)EFTXA<8>oesaK;^w;UQ zojr8mXEq~S%tehqRvH4D(=@3Xwhi$5%6~wWLeQy$mpOPEe|^ zyF=p>?aHE@(m6G4p#yfr{-Q<3U3PV&a&ludsJ{hLk?mDHrcq&=AqV=zSY7cE|6`{^ zNaUOiZ-4ysVCX_mD{1f9L&JTL)UjoM%M=}8mORhF!oeV07tRE|HO^zYmC!u0hohcPQ354 z^pgn%G8pAp^{QpPbUOx|+oKWkddA~~f29bC+@>!Lhpp1McCz%cvsk9aW*V;BMK-;C zo**G0^x6WbFHE)GJ7K4Rndt zSgh;pF;%~|>Z-u|)ZSx*$px$o)K;c5@&|()prhye4hm7O+bb_e-|WU)T4=FLDejfc zprpJ%r3cVgNVkjRrC+|&J4ErOBwFibSzwaV0XI24f7+4Bd@wn|l++e=4BC^qpvV2W z?LcW}+ z6|d8@nN~+7bZ(;gMe?j}>&%?=!vjyyRlDC0IBBQNArX1dIn^zTkT*uHyt4;avP*+< z_!)A$V&S^v7f@@0Ju78fnl&9r^M2SM%)^Q^30;rv2LKfq$FYTPIg3zZ@bR~4SUfN) zC&|~_1{pBrPZninh`-;h3Y>K}mbDc~c96WqC+OUU*9JV`oFW%~U4bT%{=WBYWc9|^ z(0JYnJjPgKsJ{vmsU15F;+8HWOaP5ApTb8;GG^ejCM)xaHyz3_ZykIoSXR>>yp9-) zj~iTQ({U270RSIh!YFy+y^(YeIO>{J*O#Fy3&llvN|x5>3RipPfi;|IP*Hi_;C0N4 zbWd0rQu56EZGf;~YSCSq(8%%&lmxa{NaqW;v`|hntC=J`PC3l=Q&o1R697V0ny0ze#=#jMxd*`6twOnm}=J!$xNo*Kh9RK?ITFne)rPS5it$HV%XDKl5= zg6Td}-$CL*%TDg@Tgp@FYW|Oj@9dp?`{UD`*vnt=@Lay;nZo$mcPnZ<^t><1cfRZ} zofeO3{Por_H~c(}oj(u=CeDHol!W+XKGnVro1A56U3)MajGoEtVOqS_o8Ep!t5xk3 z-EpJqy*wd1^ZI{5|F0|uxCmTmf!{1D+ut1Aza^{vq2>MK-S)qsIW($zDgfYx}~H%mXu6P7w}ljKFBu6I8kP8@V}c=|qVZFgR6O|)9T z+%H3M2>`QZ_oYol37oN#$PwAW>U2{o`Lc|SnaPfA9d2U>O!&_Uemh+$!3o)?Y)jMX zQ{>TSje@htaQZP^B5L^LkPL|tJo;ZdkE#{SuP`|R^ zJ)J(#71Mn(@{v);Cou6T^{+SH(GuY=GYZjcfVRf^FFEAGp}`rs-7w8w z2j%KNN|+}Pjb!MiYXq)jWUxkWsv>Wl76~xgCf6OYsVc>m^BPaE8bH0LAI!zhcz<;6 z6BS-IMGJR9!JUAN#E~)$HFe?C;0(@~vHg&!$8->v5nSW?>H*vmJh+p1F`bi$$4Lty z=^A7bqF_1~oe2ovk&;lI4wkLf9KRufSHxh>EoU7eu(167#wCII-1l9IDdgHVTJQ>> zG>Nei26?fxbVOJT7=$OsXc(9wn&nP?wEB8j=td z@o5nt&0@r*L>4Ga7{+AoVc#5y&FhLp!>h-M=S+f+P}fv$A)+hf;kUz?f_V%ZgaZ#a zV0fSc$+Y=eaaij8fUP(QFjAw(qDVMS-G@v5VVqh$Z z+=kD}qFh9J9RsH8f~f~EzWb$w*}{rpWuB8$odv|5*som47{Xa=y{fchzQ9}O&lU2ZRUq#>>r3Y#gkEq6LSN^)up1)G()); z5WGfl0=zW@pviUC$8*Xdae@iTeZeAU3=lZIBkQ7!W{wl79YG^jZ4CrepJ0^2c&yNP zv{0M1Cyp(&wVZ1hX&Pk5YO}i%m%T}CN?m^}KQD{3S$%tR8m@2-m1CP|c(Q$PNlf}U zy0{+basT;t^vPurdu=v;USccl65_l4!2Z>G5kk)obF+@nN)2L(&2f>4r0Vst^|4j{ zAemS!pw^Sx|j1nMNo8Q=ZHm*DHLTD&?-_ACEKHg1cTP2Tjai{<7| z&pzk6iJHnBkQUD&S<7@J@{O)KEV)t%E{Yp1I|WbLvy`pap#m3}BkGsn>YuPImyr)K zvCp5ep}sezRE0H@%8#-8ovvc4O|+n&mu5auO<+dCqH@Uz>Q?m70YEOe~)|4JCWO;mhUPtj{t)Z#A?bjV8 zPI{uDjdU$8g5s9Kvkw===aFX#a2?p&7}Q|*(io_f(&wt5-5 ziUb|aT1CHkE;wIDI{w!!V1rHfBI<5L9M6v8oj*0O?Y?tEbIDUUwF8%?0dZ6 zo!NB$&S5{7(UDmX-q^1B%Yoa%W4;pmTXo3hC8$f{Ly(7<3wpHh`RtJLT>suS0OAD10rg%i$)q?(p_Bt z<++}f^D_?UVPK^b(`+Wd6&+dA0Mb)KeJ%Q#A{HgV(6x)l8)o1)G2CBi*;DC=(cHzN zU?9lD_>T*PaV_B=&{NPDsg4Hp2LMZXtZYK_IME}Ev1HM(6K3J|0lfk$`5b#>Y~TyM zX^y?!3^8beVYEPRzG*nqAn2X-*6*LKsMsi9p#Py_@rM}qn-BNz2j1Th<6IrgolNMg zY>h1c6}$gGYW*J~TLVNb#UDoFzb2-${2MX-Z+L8fNl5(HD2=}V4^tYk)YX~lhk=U{ z=Ob1Hob1VxevJM1cWDR9rypgtkN+L z1qO%K+0dc=N1j7IN?V{e35jADNQjv^;J2y)4w`fc9Qbb)4{J2C8!!*j^xyIZQY9Va zWnMN=%AW`mWGFAcl|8~Z&M-@S_*w4q=u8p=#!~%oK#|V_p{BTV8DBt?CgX^ZIspIj z_5Uo@g7(iS{lnY*4|M*~Zu!$W{;hV)A9VgZkOe!ML<^(}lLjeDc+4|ffbZs_CM#)9^ z7XnW@4Z(tS5C;fPC;K$7pAw)!zG>u{y!LAUJvJfb(zmt_~@tT+! zO$m7S_*2*R)0@w>=kPL;XBkE)4rG9Qt%f+ZW!bs8J;0MNkf1~oa6Sl$WD~Qb2qxRe zSP+~naw5mI|Da?Dhbv%H^Z}VZkZ{N+K;-;F4I8DOI+s2=!Q#rNw{X8!;yMg+{H!&s zCm1qpkw`Mp$fjy;YmyBoMYDv1V-GE-)3{NK;zt+|F&G6CvU)HL&Ge_|;Uv^}SD9oq z$x<8`#Dc=X7cIU!%XTFMGNkZk9id@984YZ>pOM;XAw>uU5!Ek;55ZilAmYAikN?r# zS4U;tJa5z8DIq1@At_yw(%qfXAfZSjh=O!C2nq-aBHc)LNq2~XgtYLxZ_!8K5m9{J zbH3-jJ)d*bKlYlP-JO}8oxS#Bq;FAWUBza6kBda0wv~LW8Qkae+`gqI#Nerg>DvK? zk3;Z7(j&1<`Z^;q@2_0p?e3=I##b@7n;PD-W*OK|cqjO!%u?ZSYj5RUMz(py6U0O_ z2E};7lJyiz+6M{9q43+O8w?AH_@!G+cdFF2xHNX~bq4SjtOs}>7b>~O@@}QMmn=6m zVyNcE-7Sl}R}p{zkoxFDyUHj_@p5(iI!{%pHoN=N=r5mrtv_J;*pWN}x2 zJrfjzu^f^KdQ;J)L9|%|vxH4=LupU`xj1frqYK&o-erVP4D680t`J+cjoQ zF^^$@kTcaeiK@ohrn5`g_Vjf!&&={Z|TCNsD8$Nu>LX5$;GH~zI{2S2LQq~IlQpQ1emHqz$bB8I6ET;0E>~C zwH2MEk)FMgJss2cl?jx=;@>M1=zRZ8*8?BOY>)nsdgd!#PfZ~GRNDUmL_x+>q z%s#nc^umhJ&QzRPOr*l_*BW(1rO!0(LV`l)(`;f~c4af4+;UY=%J zQWC%l@aW(n2|iB+g4Z*Lt8{_X@?WSWY3_o%q1oH5hoMU{Z){AnD7dqNVaDFTD&;hq zQ-bSoRpP^v zIsn2V)aw(wm-t!fO-0s}7(XU{OrS)*RD&hpiGWfao%1EkRA~7V*ZyY&aJ}+SpjZ_j!oP-=JzoyiP*`ZCrV%&c z)*q%!yQLKJi7Al#v^RlcI_PyvddJjX0l|#-*O!-YXI4DCsyL1;Qb~&bEU6iax++nG z;RP)u^$sqz>7&eS5R$eWsEBRQl5{X?RR~N65ZFk5NOl28nQCcf5Y86}<~)*^eU0?j zU9u#H)ROnof6wK3TIRaeX`5Lla zk!&ifm~2j}2CH3>k^nN-l3l87&0Bdj#A`L6d3p2*KuSMd4=+4v^it3t8&BXu@^603 zz7Ck$oPep#d&vR#hw+c);x+#(!at+yU-|w#KyHZ=)xZGg{@D2yR9jBc{ow-W{z9%? zi|w$>@3sWv+-(lvTx-V3Zhi;38{OGGeICF*&s*F)nK&*<=ARF4(4~Sg><$v_} z*`aD2J1c#b`&>tT(Z0D<;+KVoDn6e;F+0|j7+|h$-~Cj&>eIHMBFIi4qnswmMy$Hh z`9gaSqSeu;cv1OXL*>%d2HZ!>4e){nRXu<^kd|q`BgdfIEzXKNiP+f1E%z-%ABYKHn$@D}5?N*6qWlpNg8!29Ab-I}asC zL(j=Zv*=|T#PGGD<&O1Npp&Blm^g13$-EPVt9~POD^+sMCRd2@ zC7NxEGx)`y!l_LQzp&MfR{l-Mc1N)lG@j zV1|#JUOz)=Km?y8VAtq>Ins!6tZ{D=AxN*83NBG1BfyDEuQWws|WGazuv<=BZu zJjUnz-#f40x-r?E!=3-oWDrn`%s5HUXG8?PiQGb#7N2&6KE{tU$q&Csmxjka_=v>{ zRugWK=drh?iShOLV7V1o3Eaio=pfkm!8$AR(ZyzOQCgoJ;lJp5Urcxyy(1px%wZ$` zJo~oqoU)Y_;mUd_w_(@gf$a@S)>%vp?>0uM(SeDpYHt`Suzi^gTCeGM=Qeky63B$z z6K&2$8-Ay-l%g~*8M>vT7?B$}F=B)`3@$-IYXr z*Za6eTt_yZo55u!){M>}lu6sElGZ(BB(I|F^7o~5Hf9L4%b$=c6&pAz726r(+r72M ze)rzp)Gy;z=Wp&VYuV4FGS3O^f8d_w=sh>PBPnm_XhLUTBqwWYLB}c~V(lUyPD#5 zz3cV_Lkbk@A?wr?cZ72-ETRez|z-R!O zwNHl|NMVH428n}>cgF?OD@k#X7qh5roD?xlS%v9|k~e%A{&SBG>a- z0R{%Ot1jzSx_~1Fk>%Qqaw-x7h)=8;Yj9)NUECo&?F8v57Z4#F4Usw;qS!8hnVnW1 zE$LBCsAzqJphB`wk@HPausagkuk|v_i#!NBTScQ^#8aSs`Z%C6Km-B{bIrpBtUp!i zfftEs{)Q4&cr(a+nVXRL5Pvts(A2GDDts^+1V%r82*yQOKTnPVS@{{e=dd6O2+Jt7 z`Oiaz#pVOa?9=rNgsJK7h?v?4A}g%JJZv>YGU6m6(%dMI)hz#P6t&zp8!8c1$Dh2@ z+bk~ykVno zP%fMM4s(h?7@HkL{6RRQPg}w=tt~Q(nOoa9R#LM5y^hN(>a?)y+Z<+3TO5fRe1HjA0L^``w@isAyBI}B3jg&+ zk`!!_jaqHK%LM-8prhH5*X&4=CGXrecdH0KRvBpY??jT!Zzfg}dcWPFc>FZqucI+K zDqeQCZ~{iRbtui^QPUIu=6#AjXlL%G&w6jk2Rq_fVK-)y5>_~{c^TinBr|ciC&hZC zxsLi!H?(+uA|!}$Zkfb_U!=6Cl6=8Dfj8I9z;YRz+5W*%Xe5sDm>}8yd}?~ty+_zb zg|XYgk^5R!ri+AVx?wWP^GB%lG{sKB-0Bf(t#W>=k1U=ex3*y1fuNPDmZvj#4j4ZB@Zf~?h)Aw;cbt>*VTD##hCg{nI&Y(Oj~-QTgzY|uR?qWSVb4Pz z9~>Qa`#AaFpy)FWd?`|v)1Wf_%g8MnnOwbt}RM49!zvL7`>_le%QJKx1MfXv2I{#^ojH9#y6*f9O`J2#Nip$U}19Ps@t zne;FI@&D%uP5CnkO+TgFhTRO2=4@UfZ%kdNqr=^OkH%SS1ldo!0@Vs9@(IAOpnBV5 z@xp}WMVdO3&^*Ui|9(QV!dqUV*D*{1931YIqjZJl291{Hqn>{!G)skdDg2_|VnY~* z@A>bUM3fpS&o4**lh8z|S}B6KOhWVA{(es@{$?r+m@nYsL*%(Ip?NXjcjU_@H2E7i zyJnBB0(ssu1NIm9aZbzvppbY^VT&(JX#T#CmrQ8BOC*jU_JjiUjL9Bvh9bj)Boo1T zOr(aQYdKa$h471kJU5|vuE>9y&`f?5qkUo8?E*BN|F9Z??teJ#h8u$iAXttArriXy z^e;iMT(ctIeY|duEjln6R6L>C-3o9a5x>ePUo-$eGRo&{%>FE+tiTGTeIlbwR3k-c z!VvBROdn?(8v4L>%mn)n8S9~0Cr5=QAw(o0M<5G6VQBVQI)aHv@*&p&K{Yn_YjN|W z&Gmpw^o8T{fRH4zG}g`vP_hX_;HRhZ9bLYEPXa?V8G&WJj@wUGBJ1LWnzg%^ z%;*Vc$05WD39c0iT@+w|+b7cV^&JvwUDcd7b%ZAtI8c3}s4492f2%&Ge-iTljrts+ zw7;yb$5d}Y|=_%XRV-qIjK^`$H$jVgZ-xZ`y z6vj-9uPm8mjEx~pwc9MatDaR(_$(zxR#HtyXl~zwUfdJzrHCLT@F|RBuZa{dB%UZraUSCWbvH9nM1?DQc1ficL34sW*;CNb6d)mB-PmVnV5@&<87(aj;u{%5eERm!Y zI{sFHET zE9DU@VV4pcoy2b{>IRs#7+FNmnHB9B12X|7Gg)_W6nSB0xwE8mD3ZN4_{og~hb+xx z1+YVi6v;-Yvvm2M#WAO>e~6Ub6(O3DOa`1%Abk|v;88?3q4qIm7Gx;|A?e_`YMwO+ zeCj3H(r@M6W8X7VP>72+0uiu!fgJSbo?KkKg1?*}fQsvWBCYO42%p&RFC;G~Pd6vH$T zNU-!r2_j%WrvaW5)Ccx7&026@>jT;LvoFAe3aC)-*<^@@8-Tn63KLR6V zS@0|4{TzK~6$sJdJhJuhs0+3q#^q$WQ9lhW>z=6k^ zdIMEL8YZMK9=MQK9Fvxo-8v&fIWo|;Rce421U zI%5Ewv&EkObqoba2p&H*^!En9@w)-|AAD*EHzt+|aCG?|_|nkj9;ra;^j$bH0Kh1p z8ny17Ho*Em0UWBGo8Z2{0{`C8WkN`&L>)hhe;B?c3S^2tLt+s%T>xurq6D)37jJb$ zl2n8d!x4IEHrBEySa;zks-H7VX*_@q_PQgJXx@`YG?K{dueB4s>$ITawgJyomjI&B zfguuedvasHw5MjMW!cE(^_#aAC1NXec4$b9jPmg$DIw&pqMMRqj0ddPgKDez^dBk`Mi$iRqOW35r;G|R1 z%`sOBLKHaE`3JHeKoh5A(&(HiSaIbud%Y(2}B9CS$SnM}5I9ZRl*FXK((Wq|rH zINdv_lf&vh`vvUBfiy6*8^_1W{Oe`K`kyVUUe*8MWfiL$3*4Gqep%H#5@9%v=gtzhB#^a8m%E-Z zOCeOxnR4CxYvlbU9xrA6wyC?9dW#WK1oF*Z9gv7sZ@}iJTm=-f92C>xYh)7MZX@wA z_w43G(suvFUprRdn>9b4i~MT^D$sq6t-lqbxAmM3t`lV)r)*r%L+%2KgWN&C6$hE~ zZrsSa%ih%!4hIvLfkwUP0Voclu0p3?h3ruT(|g4%1bLKPf?^3o`qZcdFw9v- zAM^Lnu*QOr$ov4O+bE=D_mUs5Z&4Jy*I5EXy%_wyOEdc5CQSRH-Pf8RbtoExe(n=? z9XJ7#(0P3g0D(y|;JbNVNy-IwU57=7Nk_`jz{tvogAq`AW$1MKYi$Zpk>@FM>D!Bb zlc7oYkL23NL!$xDFE#-R)Hct&PT4=p6y%>B%nm}WYvKWBLD^ncT@7o*d;GsTAJeTck@Ri6|SSO}jYR}Zz zYoz01uk+I0f|VwrlCV_s4edj)uXQEpQ*;+Ij`*3FNIiQ1cZy+LDb37!am*+P#LJwJU^hLTS@y^Gw??+d%exDT z)>pKiK7+KJoLYo^#cZOCNSDn;O|fW4)O^`F+#5%#%=IGL? zA*$YISsi8{&;=lRDD^Cv#Rp^rRI!Efkz#8dK!}IbJ)@roa}!PHh2?eRtsL3ZX?d^C z5>7m=2G@4z@$<7tysOXMc$4oSO*UwZzR^bFtx>y`XqgVF196|qHnDzkm9~mP0CU$& zvPQJ+Y&}J#C&$#Mw?t}%CbUE%maTpvVM1tGaCQxtq7n(e68)ZK$f<&+nh!f2%xDIeK?J zPP3$xerwgk%LWw!LL8L{V@f2&QV&l9UiF3|nfjtP$+Kc6i6i$#gOwOFEmfXvhnk_QEv1KVEK#a6Ig!)yBFhp)E2fCShU){SNipXI`XCQxmRu>B zdDsd&Pu2}tn5KUG@Z-la;hUpy!1R&P~Ib%)x9Ot^yh{gQY6`f@L7C}HMr zb0@J&g@W#&+zk=o`M@c&ne~Fu+n=#7D(Tf`pgG-~c$i`rx(nhLxL4+w`5-%Cge?rZ ztpkA9Rs^jb8N5KeK%p3T_4NoM8k`_U6EnoDpAtYc95IL-njSdp8v9J>RNbHXcyz{T z9NO7Hqyshra(`okfEmFHY!HuSFF!`*t1&i=+M^d9NXnjpNz<8COY*RS$z7qTP}J1K zt@Tm*tOdO{o)&_oBP8(agBFlD>gnq0=`DLefImR>^ob4`g5iS~%Npfe*osdg2Nx@! z+g%yoUn$!juh{4sY0LX~Z5QLE+zY3k=K1*Y+l>f=Ac~b$`(54pI~2TVh-6sB_qiba zvOYf!{p^8dFCP^oMSyBW9}!o)7g>tc_<)G}VVi-=vnZ~Wdr+FGDW6|l`Cw%=%0939 zT5=kx1J_EL1jR!(1tOXDp^c2}G>!L&y4P~=G*(M=ozpOJKcyFzTCo(7Kz^IZFrQ!k z!u5HS!Iw`tB03763dS4D(6NNdJk90I^2AC=#u;;>RC)t8KJS(b@q)TkxwpgLpAlog zey9c4S^ExZhs2Q~o)a8Ju!2}-_r{djhtFz00dqqwe8ZUU8v7BL<4A{Q>u3wM85;hrXW zzQ}Tick+r2;x?Cz-q%}(wv-DoqBDN1Cuj5EzM z?+tu#1$*ZGJ=8SXyz6O>sazzfj^$m}O3k<1BuX6ZXpTHAZfaS1%a+2N%_@V@I(A3HIjCFiKcZAs%E znZ(OI*aou%Kqx;M}3rKq974fumiM%{4{sy+w&K1=c!Ji(T|v1 z8+-~GH=y1z=jX|kMxh+Eb=RHn1E@xtZ)n7s{BTe8blgw8Fc5(G;}TJSxdum z7&Mlr2H?G6S|*`OIh>A}F!TwX(nu83+3)Dn+*WoBQ5{74(x{JSWTBVnIf5TQ`(UdA ztt4S;R`f2rxPFGuwb)DmpATPaXdC+Ub`p1C9tRf?o2>nfT>vJ!f7peeWVyaq-NtAg zT_;h7Br}6SY?2U>Q@=Aaw41Gn*01c-+gE^w-V6Uc_YQt!&YiI@;+im_!C4}hV73A_ zu!4frkg&w0uGAD(5gedT_%*XoDxx>5J5!8|c}7--m*4m+Pm4bg@IO3Q(iC>)Xe-SqefmxnE6v2m=3-2g{=14?9W0YvN)() z9;BDX#5iT7_N9Q3qy}_8j70+V*si{*`NHi6k}!7ZaLFeW(fx+l6hO|?%TzBdkU;!O zDf}2)OZ2sO_EVq_0`Z0zF|}q0QQo8FwENbN?ykLMAk=i7>#*q58pSD7O{&-$i|{O~%;fb`3V@)N>7E=t zkaz+Sz>l=jC5SBG;4K7$rDr=_)iIX5toOjFx%9r&PLlEPfm1eNYadwe$oOW!u6IY* zjV6FHyD;1JaaOs=sm#-Bh&Tk>;CBN|?~4)pd)Z6gD!OTm4QoU=-OTrVRiVDdqqz+! zc6oB>jr*1EDX~1>OaKqoBK{)A9b4vCV~(hmNUOr-ALiXFIOoS+F!&<9t;Q#>xPq!T zQKl8_dAk7J>!mHyK_B25h6!jr?_fbEdi-8Jc{y5%(4~NZa(mL&&A_OR)~&fQkIWWo zBlr(viYdhquP?3uEio%*Ia84{DuD3O zt5yC3AETii({h}&Yf=3BF!d8YxivjOkQA>P+DQ8j+TJ&NnGNq6vg$G9NV6N5Wth`u zpasa|CMmtu8Esn#TOQ&~Y@)MZEyBTL$TO0E+o_x`Pr>pW2gz`rCqQrN)r<1E`$xt8 z1G%I7gZ|0|3pAsl`^66`ooJ#hdRe442#!!Dz}xP9^eVOMF<9Lz7HtxOm?b1vjb8Th z<1gcE)~H;`pfho+cDjs? z>+;RjjlN=;dq-dElYSigwoY9qO9VtI;(ud}fQJ@8tkF+*O46nnOc=L}Uw>53J-XjO z{YH^Wb$RFrPJJn9Z&!EM@qOIKGY-c#$+p0`8@hC;UoA}{XaX74hE;;q3K`igl$;YtcdtxG5) zGg}1PK!Aj{2U!qIEp|xdV9J;A4N-L#)`&J#ytz3xm<$?z?TFA6z82rYIV@}CAftsb zx4{NqxC}P)_lxrVqopE2*fQg{7=!3QMRj`kIRSJ(Z=V!%*+B}fy`vB4_i%5w^aZ2! z^+v)L5Sghx+~L1Hg{^9imgctka)a|>9^C^g-JaKsJzuVb;Z8y3AfSr%NW%Z4hhPjCapUcjw*pi;-IGxH_Oe*35%wgcy0WtdjQQ`g2! zWEqY?_{^DW;s#0goEjH#rIByFsa{Ory(xyTfkTm!zMzS+97L=jxq#wZSTF?^16$$U)k>=5@=SZpN;miw|CA zlnI%j^H%?C>Ev~lbmR#eI%Zno!$Jk#?cS7UW34X@gcEWC%W*ZE+=);+iMr8f2)Y`^@qTWl7ZUT~)J zu(3+g@KA1J;1e~u?j{7V^g5ZWx+yvF+K64cd0y>|Ul>^tNlHjhAzP*1&&k zK6RUynV}fnQqKo%MIWp0wQXm*}qNA(Bwj{G!Pc4<}34%=OtlxI;o9JgV zj1sct6`-go|k&iN@&Wi}A~?br1uUyza)!15$8+DMj0Uo#e<=*^+XGOF0cj z^xmFWn6H^~gFK|Bx>0>%Of4OZ=%G%)baz+6>VvC~IG(P47R)6UYRDii>igsqxE6=@ z1mdBNn=m_kls&`@{@9%eXRc8+CvCTN=BrpSyp;z`Z==z8Yjq~nQoAdpRD=XbwFu_M+rBq% zGW(*lao89hB&F>^+p8gzoYm_<6SzB zoViz43td(1t1LJ6yqJYOrj$-|%v0gDc)of!2meOnqcUw0{zW8+t2P-ETpsW<`tQfg zSI~P0Ls!D})l{TY$&tx!lwlUzCyjD)SZd<-+q(scv%5JkG%nR)Ln)AP_k7$6PNYJ* zVeT{w5yKm4YyYmVkef5+HGR@b%#vf#Y#wQG*evk~8Pa%!A#{{H`hhy7`MTemVVQfa zIsu<^+>=zv`ZB8Iw56X#s+sj`P*y(f4=19V{IX-3F3vmzfm4@k+|Md>#Vk_F+$%=} zPVc#ryo(T@psvywjP*y>4Tn$evekvYkb_|0ol=Q!3=)H*2=cGKDtH3^X-2~u#1fHK zf-AJWYu^YZ@*0zBo^0bmVkt^eM%?zk?*~I?KI#@1mNKWC;2N`nSg%B|ib@Jt-7s`* z?dgSJ`8Pc!9+|MJ^G8lF?E#S~JP9-l#!TD#F9HBXs4i z1oTY0Mpo$)KbQL&ZKM7Dd`W&-%$p`yaC2}U9wsojzpY=ecXr!CKD|ZB?`RbW{aX_N zJaDgt1Le zWG^r5;Ka>ErEYJpNr6q$X#wk0SvAy}GH6sEj#n{7uf$(BXG|jNX&)VZJo>`2A??R& ztPw|HXJ(knYXpG@&tYP2+pkP!&c~PQxXE)Mjf5;pPI&!(6)mFN>X^u?=OM&^^ayT4 zz3j(Vltf;*x^nBEoa48OK|+v4__gimJ`{m?=*zsHzb)g5yZYAU#_qG373A@#accHN zmdOh4grUMmusx!V;r%&s{VzFTOSs8kRzb31EjMz+?f4}jtGJjX1#s0VE0)p&}UVPGU$`1BRc&KM+LUoYH}MApGK? z-i3ZkB({domHKS~wMf_?bg`vwHNmf|_Q;V3-JQ7FSsMY8W)%iFJ09_+$nHNAI}#NX>OR zz)b8;+;qU2)*zFD5KY_j4|Wa@uNbUyRWb!BKkim3*Rr}^wKg|MbM&5QHbYmc9FB%y zQMNKZqIig}2eo_$5~Eh%$v0|$90E>0*{8dlgxfp+g{}wcwC2I=Qo7W5^FfZDi)Se{O}nPRMF5Fn$kn zJ31%^r!6ZgI}{^Qp_IJFk;iKv?ZF!X?azU6LlL%d4^*BLvC~8~FVgTosilIa%alUc zVZBqc?Iw40{S`?m+TubDIEtAR9m<$&vw5IZ9w_@9OAutolk1^ma{eUQWWuyp<5Hye zsP9LjIEChtd_}2VPf*zHUipR@ zWI*7uSy`ZPb0@zM>JtqRk3muwi_m9YBdvP}v!ZvD+elnLf8vR3$a{H2viAN+=s^Bj zEj^xnp8m(pu#LoLkA}L9P?v1i8_@mwcIYsIE4v zZI^n(J#I}imtuK9)$%GARagWH#>k^joF+S7dF=s+$r2v#2A7_EX2lWkL5+ZZe? z@&(b%&|}G>*FP0znWKQb0d?R6SiKO*6|W!>@yczKCky&2L_JI(CpKZ7gUzdF%v|# z0`o#@N#jtisUno#bntgBbm&!Ad41o1THS#<)c~Kk;Wk8gmUv-5nXb>a4dDF>doamN zV%!bC0CYh-A5&168zf+D?bB=qDZFJZ3fUiBcYWl|^xbw|X0wSZ;dDM~v4?y?Fn*NC z$u0V2dswJ>FDk${%OyRyQ|E4q*nW)2#GC9T)s8h-3BP6}*`^tA6m>6MiqYHRSDQeF zj3pJJh7|f^L3`P?#G4t7S+$%J?21f0RLr;EjC4YEK;LB{?i7O(Hm*?d1A7*ZyRDJzcQMfb(y45FErKfjrK=`wNuST6}NzxSnsNEQbTxSddT4^Qk6-7 zTs=a|QK$5J-OpJpNC2KsBU6{|-pY$;$zAuZ57U0!+;l&4b0?5dXU?@|QCRXWUlL zoF%F-z)>Cn3NyR_?wI>83Q*{ct$tLUoLAjFdt8xWJQ*1i8#Wyi6{*wqvvL!|>$AF# zJEin=^$mBBA*Eh|W{c=*f$lbt#keeeXMHi#4Fm^NL&4G;HDs8X#b5?OA$<7GUxI)T zWK%b%TdKJpR)=OUp->QK<)Lu3%~@0wX?({kwQaOMwPh!?FIAH~Ruz0*|3*(M% zBMYNVebg~+SwUCRKwo)%dVC*-GP$U7^ct2wdip^kq9anw95r!y6{IVF_<$QBUZ8MX=wK>mE0WY`h0=rolV0*~Dw0^wR$cvv6-kfgfE8Dd*uUKg z7#)4hO>kxok(B#C)C9<%{GayW#~S`guKnv3t3Rq7JMPDE4rW5ynI5&T1i`!#lgZCJT}@jR9)S({`Ibi z5yI9gP5aj*^|2Pj0P+IF{O5O(yk{aFj-SgbD+vll0Dg=hdIkX!o9$*H3k6u#2lOj(9?EbcnbhX05nJVkLW-E2mtr-&j5bRV1uvvxu6%o1=0hu zXQ%-HxefS;Pl1pEVzQrs+}5))Gd8k!pgZOv1@r~DQhqvBj+F%**;^G#Z@w!F_Y}V9%C>H#GwKkJ4L6UWyO=Jy7hdFD19GN2`H zz$-C$r{t#qFcqEG{?`=I#Q;DBwNvC%ecy`UoC0_VSi3(3aQH6&xy#^(9c)2|l9U4s zu^QmJdaC&kfGpV0ng_0m-yUM%O0!d5XnH^o0^-hlRz=|mKZiJ84}dHhKt&g<2cU{h zztVGZjBRh60|1;u$L*i1qn~>qf>Szz1cYlp)6q!_4CuoJ>j0>v)2|Pl?1R=h7(gWf zL+y)o1Z*Gr6!-qg_N#<{-Ur}1_`cB2wHH5kA6Tc9^jzc&0B{CE&!xrxb3~R?i1~kr_=kS!G(rB!DRA}X zpLO*c@G(itF8#sAxNc<`0NsVj# z+PVFk1^qiT&{03TEc7`PLICw&AP-Rer@6*Y%F_qfbDlC}7fkklf})c|Jr~b^;GI!` zqxt8@ergWX1FqxznGoMOY>uP8pHzjO44r`Br^&!iYJgSx(%|2vIA+oTXyG>mIZgO| zQjp=l7vvwdoL|Xuyh6^7$n{er^61u|D&;#3&aaSvL}#a2y-#Xk|B}exIlck-=h~wm z_@}wAPvR>9Hiv&U7ms@m!2jlyFMtOs{xtLRN%#RkOm-Rge*%4$uYth@6A@?=PIDrk zl!0I2ax#1!g5%)oTpb)wzOycY_n*lJw2}FMn|YVf!I|Le0vB<1dwDttpj16ahI8)H z{d!w+-0QQ5r-OjUe~5U}C7m0~9?y6n@M#{^lf%7#IpAXlaLlR-Saj!#@bkW(4g%7( z|4iGzx7+_WTj2L@1A_nroj=7qsd4pR2LbitcT%1V z-Ak(COb~Fv960N8P6q)IdVea!clZ0-LBMGOy^|V%(!VtLcPaiP2sllWcT$k&e=o>? z76fP;{Hap@EC@Kwgm+R4ZI?v;#)}6qN9P*;GvUvXl7jCYQwj#!B{k_@`OCPU0h*T@wGF80CE>qO zTK!)4Pjf(>#P71WB>p!>sNcgs&7pJ>9|e&5{b~GlPVB?T@GS&BW>mTu9_R>8GYFl8 ze`0kx_&*PUPxIZJl)=R2ax&-~v)^2-0$>Dun(gHz{G#op;lF3@eq#lkCQCVqPi22e z{69L*I88Qk65q_>lK4NP9{Ih&IL!)jQW$B+|4JCZiTv*gbDGuTq%gQnmlWn3$H(vC zpT5z556^nR@n5@N693yh;@`tReOK@#zOeTt@xR?1{5|~BHr(V=&!o}^NsT9)W}-+<>1exM$X-+{fLfFS3;gtfmy`mWY9UTi~N-ee!!ou13U@; z9&l=WsptgY8;2H~+3MI*GM^0oEC-0<1UsSJek36!6gJ7T}L1 MDF}#0BJf}T2dHD44FCWD diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts index 91d86353b48fac..83d97555a47987 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -53,8 +53,7 @@ function createRoot() { ); } -// FAILING: https://github.com/elastic/kibana/issues/98352 -describe.skip('migration v2', () => { +describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; @@ -78,7 +77,7 @@ describe.skip('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: 'trial', + license: 'basic', // original SO: // { // _index: '.kibana_7.13.0_001', From 412fe40d74e6e6c72f828e5548826c5641c66331 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 10 Jun 2021 06:45:37 -0700 Subject: [PATCH 13/99] Uses default distribution for rewriting ids integration test (#101813) --- .../7.13.0_so_with_multiple_namespaces.zip | Bin 56841 -> 0 bytes .../7.13.2_so_with_multiple_namespaces.zip | Bin 0 -> 53705 bytes .../integration_tests/rewriting_id.test.ts | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip create mode 100644 src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip deleted file mode 100644 index a92211c16c559330c906aefae1768576c912580d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56841 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 diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip new file mode 100644 index 0000000000000000000000000000000000000000..e4dce85f15e38ee130218b7399483635cd197ba2 GIT binary patch literal 53705 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 Ldf(_j_IiH-?bC}? literal 0 HcmV?d00001 diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 0f4085f6186be4..759a9572d733b2 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -89,7 +89,7 @@ function createRoot() { } // FAILING: https://github.com/elastic/kibana/issues/98351 -describe.skip('migration v2', () => { +describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; @@ -114,7 +114,7 @@ describe.skip('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: 'trial', + license: 'basic', // original SO: // [ // { id: 'foo:1', type: 'foo', foo: { name: 'Foo 1 default' } }, @@ -133,7 +133,7 @@ describe.skip('migration v2', () => { // namespace: 'spacex', // }, // ]; - dataArchive: Path.join(__dirname, 'archives', '7.13.0_so_with_multiple_namespaces.zip'), + dataArchive: Path.join(__dirname, 'archives', '7.13.2_so_with_multiple_namespaces.zip'), }, }, }); From b35cde568b6d25944ae94d41460fc0691e3abb9c Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 10 Jun 2021 10:09:16 -0400 Subject: [PATCH 14/99] [Lens] Formula editor (#99297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :lipstick: Hack to fix suggestion box * :bug: Fix validation messages * :bug: Relax operations check for managedReferences * Change completion params * :label: Fix missing arg issue * :sparkles: Add more tinymath fns * :bug: Improved validation around math operations + multiple named arguments * :bug: Use new onError feature in math expression * :recycle: Refactor namedArguments validation * :bug: Fix circular dependency issue in tests + minor fixes * Move formula into a tab * :fire: Leftovers from previous merge * :sparkles: Move over namedArgs from previous function * :white_check_mark: Add tests for transferable scenarios * :white_check_mark: Fixed broken test * :sparkles: Use custom label for axis * Allow switching back and forth to formula tab * Add a section for the function reference * Add modal editor and markdown docs * Change the way math nodes are validated * Use custom portal to fix monaco positioning * Fix model sharing issues * Provide signature help * :bug: Fix small test issue * :bug: Mark pow arguments as required * :bug: validate on first render only if a formula is present * :fire: Remove log10 fn for now * :sparkles: Improved math validation + add tests for math functions * Fix mount/unmount issues with Monaco * [Lens] Fully unmount React when flyout closes * Fix bug with editor frame unmounting * Fix type * Add tests for monaco providers, add hover provider * Add test for last_value * Usability improvements * Add KQL and Lucene named parameters * Add kql, lucene completion and validation * Fix autocomplete on weird characters and properly connect KQL * Highlight functions that have additional requirements after validating * Fix type error and move help text to popover * Fix escape characters inside KQL * :bug: Fix dataType issue when moving over to Formula * Automatically insert single quotes on every named param * Only insert single quotes when typing kql= or lucene= * Reorganize help popover * Fix merge issues * Update grammar for formulas * Fix bad merge * Rough fullscreen mode * Type updates * Pass through fullscreen state * Remove more chrome from full screen mode * Fix minor bugs in formula typing * 🐛 Decouple column order of references and output * 🔧 Fix tests and types * ✅ Add first functional test * Fix copying formulas and empty formula * Trigger suggestion prompt when hitting enter on function or typing kql= * 🐛 Prevent flyout from closing while interacting with monaco * refactoring * move main column generation into parse module * fix tests * refactor small formula styles and markup * documentation * adjustments in formula footer * Formula refactoring (#12) * refactoring * move main column generation into parse module * fix tests * more style and markup tweak for custom formula * Fix tests * [Expressions] Use table column ID instead of name when set * [Lens] Create managedReference type for formulas * Fix test failures * Fix i18n types * fix fullscreen flex issues * Delete managedReference when replacing * refactor css and markup; add button placeholders * [Lens] Formulas * Tests for formula Co-authored-by: Marco Liberati * added error count placeholder * Add tooltips * Refactoring from code review * Fix some editor issues * Update ID matching to match by name sometimes * Improve performance of Monaco, fix formulas with 0, update labels * Improve performance of full screen toggle * Fix formula tests * fix stuff * Add an extra case to prevent insertion of duplicate column * Simplify logic and add test for output ID * add telemetry for Lens formula (#15) * Respond to review comments * :sparkles: Improve the signatures with better documentation and examples * adjust border styles to account for docs collapse * refactor docs markup; restructure docs obj; styles * Fix formula auto reordering (#18) * fix formula auto reordering * add unit test * Fix and improve suggestion experience in Formula (#19) * :sparkles: Revisit documentation and suggestions * :ok_hand: Integrated feedback * :sparkles: Add query validation for quotes * Usability updates & type fixes * add search to formula * fix form styles to match designs * fix text styles; revert to Markdown for control * :ok_hand: Integrated more feedback * improve search * improve suggestions * improve suggestions even more * :bug: Fix i18n issues (#22) * Persist formula on leave, fix fullscreen and popovers * Fix documentation tests * :label: fix type issue * :bug: Remove hidden operations from valid functions list * :bug: Fix empty string query edge case * :bug: Enable more suggestions + extends validation * Fix tests that depended on setState being called without function * Error state and text wrapping updates * :sparkles: Add new module to CodeEditor for brackets matching (#25) * Fix type * show warning * keep current quick function * :sparkles: Improve suggestions within kql query * :camera: Fix snapshot editor test * :bug: Improved suggestion for single quote and refactored debounce * Fix lodash usage * Fix tests * Revert "keep current quick function" This reverts commit ed477054c5560b50ede4245106954509617d65db. * Improve performance of dispatch by using timeout * Improve memoization of datapanel * Fix escape characters * fix reduced suggestions * fix responsiveness * fix unit test * Fix autocomplete on nested math * Show errors and warnings on first render * fix transposing column crash * Update comment * :bug: Fix field error message * fix test types * :memo: Fix i18n name * :lipstick: Manage wordwrap via react component * Fix selector for palettes that interferes with quick functions * Use word wrapping by default * Errors for managed references are handled at the top level * :bug: Move the cursor just next to new inserted text * :alembic: First pass for performance * :bug: Fix unwanted change * :zap: Memoize as many combobox props as possible * :zap: More memoization * Show errors in hover * Use temporary invalid state when moving away from formula * Remove setActiveDimension and shouldClose, fixed by async setters * Fix test dependency * do not show quick functions tab * increase documentation popover width * fix functional test * Call setActiveDimension when updating visualization * Simplify handling of flyout with incomplete columns * Fix test issues * add description to formula telemetry * fix schema * Update from design feedback * More review comments * Hide callout border from v7 theme Co-authored-by: dej611 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Reuter Co-authored-by: Michael Marcialis Co-authored-by: Joe Reuter Co-authored-by: Marco Liberati Co-authored-by: Marco Liberati --- packages/kbn-monaco/src/monaco_imports.ts | 1 + packages/kbn-tinymath/grammar/grammar.peggy | 44 +- packages/kbn-tinymath/index.d.ts | 6 +- packages/kbn-tinymath/test/library.test.js | 31 + .../server/usage/dashboard_telemetry.test.ts | 17 + .../server/usage/dashboard_telemetry.ts | 23 + .../expression_functions/specs/map_column.ts | 6 +- .../specs/tests/map_column.test.ts | 6 +- .../__snapshots__/code_editor.test.tsx.snap | 2 + .../public/code_editor/code_editor.tsx | 8 +- .../shareable_runtime/webpack.config.js | 2 +- .../lens/public/app_plugin/lens_top_nav.tsx | 251 +++--- .../components/columns.tsx | 2 +- .../config_panel/config_panel.test.tsx | 8 +- .../config_panel/config_panel.tsx | 70 +- .../config_panel/dimension_container.scss | 15 + .../config_panel/dimension_container.tsx | 29 +- .../config_panel/layer_panel.test.tsx | 110 ++- .../editor_frame/config_panel/layer_panel.tsx | 86 +- .../editor_frame/config_panel/types.ts | 2 + .../editor_frame/editor_frame.tsx | 60 +- .../editor_frame/frame_layout.scss | 19 + .../editor_frame/frame_layout.tsx | 28 +- .../editor_frame/state_management.ts | 6 + .../editor_frame/suggestion_panel.tsx | 3 +- .../workspace_panel/workspace_panel.test.tsx | 2 + .../workspace_panel/workspace_panel.tsx | 13 +- .../workspace_panel_wrapper.scss | 8 + .../workspace_panel_wrapper.test.tsx | 2 + .../workspace_panel_wrapper.tsx | 73 +- .../public/editor_frame_service/mocks.tsx | 1 + .../dimension_panel/dimension_editor.scss | 36 +- .../dimension_panel/dimension_editor.tsx | 270 ++++-- .../dimension_panel/dimension_panel.test.tsx | 111 +-- .../droppable/droppable.test.ts | 2 + .../dimension_panel/format_selector.tsx | 96 ++- .../dimension_panel/reference_editor.test.tsx | 3 + .../dimension_panel/reference_editor.tsx | 10 + .../indexpattern_datasource/indexpattern.tsx | 5 + .../indexpattern_suggestions.test.tsx | 211 ++++- .../indexpattern_suggestions.ts | 74 +- .../operations/__mocks__/index.ts | 3 + .../definitions/calculations/counter_rate.tsx | 18 + .../calculations/cumulative_sum.tsx | 16 + .../definitions/calculations/differences.tsx | 17 + .../calculations/moving_average.tsx | 26 +- .../operations/definitions/cardinality.tsx | 17 + .../operations/definitions/count.tsx | 15 + .../definitions/date_histogram.test.tsx | 3 + .../definitions/filters/filters.test.tsx | 3 + .../definitions/formula/editor/formula.scss | 167 ++++ .../formula/editor/formula_editor.tsx | 791 ++++++++++++++++++ .../formula/editor/formula_help.tsx | 469 +++++++++++ .../definitions/formula/editor/index.ts | 8 + .../formula/editor/math_completion.test.ts | 386 +++++++++ .../formula/editor/math_completion.ts | 594 +++++++++++++ .../formula/editor/math_tokenization.tsx | 66 ++ .../definitions/formula/formula.test.tsx | 192 ++++- .../definitions/formula/formula.tsx | 64 +- .../definitions/formula/math_examples.md | 28 + .../operations/definitions/formula/parse.ts | 24 +- .../operations/definitions/formula/util.ts | 273 ++++-- .../definitions/formula/validation.ts | 157 +++- .../operations/definitions/index.ts | 9 + .../definitions/last_value.test.tsx | 3 + .../operations/definitions/last_value.tsx | 16 + .../operations/definitions/metrics.tsx | 43 + .../definitions/percentile.test.tsx | 3 + .../operations/definitions/percentile.tsx | 18 +- .../definitions/ranges/ranges.test.tsx | 3 + .../definitions/terms/terms.test.tsx | 3 + .../operations/layer_helpers.test.ts | 79 +- .../operations/layer_helpers.ts | 57 +- .../operations/mocks.ts | 25 + .../public/indexpattern_datasource/types.ts | 2 + x-pack/plugins/lens/public/mocks.tsx | 3 + x-pack/plugins/lens/public/types.ts | 11 +- x-pack/plugins/lens/server/usage/schema.ts | 16 + .../lens/server/usage/visualization_counts.ts | 36 +- .../schema/xpack_plugins.json | 42 + x-pack/test/functional/apps/lens/formula.ts | 198 +++++ x-pack/test/functional/apps/lens/index.ts | 1 + .../test/functional/apps/lens/smokescreen.ts | 54 +- .../test/functional/page_objects/lens_page.ts | 65 +- 84 files changed, 5117 insertions(+), 659 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_tokenization.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_examples.md create mode 100644 x-pack/test/functional/apps/lens/formula.ts diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 872ac46352cf31..92ea23347c374c 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -21,5 +21,6 @@ import 'monaco-editor/esm/vs/editor/contrib/folding/folding.js'; // Needed for f import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature +import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight export { monaco }; diff --git a/packages/kbn-tinymath/grammar/grammar.peggy b/packages/kbn-tinymath/grammar/grammar.peggy index cbcb0b91bfea90..1c6f8c3334c234 100644 --- a/packages/kbn-tinymath/grammar/grammar.peggy +++ b/packages/kbn-tinymath/grammar/grammar.peggy @@ -1,16 +1,16 @@ // tinymath parsing grammar { - function simpleLocation (location) { - // Returns an object representing the position of the function within the expression, - // demarcated by the position of its first character and last character. We calculate these values - // using the offset because the expression could span multiple lines, and we don't want to deal - // with column and line values. - return { - min: location.start.offset, - max: location.end.offset + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } } - } } start @@ -74,26 +74,34 @@ Expression = AddSubtract AddSubtract - = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)* _ { - return rest.reduce((acc, curr) => ({ + = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)+ _ { + const topLevel = rest.reduce((acc, curr) => ({ type: 'function', name: curr[0] === '+' ? 'add' : 'subtract', args: [acc, curr[1]], - location: simpleLocation(location()), - text: text() - }), left) + }), left); + if (typeof topLevel === 'object') { + topLevel.location = simpleLocation(location()); + topLevel.text = text(); + } + return topLevel; } + / MultiplyDivide MultiplyDivide = _ left:Factor rest:(('*' / '/') Factor)* _ { - return rest.reduce((acc, curr) => ({ + const topLevel = rest.reduce((acc, curr) => ({ type: 'function', name: curr[0] === '*' ? 'multiply' : 'divide', args: [acc, curr[1]], - location: simpleLocation(location()), - text: text() - }), left) + }), left); + if (typeof topLevel === 'object') { + topLevel.location = simpleLocation(location()); + topLevel.text = text(); + } + return topLevel; } + / Factor Factor = Group diff --git a/packages/kbn-tinymath/index.d.ts b/packages/kbn-tinymath/index.d.ts index c3c32a59fa15ad..8e15d86c88fc84 100644 --- a/packages/kbn-tinymath/index.d.ts +++ b/packages/kbn-tinymath/index.d.ts @@ -24,9 +24,11 @@ export interface TinymathLocation { export interface TinymathFunction { type: 'function'; name: string; - text: string; args: TinymathAST[]; - location: TinymathLocation; + // Location is not guaranteed because PEG grammars are not left-recursive + location?: TinymathLocation; + // Text is not guaranteed because PEG grammars are not left-recursive + text?: string; } export interface TinymathVariable { diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js index bf1c7a9dbc5fb5..bbc8503684fd40 100644 --- a/packages/kbn-tinymath/test/library.test.js +++ b/packages/kbn-tinymath/test/library.test.js @@ -41,6 +41,35 @@ describe('Parser', () => { }); }); + describe('Math', () => { + it('converts basic symbols into left-to-right pairs', () => { + expect(parse('a + b + c - d')).toEqual({ + args: [ + { + name: 'add', + type: 'function', + args: [ + { + name: 'add', + type: 'function', + args: [ + expect.objectContaining({ location: { min: 0, max: 2 } }), + expect.objectContaining({ location: { min: 3, max: 6 } }), + ], + }, + expect.objectContaining({ location: { min: 7, max: 10 } }), + ], + }, + expect.objectContaining({ location: { min: 11, max: 13 } }), + ], + name: 'subtract', + type: 'function', + text: 'a + b + c - d', + location: { min: 0, max: 13 }, + }); + }); + }); + describe('Variables', () => { it('strings', () => { expect(parse('f')).toEqual(variableEqual('f')); @@ -263,6 +292,8 @@ describe('Evaluate', () => { expect(evaluate('5/20')).toEqual(0.25); expect(evaluate('1 + 1 + 2 + 3 + 12')).toEqual(19); expect(evaluate('100 / 10 / 10')).toEqual(1); + expect(evaluate('0 * 1 - 100 / 10 / 10')).toEqual(-1); + expect(evaluate('100 / (10 / 10)')).toEqual(100); }); it('equations with functions', () => { diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts index 1cfa9d862e6b95..60f1f7eb0955c0 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts @@ -72,6 +72,22 @@ const lensXYSeriesB = ({ visualization: { preferredSeriesType: 'seriesB', }, + datasourceStates: { + indexpattern: { + layers: { + first: { + columns: { + first: { + operationType: 'terms', + }, + second: { + operationType: 'formula', + }, + }, + }, + }, + }, + }, }, }, }, @@ -144,6 +160,7 @@ describe('dashboard telemetry', () => { expect(collectorData.lensByValue.a).toBe(3); expect(collectorData.lensByValue.seriesA).toBe(2); expect(collectorData.lensByValue.seriesB).toBe(1); + expect(collectorData.lensByValue.formula).toBe(1); }); it('handles misshapen lens panels', () => { diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts index 912dc04d16d092..fb1ddff469f578 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts @@ -27,6 +27,16 @@ interface LensPanel extends SavedDashboardPanel730ToLatest { visualization?: { preferredSeriesType?: string; }; + datasourceStates?: { + indexpattern?: { + layers: Record< + string, + { + columns: Record; + } + >; + }; + }; }; }; }; @@ -109,6 +119,19 @@ export const collectByValueLensInfo: DashboardCollectorFunction = (panels, colle } collectorData.lensByValue[type] = collectorData.lensByValue[type] + 1; + + const hasFormula = Object.values( + lensPanel.embeddableConfig.attributes.state?.datasourceStates?.indexpattern?.layers || {} + ).some((layer) => + Object.values(layer.columns).some((column) => column.operationType === 'formula') + ); + + if (hasFormula && !collectorData.lensByValue.formula) { + collectorData.lensByValue.formula = 0; + } + if (hasFormula) { + collectorData.lensByValue.formula++; + } } } }; diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts index 7939441ff0d602..d6af19d9dbf531 100644 --- a/src/plugins/expressions/common/expression_functions/specs/map_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; -import { Datatable, getType } from '../../expression_types'; +import { Datatable, DatatableColumn, getType } from '../../expression_types'; export interface MapColumnArguments { id?: string | null; @@ -110,10 +110,10 @@ export const mapColumn: ExpressionFunctionDefinition< return Promise.all(rowPromises).then((rows) => { const type = rows.length ? getType(rows[0][columnId]) : 'null'; - const newColumn = { + const newColumn: DatatableColumn = { id: columnId, name: args.name, - meta: { type }, + meta: { type, params: { id: type } }, }; if (args.copyMetaFrom) { const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts index f5c1f3838f66c7..bb4e6303e90b7d 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts @@ -29,7 +29,11 @@ describe('mapColumn', () => { expect(result.type).toBe('datatable'); expect(result.columns).toEqual([ ...testTable.columns, - { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } }, + { + id: 'pricePlusTwo', + name: 'pricePlusTwo', + meta: { type: 'number', params: { id: 'number' } }, + }, ]); expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo'); expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo'); diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index a779ef540d72ec..d4fb5a708e4405 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -11,6 +11,7 @@ exports[`is rendered 1`] = ` onChange={[Function]} options={ Object { + "matchBrackets": "never", "minimap": Object { "enabled": false, }, @@ -39,6 +40,7 @@ exports[`is rendered 1`] = ` nodeType="div" onResize={[Function]} querySelector={null} + refreshMode="debounce" refreshRate={1000} skipOnMount={false} targetDomEl={null} diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 55e10e7861e518..251f05950da227 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -187,10 +187,16 @@ export class CodeEditor extends React.Component { wordBasedSuggestions: false, wordWrap: 'on', wrappingIndent: 'indent', + matchBrackets: 'never', ...options, }} /> - + ); } diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index e85840e8734308..d0bdc292619d89 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -38,7 +38,7 @@ module.exports = { 'src/plugins/data/public/expressions/interpreter' ), 'kbn/interpreter': path.resolve(KIBANA_ROOT, 'packages/kbn-interpreter/target/common'), - tinymath: path.resolve(KIBANA_ROOT, 'node_modules/tinymath/lib/tinymath.es5.js'), + tinymath: path.resolve(KIBANA_ROOT, 'node_modules/tinymath/lib/tinymath.min.js'), core_app_image_assets: path.resolve(KIBANA_ROOT, 'src/core/public/core_app/images'), }, extensions: ['.js', '.json', '.ts', '.tsx', '.scss'], diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 245e964bbd2e67..ecaae04232f8af 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types'; import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; @@ -164,79 +164,152 @@ export const LensTopNavMenu = ({ const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { defaultMessage: 'unsaved', }); - const topNavConfig = getLensTopNavConfig({ - showSaveAndReturn: Boolean( - isLinkedToOriginatingApp && - // Temporarily required until the 'by value' paradigm is default. - (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) - ), - enableExportToCSV: Boolean(isSaveable && activeData && Object.keys(activeData).length), - isByValueMode: getIsByValueMode(), - allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, - showCancel: Boolean(isLinkedToOriginatingApp), - savingToLibraryPermitted, - savingToDashboardPermitted, - actions: { - exportToCSV: () => { - if (!activeData) { - return; - } - const datatables = Object.values(activeData); - const content = datatables.reduce>( - (memo, datatable, i) => { - // skip empty datatables - if (datatable) { - const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + const topNavConfig = useMemo( + () => + getLensTopNavConfig({ + showSaveAndReturn: Boolean( + isLinkedToOriginatingApp && + // Temporarily required until the 'by value' paradigm is default. + (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) + ), + enableExportToCSV: Boolean(isSaveable && activeData && Object.keys(activeData).length), + isByValueMode: getIsByValueMode(), + allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, + showCancel: Boolean(isLinkedToOriginatingApp), + savingToLibraryPermitted, + savingToDashboardPermitted, + actions: { + exportToCSV: () => { + if (!activeData) { + return; + } + const datatables = Object.values(activeData); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; - memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { - content: exporters.datatableToCSV(datatable, { - csvSeparator: uiSettings.get('csv:separator', ','), - quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, - }), - type: exporters.CSV_MIME_TYPE, - }; + memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); + if (content) { + downloadMultipleAs(content); } - return memo; }, - {} - ); - if (content) { - downloadMultipleAs(content); - } - }, - saveAndReturn: () => { - if (savingToDashboardPermitted && lastKnownDoc) { - // disabling the validation on app leave because the document has been saved. - onAppLeave((actions) => { - return actions.default(); - }); - runSave( - { - newTitle: lastKnownDoc.title, - newCopyOnSave: false, - isTitleDuplicateConfirmed: false, - returnToOrigin: true, - }, - { - saveToLibrary: - (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, + saveAndReturn: () => { + if (savingToDashboardPermitted && lastKnownDoc) { + // disabling the validation on app leave because the document has been saved. + onAppLeave((actions) => { + return actions.default(); + }); + runSave( + { + newTitle: lastKnownDoc.title, + newCopyOnSave: false, + isTitleDuplicateConfirmed: false, + returnToOrigin: true, + }, + { + saveToLibrary: + (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, + } + ); } - ); - } - }, - showSaveModal: () => { - if (savingToDashboardPermitted || savingToLibraryPermitted) { - setIsSaveModalVisible(true); - } - }, - cancel: () => { - if (redirectToOrigin) { - redirectToOrigin(); + }, + showSaveModal: () => { + if (savingToDashboardPermitted || savingToLibraryPermitted) { + setIsSaveModalVisible(true); + } + }, + cancel: () => { + if (redirectToOrigin) { + redirectToOrigin(); + } + }, + }, + }), + [ + activeData, + attributeService, + dashboardFeatureFlag.allowByValueEmbeddables, + data.fieldFormats.deserialize, + getIsByValueMode, + initialInput, + isLinkedToOriginatingApp, + isSaveable, + lastKnownDoc, + onAppLeave, + redirectToOrigin, + runSave, + savingToDashboardPermitted, + savingToLibraryPermitted, + setIsSaveModalVisible, + uiSettings, + unsavedTitle, + ] + ); + + const onQuerySubmitWrapped = useCallback( + (payload) => { + const { dateRange, query: newQuery } = payload; + const currentRange = data.query.timefilter.timefilter.getTime(); + if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { + data.query.timefilter.timefilter.setTime(dateRange); + trackUiEvent('app_date_change'); + } else { + // Query has changed, renew the session id. + // Time change will be picked up by the time subscription + dispatchSetState({ searchSessionId: data.search.session.start() }); + trackUiEvent('app_query_change'); + } + if (newQuery) { + if (!isEqual(newQuery, query)) { + dispatchSetState({ query: newQuery }); } - }, + } }, - }); + [data.query.timefilter.timefilter, data.search.session, dispatchSetState, query] + ); + + const onSavedWrapped = useCallback( + (newSavedQuery) => { + dispatchSetState({ savedQuery: newSavedQuery }); + }, + [dispatchSetState] + ); + + const onSavedQueryUpdatedWrapped = useCallback( + (newSavedQuery) => { + const savedQueryFilters = newSavedQuery.attributes.filters || []; + const globalFilters = data.query.filterManager.getGlobalFilters(); + data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); + dispatchSetState({ + query: newSavedQuery.attributes.query, + savedQuery: { ...newSavedQuery }, + }); // Shallow query for reference issues + }, + [data.query.filterManager, dispatchSetState] + ); + + const onClearSavedQueryWrapped = useCallback(() => { + data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); + dispatchSetState({ + filters: data.query.filterManager.getGlobalFilters(), + query: data.query.queryString.getDefaultQuery(), + savedQuery: undefined, + }); + }, [data.query.filterManager, data.query.queryString, dispatchSetState]); return ( { - const { dateRange, query: newQuery } = payload; - const currentRange = data.query.timefilter.timefilter.getTime(); - if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { - data.query.timefilter.timefilter.setTime(dateRange); - trackUiEvent('app_date_change'); - } else { - // Query has changed, renew the session id. - // Time change will be picked up by the time subscription - dispatchSetState({ searchSessionId: data.search.session.start() }); - trackUiEvent('app_query_change'); - } - if (newQuery) { - if (!isEqual(newQuery, query)) { - dispatchSetState({ query: newQuery }); - } - } - }} - onSaved={(newSavedQuery) => { - dispatchSetState({ savedQuery: newSavedQuery }); - }} - onSavedQueryUpdated={(newSavedQuery) => { - const savedQueryFilters = newSavedQuery.attributes.filters || []; - const globalFilters = data.query.filterManager.getGlobalFilters(); - data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); - dispatchSetState({ - query: newSavedQuery.attributes.query, - savedQuery: { ...newSavedQuery }, - }); // Shallow query for reference issues - }} - onClearSavedQuery={() => { - data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); - dispatchSetState({ - filters: data.query.filterManager.getGlobalFilters(), - query: data.query.queryString.getDefaultQuery(), - savedQuery: undefined, - }); - }} + onQuerySubmit={onQuerySubmitWrapped} + onSaved={onSavedWrapped} + onSavedQueryUpdated={onSavedQueryUpdatedWrapped} + onClearSavedQuery={onClearSavedQueryWrapped} indexPatterns={indexPatternsForTopNav} query={query} dateRangeFrom={from} diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx index ba24da8309ed7c..5c53d40f999b77 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx @@ -51,7 +51,7 @@ export const createGridColumns = ( columnId, }: Pick) => { const rowValue = table.rows[rowIndex][columnId]; - const column = columnsReverseLookup[columnId]; + const column = columnsReverseLookup?.[columnId]; const contentsIsDefined = rowValue != null; const cellContent = formatFactory(column?.meta?.params).convert(rowValue); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 3936fb9e1a1b12..1ec48f516bd32d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -76,6 +76,8 @@ describe('ConfigPanel', () => { framePublicAPI: frame, dispatch: jest.fn(), core: coreMock.createStart(), + isFullscreen: false, + toggleFullscreen: jest.fn(), }; } @@ -119,19 +121,23 @@ describe('ConfigPanel', () => { expect(component.find(LayerPanel).exists()).toBe(false); }); - it('allow datasources and visualizations to use setters', () => { + it('allow datasources and visualizations to use setters', async () => { const props = getDefaultProps(); const component = mountWithIntl(); const { updateDatasource, updateAll } = component.find(LayerPanel).props(); const updater = () => 'updated'; updateDatasource('ds1', updater); + // wait for one tick so async updater has a chance to trigger + await new Promise((r) => setTimeout(r, 0)); expect(props.dispatch).toHaveBeenCalledTimes(1); expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual( 'updated' ); updateAll('ds1', updater, props.visualizationState); + // wait for one tick so async updater has a chance to trigger + await new Promise((r) => setTimeout(r, 0)); expect(props.dispatch).toHaveBeenCalledTimes(2); expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual( 'updated' diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index c1ab2b4586ab33..81c044af532fbf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -71,32 +71,54 @@ export function LayerPanels( }, [dispatch] ); + const updateDatasourceAsync = useMemo( + () => (datasourceId: string, newState: unknown) => { + // React will synchronously update if this is triggered from a third party component, + // which we don't want. The timeout lets user interaction have priority, then React updates. + setTimeout(() => { + updateDatasource(datasourceId, newState); + }, 0); + }, + [updateDatasource] + ); const updateAll = useMemo( () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { - dispatch({ - type: 'UPDATE_STATE', - subType: 'UPDATE_ALL_STATES', - updater: (prevState) => { - const updatedDatasourceState = - typeof newDatasourceState === 'function' - ? newDatasourceState(prevState.datasourceStates[datasourceId].state) - : newDatasourceState; - return { - ...prevState, - datasourceStates: { - ...prevState.datasourceStates, - [datasourceId]: { - state: updatedDatasourceState, - isLoading: false, + // React will synchronously update if this is triggered from a third party component, + // which we don't want. The timeout lets user interaction have priority, then React updates. + setTimeout(() => { + dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: (prevState) => { + const updatedDatasourceState = + typeof newDatasourceState === 'function' + ? newDatasourceState(prevState.datasourceStates[datasourceId].state) + : newDatasourceState; + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: updatedDatasourceState, + isLoading: false, + }, + }, + visualization: { + ...prevState.visualization, + state: newVisualizationState, }, - }, - visualization: { - ...prevState.visualization, - state: newVisualizationState, - }, - stagedPreview: undefined, - }; - }, + stagedPreview: undefined, + }; + }, + }); + }, 0); + }, + [dispatch] + ); + const toggleFullscreen = useMemo( + () => () => { + dispatch({ + type: 'TOGGLE_FULLSCREEN', }); }, [dispatch] @@ -118,6 +140,7 @@ export function LayerPanels( visualizationState={visualizationState} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} + updateDatasourceAsync={updateDatasourceAsync} updateAll={updateAll} isOnlyLayer={layerIds.length === 1} onRemoveLayer={() => { @@ -135,6 +158,7 @@ export function LayerPanels( }); removeLayerRef(layerId); }} + toggleFullscreen={toggleFullscreen} /> ) : null )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss index 91cd706ea77d11..135286fc2172bb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.scss @@ -8,21 +8,36 @@ position: absolute; left: 0; animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance; + @include euiBreakpoint('l', 'xl') { top: 0 !important; height: 100% !important; } + @include euiBreakpoint('xs', 's', 'm') { @include euiFlyout; } + + .lnsFrameLayout__sidebar-isFullscreen & { + border-left: $euiBorderThin; // Force border regardless of theme in fullscreen + box-shadow: none; + } } .lnsDimensionContainer__footer { padding: $euiSizeS; + + .lnsFrameLayout__sidebar-isFullscreen & { + display: none; + } } .lnsDimensionContainer__header { padding: $euiSizeS $euiSizeXS; + + .lnsFrameLayout__sidebar-isFullscreen & { + display: none; + } } .lnsDimensionContainer__headerTitle { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx index b14d391c2c9696..2f3eb5043d6106 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx @@ -29,26 +29,33 @@ export function DimensionContainer({ groupLabel, handleClose, panel, + isFullscreen, panelRef, }: { isOpen: boolean; - handleClose: () => void; - panel: React.ReactElement; + handleClose: () => boolean; + panel: React.ReactElement | null; groupLabel: string; + isFullscreen: boolean; panelRef: (el: HTMLDivElement) => void; }) { const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false); const closeFlyout = useCallback(() => { - handleClose(); - setFocusTrapIsEnabled(false); + const canClose = handleClose(); + if (canClose) { + setFocusTrapIsEnabled(false); + } + return canClose; }, [handleClose]); const closeOnEscape = useCallback( (event: KeyboardEvent) => { if (event.key === keys.ESCAPE) { - event.preventDefault(); - closeFlyout(); + const canClose = closeFlyout(); + if (canClose) { + event.preventDefault(); + } } }, [closeFlyout] @@ -69,7 +76,15 @@ export function DimensionContainer({
- + { + if (isFullscreen) { + return; + } + closeFlyout(); + }} + isDisabled={!isOpen} + >
{ visualizationState: 'state', updateVisualization: jest.fn(), updateDatasource: jest.fn(), + updateDatasourceAsync: jest.fn(), updateAll: jest.fn(), framePublicAPI: frame, isOnlyLayer: true, @@ -86,6 +87,8 @@ describe('LayerPanel', () => { core: coreMock.createStart(), layerIndex: 0, registerNewLayerRef: jest.fn(), + isFullscreen: false, + toggleFullscreen: jest.fn(), }; } @@ -255,7 +258,7 @@ describe('LayerPanel', () => { it('should not update the visualization if the datasource is incomplete', () => { (generateId as jest.Mock).mockReturnValue(`newid`); const updateAll = jest.fn(); - const updateDatasource = jest.fn(); + const updateDatasourceAsync = jest.fn(); mockVisualization.getConfiguration.mockReturnValue({ groups: [ @@ -273,7 +276,7 @@ describe('LayerPanel', () => { const component = mountWithIntl( ); @@ -292,15 +295,88 @@ describe('LayerPanel', () => { mockDatasource.renderDimensionEditor.mock.calls.length - 1 ][1].setState; + act(() => { + stateFn( + { + indexPatternId: '1', + columns: {}, + columnOrder: [], + incompleteColumns: { newId: { operationType: 'count' } }, + }, + { isDimensionComplete: false } + ); + }); + expect(updateAll).not.toHaveBeenCalled(); + expect(updateDatasourceAsync).toHaveBeenCalled(); + act(() => { stateFn({ indexPatternId: '1', columns: {}, columnOrder: [], - incompleteColumns: { newId: { operationType: 'count' } }, }); }); - expect(updateAll).not.toHaveBeenCalled(); + expect(updateAll).toHaveBeenCalled(); + }); + + it('should remove the dimension when the datasource marks it as removed', () => { + const updateAll = jest.fn(); + const updateDatasource = jest.fn(); + + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'y' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl( + + ); + + act(() => { + component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + }); + component.update(); + + expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ columnId: 'y' }) + ); + const stateFn = + mockDatasource.renderDimensionEditor.mock.calls[ + mockDatasource.renderDimensionEditor.mock.calls.length - 1 + ][1].setState; act(() => { stateFn( @@ -308,11 +384,19 @@ describe('LayerPanel', () => { indexPatternId: '1', columns: {}, columnOrder: [], + incompleteColumns: { y: { operationType: 'average' } }, }, - { shouldReplaceDimension: true } + { + isDimensionComplete: false, + } ); }); expect(updateAll).toHaveBeenCalled(); + expect(mockVisualization.removeDimension).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'y', + }) + ); }); it('should keep the DimensionContainer open when configuring a new dimension', () => { @@ -331,6 +415,7 @@ describe('LayerPanel', () => { accessors: [], filterOperations: () => true, supportsMoreColumns: true, + enableDimensionEditor: true, dataTestSubj: 'lnsGroup', }, ], @@ -345,6 +430,7 @@ describe('LayerPanel', () => { accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: false, + enableDimensionEditor: true, dataTestSubj: 'lnsGroup', }, ], @@ -357,6 +443,20 @@ describe('LayerPanel', () => { component.update(); expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + + const lastArgs = + mockDatasource.renderDimensionEditor.mock.calls[ + mockDatasource.renderDimensionEditor.mock.calls.length - 1 + ][1]; + + // Simulate what is called by the dimension editor + act(() => { + lastArgs.setState(lastArgs.state, { + isDimensionComplete: true, + }); + }); + + expect(mockVisualization.renderDimensionEditor).toHaveBeenCalled(); }); it('should close the DimensionContainer when the active visualization changes', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index a605a94a346468..3a299de0fca6a3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -42,6 +42,7 @@ export function LayerPanel( isOnlyLayer: boolean; updateVisualization: StateSetter; updateDatasource: (datasourceId: string, newState: unknown) => void; + updateDatasourceAsync: (datasourceId: string, newState: unknown) => void; updateAll: ( datasourceId: string, newDatasourcestate: unknown, @@ -49,6 +50,8 @@ export function LayerPanel( ) => void; onRemoveLayer: () => void; registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; + toggleFullscreen: () => void; + isFullscreen: boolean; } ) { const [activeDimension, setActiveDimension] = useState( @@ -65,6 +68,8 @@ export function LayerPanel( activeVisualization, updateVisualization, updateDatasource, + toggleFullscreen, + isFullscreen, } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; @@ -197,9 +202,16 @@ export function LayerPanel( setNextFocusedButtonId, ]); + const isDimensionPanelOpen = Boolean(activeId); + return ( <> -
+
@@ -407,9 +419,16 @@ export function LayerPanel( (panelRef.current = el)} - isOpen={!!activeId} + isOpen={isDimensionPanelOpen} + isFullscreen={isFullscreen} groupLabel={activeGroup?.groupLabel || ''} handleClose={() => { + if ( + layerDatasource.canCloseDimensionEditor && + !layerDatasource.canCloseDimensionEditor(layerDatasourceState) + ) { + return false; + } if (layerDatasource.updateStateOnCloseDimension) { const newState = layerDatasource.updateStateOnCloseDimension({ state: layerDatasourceState, @@ -421,9 +440,13 @@ export function LayerPanel( } } setActiveDimension(initialActiveDimensionState); + if (isFullscreen) { + toggleFullscreen(); + } + return true; }} panel={ - <> +
{activeGroup && activeId && ( { - if (shouldReplaceDimension || shouldRemoveDimension) { + if (allAccessors.includes(activeId)) { + if (isDimensionComplete) { + props.updateDatasourceAsync(datasourceId, newState); + } else { + // The datasource can indicate that the previously-valid column is no longer + // complete, which clears the visualization. This keeps the flyout open and reuses + // the previous columnId + props.updateAll( + datasourceId, + newState, + activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } + } else if (isDimensionComplete) { props.updateAll( datasourceId, newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ - layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ - layerId, - groupId: activeGroup.groupId, - columnId: activeId, - prevState: props.visualizationState, - }) + activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) ); + setActiveDimension({ ...activeDimension, isNew: false }); } else { - props.updateDatasource(datasourceId, newState); + props.updateDatasourceAsync(datasourceId, newState); } - setActiveDimension({ - ...activeDimension, - isNew: false, - }); }, }} /> )} {activeGroup && activeId && + !isFullscreen && !activeDimension.isNew && activeVisualization.renderDimensionEditor && activeGroup?.enableDimensionEditor && ( @@ -491,7 +519,7 @@ export function LayerPanel( />
)} - +
} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 37b2198cfd51f9..1af8c16fa1395f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -29,6 +29,7 @@ export interface ConfigPanelWrapperProps { } >; core: DatasourceDimensionEditorProps['core']; + isFullscreen: boolean; } export interface LayerPanelProps { @@ -46,6 +47,7 @@ export interface LayerPanelProps { } >; core: DatasourceDimensionEditorProps['core']; + isFullscreen: boolean; } export interface LayerDatasourceDropProps { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 4710e03d336bcf..161b0125a172a3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useReducer, useState, useCallback } from 'react'; +import React, { useEffect, useReducer, useState, useCallback, useRef } from 'react'; import { CoreStart } from 'kibana/public'; import { isEqual } from 'lodash'; import { PaletteRegistry } from 'src/plugins/charts/public'; @@ -30,6 +30,7 @@ import { applyVisualizeFieldSuggestions, getTopSuggestionForField, switchToSuggestion, + Suggestion, } from './suggestion_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { @@ -327,45 +328,37 @@ export function EditorFrame(props: EditorFrameProps) { ] ); - const getSuggestionForField = React.useCallback( - (field: DragDropIdentifier) => { - const { activeDatasourceId, datasourceStates } = state; - const activeVisualizationId = state.visualization.activeId; - const visualizationState = state.visualization.state; - const { visualizationMap, datasourceMap } = props; + // Using a ref to prevent rerenders in the child components while keeping the latest state + const getSuggestionForField = useRef<(field: DragDropIdentifier) => Suggestion | undefined>(); + getSuggestionForField.current = (field: DragDropIdentifier) => { + const { activeDatasourceId, datasourceStates } = state; + const activeVisualizationId = state.visualization.activeId; + const visualizationState = state.visualization.state; + const { visualizationMap, datasourceMap } = props; - if (!field || !activeDatasourceId) { - return; - } + if (!field || !activeDatasourceId) { + return; + } - return getTopSuggestionForField( - datasourceLayers, - activeVisualizationId, - visualizationMap, - visualizationState, - datasourceMap[activeDatasourceId], - datasourceStates, - field - ); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - state.visualization.state, - props.datasourceMap, - props.visualizationMap, - state.activeDatasourceId, - state.datasourceStates, - ] - ); + return getTopSuggestionForField( + datasourceLayers, + activeVisualizationId, + visualizationMap, + visualizationState, + datasourceMap[activeDatasourceId], + datasourceStates, + field + ); + }; const hasSuggestionForField = useCallback( - (field: DragDropIdentifier) => getSuggestionForField(field) !== undefined, + (field: DragDropIdentifier) => getSuggestionForField.current!(field) !== undefined, [getSuggestionForField] ); const dropOntoWorkspace = useCallback( (field) => { - const suggestion = getSuggestionForField(field); + const suggestion = getSuggestionForField.current!(field); if (suggestion) { trackUiEvent('drop_onto_workspace'); switchToSuggestion(dispatch, suggestion, 'SWITCH_VISUALIZATION'); @@ -377,6 +370,7 @@ export function EditorFrame(props: EditorFrameProps) { return ( ) } @@ -429,11 +424,12 @@ export function EditorFrame(props: EditorFrameProps) { visualizationState={state.visualization.state} visualizationMap={props.visualizationMap} dispatch={dispatch} + isFullscreen={Boolean(state.isFullscreenDatasource)} ExpressionRenderer={props.ExpressionRenderer} core={props.core} plugins={props.plugins} visualizeTriggerFieldContext={visualizeTriggerFieldContext} - getSuggestionForField={getSuggestionForField} + getSuggestionForField={getSuggestionForField.current} /> ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss index 0756c13f6999bf..282e69cd7636c7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss @@ -67,9 +67,16 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ padding: $euiSize $euiSize 0; position: relative; z-index: $lnsZLevel1; + &:first-child { padding-left: $euiSize; } + + &.lnsFrameLayout__pageBody-isFullscreen { + background: $euiColorEmptyShade; + flex: 1; + padding: 0; + } } .lnsFrameLayout__sidebar { @@ -81,6 +88,13 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ position: relative; } +.lnsFrameLayout-isFullscreen .lnsFrameLayout__sidebar--left, +.lnsFrameLayout-isFullscreen .lnsFrameLayout__suggestionPanel { + // Hide the datapanel and suggestions in fullscreen mode. Using display: none does trigger + // a rerender when the container becomes visible again, maybe pushing offscreen is better + display: none; +} + .lnsFrameLayout__sidebar--right { flex-basis: 25%; background-color: lightOrDarkTheme($euiColorLightestShade, $euiColorInk); @@ -106,3 +120,8 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ } } } + +.lnsFrameLayout__sidebar-isFullscreen { + flex: 1; + max-width: none; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index a54901a2a2fe1d..f27e0f9c24d7bd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -10,23 +10,32 @@ import './frame_layout.scss'; import React from 'react'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; export interface FrameLayoutProps { dataPanel: React.ReactNode; configPanel?: React.ReactNode; suggestionsPanel?: React.ReactNode; workspacePanel?: React.ReactNode; + isFullscreen?: boolean; } export function FrameLayout(props: FrameLayoutProps) { return ( - + -
+

{i18n.translate('xpack.lens.section.dataPanelLabel', { @@ -36,7 +45,13 @@ export function FrameLayout(props: FrameLayoutProps) { {props.dataPanel}

-
+

{i18n.translate('xpack.lens.section.workspaceLabel', { @@ -45,10 +60,13 @@ export function FrameLayout(props: FrameLayoutProps) {

{props.workspacePanel} - {props.suggestionsPanel} +
{props.suggestionsPanel}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index aa365d1e66d6c5..a87aa7a2cb4282 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -22,6 +22,7 @@ export interface EditorFrameState extends PreviewState { description?: string; stagedPreview?: PreviewState; activeDatasourceId: string | null; + isFullscreenDatasource?: boolean; } export type Action = @@ -90,6 +91,9 @@ export type Action = | { type: 'SWITCH_DATASOURCE'; newDatasourceId: string; + } + | { + type: 'TOGGLE_FULLSCREEN'; }; export function getActiveDatasourceIdFromDoc(doc?: Document) { @@ -281,6 +285,8 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta }, stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview, }; + case 'TOGGLE_FULLSCREEN': + return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource }; default: return state; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 0c2eb4f39d8959..8107b6646500d5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -200,15 +200,16 @@ export function SuggestionPanel({ visualizationState: currentVisualizationState, activeData: frame.activeData, }) - .filter((suggestion) => !suggestion.hide) .filter( ({ + hide, visualizationId, visualizationState: suggestionVisualizationState, datasourceState: suggestionDatasourceState, datasourceId: suggetionDatasourceId, }) => { return ( + !hide && validateDatasourceAndVisualization( suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, suggestionDatasourceState, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 1d248c4411023c..38e9bb868b26aa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -64,6 +64,8 @@ const defaultProps = { data: mockDataPlugin(), }, getSuggestionForField: () => undefined, + isFullscreen: false, + toggleFullscreen: jest.fn(), }; describe('workspace_panel', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 45abbf120042d0..01d4e84ec4374d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -79,6 +79,7 @@ export interface WorkspacePanelProps { title?: string; visualizeTriggerFieldContext?: VisualizeFieldContext; getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined; + isFullscreen: boolean; } interface WorkspaceState { @@ -134,6 +135,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ title, visualizeTriggerFieldContext, suggestionForDraggedField, + isFullscreen, }: Omit & { suggestionForDraggedField: Suggestion | undefined; }) { @@ -346,6 +348,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ); }; + const element = expression !== null ? renderVisualization() : renderEmptyWorkspace(); + const dragDropContext = useContext(DragContext); const renderDragDrop = () => { @@ -363,7 +367,10 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ customWorkspaceRenderer() ) : ( - {renderVisualization()} - {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()} + {element} ); @@ -389,6 +395,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ datasourceStates={datasourceStates} datasourceMap={datasourceMap} visualizationMap={visualizationMap} + isFullscreen={isFullscreen} > {renderDragDrop()} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index e687e478cd3680..21e3f9aa36674b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -31,6 +31,10 @@ overflow: hidden; } } + + &.lnsWorkspacePanelWrapper--fullscreen { + margin-bottom: 0; + } } .lnsWorkspacePanel__dragDrop { @@ -62,6 +66,10 @@ animation: lnsWorkspacePanel__illustrationPulseContinuous 1.5s ease-in-out 0s infinite normal forwards; } } + + &.lnsWorkspacePanel__dragDrop--fullscreen { + border: none; + } } .lnsWorkspacePanel__emptyContent { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx index 7bb467df9ab0e1..c18b362e2faa4e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx @@ -37,6 +37,7 @@ describe('workspace_panel_wrapper', () => { visualizationMap={{ myVis: mockVisualization }} datasourceMap={{}} datasourceStates={{}} + isFullscreen={false} > @@ -58,6 +59,7 @@ describe('workspace_panel_wrapper', () => { visualizationMap={{ myVis: { ...mockVisualization, renderToolbar: renderToolbarMock } }} datasourceMap={{}} datasourceStates={{}} + isFullscreen={false} /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index ec12e9e4002039..6724002d23e0bb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -10,6 +10,7 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly } from '@elastic/eui'; +import classNames from 'classnames'; import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { Action } from '../state_management'; @@ -32,6 +33,7 @@ export interface WorkspacePanelWrapperProps { state: unknown; } >; + isFullscreen: boolean; } export function WorkspacePanelWrapper({ @@ -44,6 +46,7 @@ export function WorkspacePanelWrapper({ visualizationMap, datasourceMap, datasourceStates, + isFullscreen, }: WorkspacePanelWrapperProps) { const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null; const setVisualizationState = useCallback( @@ -85,40 +88,42 @@ export function WorkspacePanelWrapper({ wrap={true} justifyContent="spaceBetween" > - - - - - - {activeVisualization && activeVisualization.renderToolbar && ( + {!isFullscreen ? ( + + - - )} - - + {activeVisualization && activeVisualization.renderToolbar && ( + + + + )} + + + ) : null} {warningMessages && warningMessages.length ? ( {warningMessages} @@ -126,7 +131,11 @@ export function WorkspacePanelWrapper({
- +
} + body={

{i18nTexts.pluginError.description}

} + /> + ); } return ( - + > + {i18nTexts.loadingError.title}} + body={

{i18nTexts.loadingError.description}

} + /> + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index a9608109728ba2..31b5c80d5b3773 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -8,14 +8,7 @@ import React, { useEffect, useState, useCallback } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { - EuiButtonEmpty, - EuiPageBody, - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiPageContent, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DomainDeprecationDetails } from 'kibana/public'; @@ -135,46 +128,28 @@ export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponent getAllDeprecations(); }, [deprecations, getAllDeprecations]); - const getPageContent = () => { - if (kibanaDeprecations && kibanaDeprecations.length === 0) { - return ( + if (kibanaDeprecations && kibanaDeprecations.length === 0) { + return ( + history.push('/overview')} /> - ); - } - - let content: React.ReactNode; - - if (isLoading) { - content = {i18nTexts.isLoading}; - } else if (kibanaDeprecations?.length) { - content = ( - - ); - } else if (error) { - content = ; - } + + ); + } + if (isLoading) { return ( -
- - {content} -
+ + {i18nTexts.isLoading} + ); - }; - - return ( - - + } else if (kibanaDeprecations?.length) { + return ( +
- - {getPageContent()} - - {stepsModalContent && ( - toggleStepsModal()} modalContent={stepsModalContent} /> - )} - - {resolveModalContent && ( - toggleResolveModal()} - resolveDeprecation={resolveDeprecation} - isResolvingDeprecation={isResolvingDeprecation} - deprecation={resolveModalContent} - /> - )} - - - - ); + + + + + {stepsModalContent && ( + toggleStepsModal()} modalContent={stepsModalContent} /> + )} + + {resolveModalContent && ( + toggleResolveModal()} + resolveDeprecation={resolveDeprecation} + isResolvingDeprecation={isResolvingDeprecation} + deprecation={resolveModalContent} + /> + )} +
+ ); + } else if (error) { + return ; + } + + return null; }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 6b3048b669aa2e..b77d4a337295c2 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -8,11 +8,9 @@ import React, { FunctionComponent, useEffect } from 'react'; import { - EuiPageContent, EuiPageContentBody, EuiText, EuiPageHeader, - EuiPageBody, EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, @@ -91,72 +89,68 @@ export const DeprecationsOverview: FunctionComponent = ({ history }) => { }, [breadcrumbs]); return ( - - - - {i18nTexts.docLink} - , - ]} - /> - - - <> - -

{i18nTexts.pageDescription}

-
- - - - {/* Remove this in last minor of the current major (e.g., 7.15) */} - - - - - {/* Deprecation stats */} - - - - - - - - - - - - - {/* Deprecation logging */} - - - -

{i18nTexts.deprecationLoggingTitle}

-
- - -

- {i18nTexts.getDeprecationLoggingDescription( - `${nextMajor}.x`, - docLinks.links.elasticsearch.deprecationLogging - )} -

-
- - - - -
-
- -
-
-
+
+ + {i18nTexts.docLink} + , + ]} + /> + + + + + <> + {/* Remove this in last minor of the current major (e.g., 7.15) */} + + + + + {/* Deprecation stats */} + + + + + + + + + + + + + {/* Deprecation logging */} + + + +

{i18nTexts.deprecationLoggingTitle}

+
+ + +

+ {i18nTexts.getDeprecationLoggingDescription( + `${nextMajor}.x`, + docLinks.links.elasticsearch.deprecationLogging + )} +

+
+ + + + +
+
+ +
+
); }; From 92bd5c642417625152153cab7abff3a9e68d32d0 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 10 Jun 2021 18:17:41 +0200 Subject: [PATCH 20/99] [Fleet] Adjustments to the "Add agent" flyout (#101576) * updated add agent flyout buttons * move close button to left * added new optional prop for the add agent flyout and factored out a component from the index.tsx file * quite a big refactor, moved the agent policy authentication to own component and reused in two places * fixed layout on view where policy was not selected, also fixed a render cycle * removed unnecessary filter * move handler to function body instead of in JSX * remove unused i18n * added jest test for agent enrollment flyout steps behaviour * fix issues after master merge * Fix bad import in type file Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../agent_policy/components/actions_menu.tsx | 2 +- ...advanced_agent_authentication_settings.tsx | 196 +++++++++++++ .../agent_enrollment_flyout.test.mocks.ts | 49 ++++ .../agent_enrollment_flyout.test.tsx | 164 +++++++++++ .../agent_policy_selection.tsx | 264 +++--------------- .../agent_enrollment_flyout/index.tsx | 88 ++---- .../managed_instructions.tsx | 33 +-- .../missing_fleet_server_host_callout.tsx | 56 ++++ .../standalone_instructions.tsx | 18 +- .../agent_enrollment_flyout/steps.tsx | 49 +++- .../agent_enrollment_flyout/types.ts | 20 ++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 13 files changed, 609 insertions(+), 334 deletions(-) create mode 100644 x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx create mode 100644 x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts create mode 100644 x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx create mode 100644 x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx create mode 100644 x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index 798ed4f0381566..ecc538bd95e2a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -101,7 +101,7 @@ export const AgentPolicyActionMenu = memo<{ ) : null} {isEnrollmentFlyoutOpen && ( - + )} void; +} + +export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({ + agentPolicyId, + onKeyChange, +}) => { + const { notifications } = useStartServices(); + const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( + [] + ); + // TODO: Remove this piece of state since we don't need it here. The currently selected enrollment API key only + // needs to live on the form + const [selectedEnrollmentApiKey, setSelectedEnrollmentApiKey] = useState(); + const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); + const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false); + + const onCreateEnrollmentTokenClick = async () => { + setIsLoadingEnrollmentKey(true); + if (agentPolicyId) { + try { + const res = await sendCreateEnrollmentAPIKey({ policy_id: agentPolicyId }); + if (res.error) { + throw res.error; + } + setIsLoadingEnrollmentKey(false); + if (!res.data?.item) { + return; + } + setEnrollmentAPIKeys([res.data.item]); + setSelectedEnrollmentApiKey(res.data.item.id); + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { + defaultMessage: 'Enrollment token created', + }) + ); + } catch (error) { + setIsLoadingEnrollmentKey(false); + notifications.toasts.addError(error, { + title: 'Error', + }); + } + } + }; + + useEffect( + function triggerOnKeyChangeEffect() { + if (onKeyChange) { + onKeyChange(selectedEnrollmentApiKey); + } + }, + [onKeyChange, selectedEnrollmentApiKey] + ); + + useEffect( + function useEnrollmentKeysForAgentPolicyEffect() { + if (!agentPolicyId) { + setIsAuthenticationSettingsOpen(true); + setEnrollmentAPIKeys([]); + return; + } + + async function fetchEnrollmentAPIKeys() { + try { + const res = await sendGetEnrollmentAPIKeys({ + page: 1, + perPage: SO_SEARCH_LIMIT, + }); + if (res.error) { + throw res.error; + } + + if (!res.data) { + throw new Error('No data while fetching enrollment API keys'); + } + + setEnrollmentAPIKeys( + res.data.list.filter((key) => key.policy_id === agentPolicyId && key.active === true) + ); + } catch (error) { + notifications.toasts.addError(error, { + title: 'Error', + }); + } + } + fetchEnrollmentAPIKeys(); + }, + [agentPolicyId, notifications.toasts] + ); + + useEffect( + function useDefaultEnrollmentKeyForAgentPolicyEffect() { + if ( + !selectedEnrollmentApiKey && + enrollmentAPIKeys.length > 0 && + enrollmentAPIKeys[0].policy_id === agentPolicyId + ) { + const enrollmentAPIKeyId = enrollmentAPIKeys[0].id; + setSelectedEnrollmentApiKey(enrollmentAPIKeyId); + } + }, + [enrollmentAPIKeys, selectedEnrollmentApiKey, agentPolicyId] + ); + return ( + <> + setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)} + > + + + {isAuthenticationSettingsOpen && ( + <> + + {enrollmentAPIKeys.length && selectedEnrollmentApiKey ? ( + ({ + value: key.id, + text: key.name, + }))} + value={selectedEnrollmentApiKey || undefined} + prepend={ + + + + } + onChange={(e) => { + setSelectedEnrollmentApiKey(e.target.value); + }} + /> + ) : ( + +
+ +
+ + + + +
+ )} + + )} + + ); +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts new file mode 100644 index 00000000000000..f1055e7e2583ee --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('../../hooks/use_request', () => { + const module = jest.requireActual('../../hooks/use_request'); + return { + ...module, + useGetSettings: jest.fn(), + sendGetFleetStatus: jest.fn(), + }; +}); + +jest.mock( + '../../applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page', + () => { + const module = jest.requireActual( + '../../applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page' + ); + return { + ...module, + FleetServerRequirementPage: jest.fn(), + useFleetServerInstructions: jest.fn(), + }; + } +); + +/** + * These steps functions use hooks inside useMemo which is not compatible with jest currently + */ +jest.mock('./steps', () => { + const module = jest.requireActual('./steps'); + return { + ...module, + AgentPolicySelectionStep: jest.fn(), + AgentEnrollmentKeySelectionStep: jest.fn(), + }; +}); + +jest.mock('@elastic/eui', () => { + const module = jest.requireActual('@elastic/eui'); + return { + ...module, + EuiSteps: 'eui-steps', + }; +}); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx new file mode 100644 index 00000000000000..db9245b11b0f99 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -0,0 +1,164 @@ +/* + * Copyright 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 './agent_enrollment_flyout.test.mocks'; + +import React from 'react'; +import { registerTestBed } from '@kbn/test/jest'; +import { act } from '@testing-library/react'; + +import { coreMock } from 'src/core/public/mocks'; + +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; + +import type { AgentPolicy } from '../../../common'; +import { useGetSettings, sendGetFleetStatus } from '../../hooks/use_request'; +import { FleetStatusProvider, ConfigContext } from '../../hooks'; + +import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page'; + +import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps'; + +import type { Props } from '.'; +import { AgentEnrollmentFlyout } from '.'; + +const TestComponent = (props: Props) => ( + + + + + + + +); + +const setup = async (props?: Props) => { + const testBed = await registerTestBed(TestComponent)(props); + const { find, component } = testBed; + + return { + ...testBed, + actions: { + goToStandaloneTab: () => + act(() => { + find('agentEnrollmentFlyout.standaloneTab').simulate('click'); + component.update(); + }), + }, + }; +}; + +type SetupReturn = ReturnType; +type TestBed = SetupReturn extends Promise ? U : SetupReturn; + +const testAgentPolicy: AgentPolicy = { + id: 'test', + is_managed: false, + namespace: 'test', + package_policies: [], + revision: 1, + status: 'active', + updated_at: 'test', + updated_by: 'test', + name: 'test', +}; + +describe('', () => { + let testBed: TestBed; + + beforeEach(async () => { + (useGetSettings as jest.Mock).mockReturnValue({ + data: { item: { fleet_server_hosts: ['test'] } }, + }); + + (sendGetFleetStatus as jest.Mock).mockResolvedValue({ + data: { isReady: true }, + }); + + (useFleetServerInstructions as jest.Mock).mockReturnValue({ + serviceToken: 'test', + getServiceToken: jest.fn(), + isLoadingServiceToken: false, + installCommand: jest.fn(), + platform: 'test', + setPlatform: jest.fn(), + }); + + await act(async () => { + testBed = await setup({ + agentPolicies: [], + onClose: jest.fn(), + }); + testBed.component.update(); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('managed instructions', () => { + it('uses the agent policy selection step', async () => { + const { exists } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(AgentPolicySelectionStep).toHaveBeenCalled(); + expect(AgentEnrollmentKeySelectionStep).not.toHaveBeenCalled(); + }); + + describe('with a specific policy', () => { + beforeEach(async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicy: testAgentPolicy, + onClose: jest.fn(), + }); + testBed.component.update(); + }); + }); + + it('uses the configure enrollment step, not the agent policy selection step', () => { + const { exists } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(AgentPolicySelectionStep).not.toHaveBeenCalled(); + expect(AgentEnrollmentKeySelectionStep).toHaveBeenCalled(); + }); + }); + }); + + describe('standalone instructions', () => { + it('uses the agent policy selection step', async () => { + const { exists, actions } = testBed; + actions.goToStandaloneTab(); + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(AgentPolicySelectionStep).toHaveBeenCalled(); + expect(AgentEnrollmentKeySelectionStep).not.toHaveBeenCalled(); + }); + + describe('with a specific policy', () => { + beforeEach(async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicy: testAgentPolicy, + onClose: jest.fn(), + }); + testBed.component.update(); + }); + }); + + it('does not use either of the agent policy selection or enrollment key steps', () => { + const { exists, actions } = testBed; + jest.clearAllMocks(); + expect(exists('agentEnrollmentFlyout')).toBe(true); + actions.goToStandaloneTab(); + expect(AgentPolicySelectionStep).not.toHaveBeenCalled(); + expect(AgentEnrollmentKeySelectionStep).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx index dc239213baf360..f92b2d48259351 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -8,17 +8,13 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonEmpty, EuiButton, EuiCallOut, EuiSelect, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiSelect, EuiSpacer, EuiText } from '@elastic/eui'; -import { SO_SEARCH_LIMIT } from '../../constants'; -import type { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../types'; -import { - sendGetEnrollmentAPIKeys, - useStartServices, - sendCreateEnrollmentAPIKey, -} from '../../hooks'; +import type { AgentPolicy } from '../../types'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; +import { AdvancedAgentAuthenticationSettings } from './advanced_agent_authentication_settings'; + type Props = { agentPolicies?: AgentPolicy[]; onAgentPolicyChange?: (key?: string) => void; @@ -33,131 +29,48 @@ type Props = { } ); +const resolveAgentId = ( + agentPolicies?: AgentPolicy[], + selectedAgentId?: string +): undefined | string => { + if (agentPolicies && agentPolicies.length && !selectedAgentId) { + if (agentPolicies.length === 1) { + return agentPolicies[0].id; + } + + const defaultAgentPolicy = agentPolicies.find((agentPolicy) => agentPolicy.is_default); + if (defaultAgentPolicy) { + return defaultAgentPolicy.id; + } + } + + return selectedAgentId; +}; + export const EnrollmentStepAgentPolicy: React.FC = (props) => { - const { notifications } = useStartServices(); const { withKeySelection, agentPolicies, onAgentPolicyChange, excludeFleetServer } = props; const onKeyChange = props.withKeySelection && props.onKeyChange; - - const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false); - const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( - [] + const [selectedAgentId, setSelectedAgentId] = useState( + () => resolveAgentId(agentPolicies, undefined) // no agent id selected yet ); - const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); - - const [selectedState, setSelectedState] = useState<{ - agentPolicyId?: string; - enrollmentAPIKeyId?: string; - }>({}); useEffect( function triggerOnAgentPolicyChangeEffect() { if (onAgentPolicyChange) { - onAgentPolicyChange(selectedState.agentPolicyId); - } - }, - [selectedState.agentPolicyId, onAgentPolicyChange] - ); - - useEffect( - function triggerOnKeyChangeEffect() { - if (!withKeySelection || !onKeyChange) { - return; - } - - if (onKeyChange) { - onKeyChange(selectedState.enrollmentAPIKeyId); + onAgentPolicyChange(selectedAgentId); } }, - [withKeySelection, onKeyChange, selectedState.enrollmentAPIKeyId] + [selectedAgentId, onAgentPolicyChange] ); useEffect( function useDefaultAgentPolicyEffect() { - if (agentPolicies && agentPolicies.length && !selectedState.agentPolicyId) { - if (agentPolicies.length === 1) { - setSelectedState({ - ...selectedState, - agentPolicyId: agentPolicies[0].id, - }); - return; - } - - const defaultAgentPolicy = agentPolicies.find((agentPolicy) => agentPolicy.is_default); - if (defaultAgentPolicy) { - setSelectedState({ - ...selectedState, - agentPolicyId: defaultAgentPolicy.id, - }); - } + const resolvedId = resolveAgentId(agentPolicies, selectedAgentId); + if (resolvedId !== selectedAgentId) { + setSelectedAgentId(resolvedId); } }, - [agentPolicies, selectedState] - ); - - useEffect( - function useEnrollmentKeysForAgentPolicyEffect() { - if (!withKeySelection) { - return; - } - if (!selectedState.agentPolicyId) { - setIsAuthenticationSettingsOpen(true); - setEnrollmentAPIKeys([]); - return; - } - - async function fetchEnrollmentAPIKeys() { - try { - const res = await sendGetEnrollmentAPIKeys({ - page: 1, - perPage: SO_SEARCH_LIMIT, - }); - if (res.error) { - throw res.error; - } - - if (!res.data) { - throw new Error('No data while fetching enrollment API keys'); - } - - setEnrollmentAPIKeys( - res.data.list.filter( - (key) => key.policy_id === selectedState.agentPolicyId && key.active === true - ) - ); - } catch (error) { - notifications.toasts.addError(error, { - title: 'Error', - }); - } - } - fetchEnrollmentAPIKeys(); - }, - [withKeySelection, selectedState.agentPolicyId, notifications.toasts] - ); - - useEffect( - function useDefaultEnrollmentKeyForAgentPolicyEffect() { - if (!withKeySelection) { - return; - } - if ( - !selectedState.enrollmentAPIKeyId && - enrollmentAPIKeys.length > 0 && - enrollmentAPIKeys[0].policy_id === selectedState.agentPolicyId - ) { - const enrollmentAPIKeyId = enrollmentAPIKeys[0].id; - setSelectedState({ - agentPolicyId: selectedState.agentPolicyId, - enrollmentAPIKeyId, - }); - } - }, - [ - withKeySelection, - enrollmentAPIKeys, - selectedState.enrollmentAPIKeyId, - selectedState.agentPolicyId, - ] + [agentPolicies, selectedAgentId] ); return ( @@ -177,125 +90,26 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { value: agentPolicy.id, text: agentPolicy.name, }))} - value={selectedState.agentPolicyId || undefined} - onChange={(e) => - setSelectedState({ - agentPolicyId: e.target.value, - enrollmentAPIKeyId: undefined, - }) - } + value={selectedAgentId || undefined} + onChange={(e) => setSelectedAgentId(e.target.value)} aria-label={i18n.translate('xpack.fleet.enrollmentStepAgentPolicy.policySelectAriaLabel', { defaultMessage: 'Agent policy', })} /> - {selectedState.agentPolicyId && ( + {selectedAgentId && ( )} {withKeySelection && onKeyChange && ( <> - - setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)} - > - - - {isAuthenticationSettingsOpen && ( - <> - - {enrollmentAPIKeys.length && selectedState.enrollmentAPIKeyId ? ( - ({ - value: key.id, - text: key.name, - }))} - value={selectedState.enrollmentAPIKeyId || undefined} - prepend={ - - - - } - onChange={(e) => { - setSelectedState({ - ...selectedState, - enrollmentAPIKeyId: e.target.value, - }); - }} - /> - ) : ( - -
- -
- - { - setIsLoadingEnrollmentKey(true); - if (selectedState.agentPolicyId) { - sendCreateEnrollmentAPIKey({ policy_id: selectedState.agentPolicyId }) - .then((res) => { - if (res.error) { - throw res.error; - } - setIsLoadingEnrollmentKey(false); - if (res.data?.item) { - setEnrollmentAPIKeys([res.data.item]); - setSelectedState({ - ...selectedState, - enrollmentAPIKeyId: res.data.item.id, - }); - notifications.toasts.addSuccess( - i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { - defaultMessage: 'Enrollment token created', - }) - ); - } - }) - .catch((error) => { - setIsLoadingEnrollmentKey(false); - notifications.toasts.addError(error, { - title: 'Error', - }); - }); - } - }} - > - - -
- )} - - )} + + )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 84f881e8baa0cc..b91af80691033e 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -16,25 +16,21 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, - EuiButton, EuiFlyoutFooter, EuiTab, EuiTabs, - EuiCallOut, - EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { useGetSettings, useUrlModal } from '../../hooks'; -import type { AgentPolicy } from '../../types'; import { ManagedInstructions } from './managed_instructions'; import { StandaloneInstructions } from './standalone_instructions'; +import { MissingFleetServerHostCallout } from './missing_fleet_server_host_callout'; +import type { BaseProps } from './types'; -interface Props { +export interface Props extends BaseProps { onClose: () => void; - agentPolicies?: AgentPolicy[]; } export * from './agent_policy_selection'; @@ -42,51 +38,9 @@ export * from './managed_instructions'; export * from './standalone_instructions'; export * from './steps'; -const MissingFleetServerHostCallout: React.FunctionComponent = () => { - const { setModal } = useUrlModal(); - return ( - - - - - ), - }} - /> - - { - setModal('settings'); - }} - > - - - - ); -}; - export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, + agentPolicy, agentPolicies, }) => { const [mode, setMode] = useState<'managed' | 'standalone'>('managed'); @@ -105,7 +59,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ }, [modal, lastModal, settings]); return ( - +

@@ -124,13 +78,21 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ - setMode('managed')}> + setMode('managed')} + > - setMode('standalone')}> + setMode('standalone')} + > = ({ } > {fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? ( - + ) : ( - + )} - + - + - - - - - diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index 5f2e0d6e4c4145..2bb8586a11503d 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -11,8 +11,6 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { AgentPolicy } from '../../types'; -import { NewEnrollmentTokenModal } from '../../components'; import { useGetOneEnrollmentAPIKey, useGetSettings, useLink, useFleetStatus } from '../../hooks'; import { ManualInstructions } from '../../components/enrollment_instructions'; @@ -23,11 +21,10 @@ import { useFleetServerInstructions, } from '../../applications/fleet/sections/agents/agent_requirements_page'; -import { DownloadStep, AgentPolicySelectionStep } from './steps'; +import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps'; +import type { BaseProps } from './types'; -interface Props { - agentPolicies?: AgentPolicy[]; -} +type Props = BaseProps; const DefaultMissingRequirements = () => { const { getHref } = useLink(); @@ -56,7 +53,7 @@ const FleetServerMissingRequirements = () => { return ; }; -export const ManagedInstructions = React.memo(({ agentPolicies }) => { +export const ManagedInstructions = React.memo(({ agentPolicy, agentPolicies }) => { const fleetStatus = useFleetStatus(); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); @@ -78,11 +75,13 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; const baseSteps: EuiContainedStepProps[] = [ DownloadStep(), - AgentPolicySelectionStep({ - agentPolicies, - setSelectedAPIKeyId, - setIsFleetServerPolicySelected, - }), + !agentPolicy + ? AgentPolicySelectionStep({ + agentPolicies, + setSelectedAPIKeyId, + setIsFleetServerPolicySelected, + }) + : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), ]; if (isFleetServerPolicySelected) { baseSteps.push( @@ -103,6 +102,7 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { } return baseSteps; }, [ + agentPolicy, agentPolicies, selectedAPIKeyId, apiKey.data, @@ -111,11 +111,6 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { fleetServerInstructions, ]); - const [isModalOpen, setModalOpen] = useState(false); - const closeModal = () => { - setModalOpen(false); - }; - return ( <> {fleetStatus.isReady ? ( @@ -128,10 +123,6 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { - - {isModalOpen && ( - - )} ) : fleetStatus.missingRequirements?.length === 1 && fleetStatus.missingRequirements[0] === 'fleet_server' ? ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx new file mode 100644 index 00000000000000..636032552a1ae8 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut, EuiLink, EuiButton, EuiSpacer } from '@elastic/eui'; + +import { useUrlModal } from '../../hooks'; + +export const MissingFleetServerHostCallout: React.FunctionComponent = () => { + const { setModal } = useUrlModal(); + return ( + + + + + ), + }} + /> + + { + setModal('settings'); + }} + > + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index 0fd846b074f98c..59898b9190c00d 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -22,24 +22,22 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { AgentPolicy } from '../../types'; import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../hooks'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; +import type { BaseProps } from './types'; -interface Props { - agentPolicies?: AgentPolicy[]; -} +type Props = BaseProps; const RUN_INSTRUCTIONS = './elastic-agent install'; -export const StandaloneInstructions = React.memo(({ agentPolicies }) => { +export const StandaloneInstructions = React.memo(({ agentPolicy, agentPolicies }) => { const { getHref } = useLink(); const core = useStartServices(); const { notifications } = core; - const [selectedPolicyId, setSelectedPolicyId] = useState(); + const [selectedPolicyId, setSelectedPolicyId] = useState(agentPolicy?.id); const [fullAgentPolicy, setFullAgentPolicy] = useState(); const downloadLink = selectedPolicyId @@ -74,9 +72,11 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { }, [selectedPolicyId, notifications.toasts]); const yaml = useMemo(() => fullAgentPolicyToYaml(fullAgentPolicy), [fullAgentPolicy]); - const steps: EuiContainedStepProps[] = [ + const steps = [ DownloadStep(), - AgentPolicySelectionStep({ agentPolicies, setSelectedPolicyId, excludeFleetServer: true }), + !agentPolicy + ? AgentPolicySelectionStep({ agentPolicies, setSelectedPolicyId, excludeFleetServer: true }) + : undefined, { title: i18n.translate('xpack.fleet.agentEnrollment.stepConfigureAgentTitle', { defaultMessage: 'Configure the agent', @@ -178,7 +178,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { ), }, - ]; + ].filter(Boolean) as EuiContainedStepProps[]; return ( <> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index fc22cbdd047c3b..ea4fa626afbb6a 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -15,6 +15,7 @@ import { sendGetOneAgentPolicy } from '../../hooks'; import { FLEET_SERVER_PACKAGE } from '../../constants'; import { EnrollmentStepAgentPolicy } from './agent_policy_selection'; +import { AdvancedAgentAuthenticationSettings } from './advanced_agent_authentication_settings'; export const DownloadStep = () => { return { @@ -59,12 +60,14 @@ export const AgentPolicySelectionStep = ({ setIsFleetServerPolicySelected?: (selected: boolean) => void; excludeFleetServer?: boolean; }) => { - const regularAgentPolicies = Array.isArray(agentPolicies) - ? agentPolicies.filter( - (policy) => - policy && !policy.is_managed && (!excludeFleetServer || !policy.is_default_fleet_server) - ) - : []; + const regularAgentPolicies = useMemo(() => { + return Array.isArray(agentPolicies) + ? agentPolicies.filter( + (policy) => + policy && !policy.is_managed && (!excludeFleetServer || !policy.is_default_fleet_server) + ) + : []; + }, [agentPolicies, excludeFleetServer]); const onAgentPolicyChange = useCallback( async (policyId?: string) => { @@ -103,3 +106,35 @@ export const AgentPolicySelectionStep = ({ ), }; }; + +export const AgentEnrollmentKeySelectionStep = ({ + agentPolicy, + setSelectedAPIKeyId, +}: { + agentPolicy: AgentPolicy; + setSelectedAPIKeyId: (key?: string) => void; +}) => { + return { + title: i18n.translate('xpack.fleet.agentEnrollment.stepConfigurePolicyAuthenticationTitle', { + defaultMessage: 'Configure agent authentication', + }), + children: ( + <> + + {agentPolicy.name}, + }} + /> + + + + + ), + }; +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts new file mode 100644 index 00000000000000..b9bcf8fb3e4b27 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentPolicy } from '../../types'; + +export interface BaseProps { + /** + * The user selected policy to be used + */ + agentPolicy?: AgentPolicy; + + /** + * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided + */ + agentPolicies?: AgentPolicy[]; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 573e101a7a63b9..13ec1efeb1bec1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8795,8 +8795,6 @@ "xpack.fleet.agentDetailsIntegrations.viewLogsButton": "ログを表示", "xpack.fleet.agentEnrollment.agentDescription": "Elastic エージェントをホストに追加し、データを収集して、Elastic Stack に送信します。", "xpack.fleet.agentEnrollment.agentsNotInitializedText": "エージェントを登録する前に、{link}。", - "xpack.fleet.agentEnrollment.cancelButtonLabel": "キャンセル", - "xpack.fleet.agentEnrollment.continueButtonLabel": "続行", "xpack.fleet.agentEnrollment.copyPolicyButton": "クリップボードにコピー", "xpack.fleet.agentEnrollment.copyRunInstructionsButton": "クリップボードにコピー", "xpack.fleet.agentEnrollment.downloadDescription": "Elasticエージェントダウンロードページでは、エージェントバイナリと検証署名をダウンロードできます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7ae9f30b8a394d..efa055e06bc405 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8872,8 +8872,6 @@ "xpack.fleet.agentDetailsIntegrations.viewLogsButton": "查看日志", "xpack.fleet.agentEnrollment.agentDescription": "将 Elastic 代理添加到您的主机,以收集数据并将其发送到 Elastic Stack。", "xpack.fleet.agentEnrollment.agentsNotInitializedText": "注册代理前,请{link}。", - "xpack.fleet.agentEnrollment.cancelButtonLabel": "取消", - "xpack.fleet.agentEnrollment.continueButtonLabel": "继续", "xpack.fleet.agentEnrollment.copyPolicyButton": "复制到剪贴板", "xpack.fleet.agentEnrollment.copyRunInstructionsButton": "复制到剪贴板", "xpack.fleet.agentEnrollment.downloadDescription": "可从 Elastic 代理下载页面下载代理二进制文件及其验证签名。", From 00089bf85a3f249001e00192274bd79ec060be5f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 10 Jun 2021 12:00:38 -0500 Subject: [PATCH 21/99] skip flaky runtime fields test. #100966 --- test/functional/apps/discover/_runtime_fields_editor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index fd2ca4dd4b5cf1..648fa3efe337c0 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.save(); }; - describe('discover integration with runtime fields editor', function describeIndexTests() { + describe.skip('discover integration with runtime fields editor', function describeIndexTests() { before(async function () { await esArchiver.load('test/functional/fixtures/es_archiver/discover'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); @@ -104,6 +104,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + // flaky https://github.com/elastic/kibana/issues/100966 it('doc view includes runtime fields', async function () { // navigate to doc view const table = await PageObjects.discover.getDocTable(); From 5ee5720cb7ecf44889c4206c8fd8d4f36740dab6 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 10 Jun 2021 12:03:37 -0500 Subject: [PATCH 22/99] [plugin cli] Remove settings argument from warnings log (#101824) The plugin CLI watches for warnings with a function taking the logger as an argument. There are two cases where we're passing a settings object as the first argument, causing syntax errors instead of properly logging. This removes the extra argument. --- src/cli_plugin/install/index.js | 2 +- src/cli_plugin/remove/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index 2683dd41d2bb32..dbad6bc8ba19c9 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -24,7 +24,7 @@ function processCommand(command, options) { const logger = new Logger(settings); - logWarnings(settings, logger); + logWarnings(logger); install(settings, logger); } diff --git a/src/cli_plugin/remove/index.js b/src/cli_plugin/remove/index.js index 329f506520c245..3f571e605028f3 100644 --- a/src/cli_plugin/remove/index.js +++ b/src/cli_plugin/remove/index.js @@ -24,7 +24,7 @@ function processCommand(command, options) { const logger = new Logger(settings); - logWarnings(settings, logger); + logWarnings(logger); remove(settings, logger); } From a9a834a105ae91d420fe6e93abab702fbe562af2 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Thu, 10 Jun 2021 14:20:30 -0300 Subject: [PATCH 23/99] [Workplace Search] Add Account Settings page imported from Security plugin (#99791) * Copy lazy_wrapper and suspense_error_boundary from Spaces plugin These components are needed to enable async loading of Security components into Enterprise Search. The components are copied without any changes except for i18n ids, so it's easier to DRY out in the future if needed. * Create async versions of personal_info and change_password components * Create ui_api that allows to load Security components asuncronously The patterns were mostly copied from Spaces plugin * Make ui_api available through Security components's lifecycle methods * Import Security plugin into Enterprise Search * Add Security plugin and Notifications service to Kibana Logic file * Export the required components from the Security plugin and use them in the new AccountSettings component * Update link to the Account Settings page * Move getUiApi call to security start and pass core instead of getStartServices * Simplify import of change_password_async component by providing... ... `notifications` and `userAPIClient` props in the security plugin * Remove UserAPIClient from ui_api It's not needed anymore since the components are initiated with this prop already passed * Export ChangePasswordProps and PersonalInfoProps from account_management/index.ts This makes it easier to import these props from outside the account_management folder * Remove notifications service from kibana_logic It is not needed anymore since we're initializing security components with notifications already provided * Add UiApi to SecurityPluginStart interface * Utilize index files for exporting Props types * Replace Pick<...> with two separate interfaces as it doesn't work well with our docs * Add a comment explaining why we're not loading async components through index file --- .../__mocks__/kea_logic/kibana_logic.mock.ts | 3 ++ .../public/applications/index.test.tsx | 2 + .../public/applications/index.tsx | 1 + .../shared/kibana/kibana_logic.ts | 3 ++ .../layout/account_header/account_header.tsx | 5 +- .../applications/workplace_search/index.tsx | 7 +++ .../applications/workplace_search/routes.ts | 1 - .../account_settings/account_settings.tsx | 39 ++++++++++++++ .../views/account_settings/index.ts | 8 +++ .../enterprise_search/public/plugin.ts | 3 ++ .../change_password/change_password.tsx | 7 ++- .../change_password/change_password_async.tsx | 29 ++++++++++ .../change_password/index.ts | 2 + .../public/account_management/index.ts | 3 ++ .../account_management/personal_info/index.ts | 2 + .../personal_info/personal_info.tsx | 4 +- .../personal_info/personal_info_async.tsx | 17 ++++++ x-pack/plugins/security/public/mocks.ts | 2 + .../plugins/security/public/plugin.test.tsx | 6 +++ x-pack/plugins/security/public/plugin.tsx | 7 +++ .../public/suspense_error_boundary/index.ts | 8 +++ .../suspense_error_boundary.tsx | 54 +++++++++++++++++++ .../security/public/ui_api/components.tsx | 43 +++++++++++++++ .../security/public/ui_api/index.mock.ts | 17 ++++++ .../plugins/security/public/ui_api/index.ts | 34 ++++++++++++ .../security/public/ui_api/lazy_wrapper.tsx | 39 ++++++++++++++ 26 files changed, 338 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts create mode 100644 x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx create mode 100644 x-pack/plugins/security/public/account_management/personal_info/personal_info_async.tsx create mode 100644 x-pack/plugins/security/public/suspense_error_boundary/index.ts create mode 100644 x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx create mode 100644 x-pack/plugins/security/public/ui_api/components.tsx create mode 100644 x-pack/plugins/security/public/ui_api/index.mock.ts create mode 100644 x-pack/plugins/security/public/ui_api/index.ts create mode 100644 x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index ebb6f8c4fe5aad..f2c6ccfacf2bbf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -7,6 +7,8 @@ import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; +import { securityMock } from '../../../../../security/public/mocks'; + import { mockHistory } from '../react_router/state.mock'; export const mockKibanaValues = { @@ -18,6 +20,7 @@ export const mockKibanaValues = { }, history: mockHistory, navigateToUrl: jest.fn(), + security: securityMock.createStart(), setBreadcrumbs: jest.fn(), setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 2e0940b9c4af22..cb73686eb4c5ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -12,6 +12,7 @@ import { getContext } from 'kea'; import { coreMock } from '../../../../../src/core/public/mocks'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { licensingMock } from '../../../licensing/public/mocks'; +import { securityMock } from '../../../security/public/mocks'; import { AppSearch } from './app_search'; import { EnterpriseSearch } from './enterprise_search'; @@ -27,6 +28,7 @@ describe('renderApp', () => { plugins: { licensing: licensingMock.createStart(), charts: chartPluginMock.createStartContract(), + security: securityMock.createStart(), }, } as any; const pluginData = { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index c2bf77751528ae..ba2b28e64b9cf0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -48,6 +48,7 @@ export const renderApp = ( cloud: plugins.cloud || {}, history: params.history, navigateToUrl: core.application.navigateToUrl, + security: plugins.security || {}, setBreadcrumbs: core.chrome.setBreadcrumbs, setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 2bef7d373f1606..9c6db7d09f72c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -13,6 +13,7 @@ import { kea, MakeLogicType } from 'kea'; import { ApplicationStart, ChromeBreadcrumb } from '../../../../../../../src/core/public'; import { ChartsPluginStart } from '../../../../../../../src/plugins/charts/public'; import { CloudSetup } from '../../../../../cloud/public'; +import { SecurityPluginStart } from '../../../../../security/public'; import { HttpLogic } from '../http'; import { createHref, CreateHrefOptions } from '../react_router_helpers'; @@ -23,6 +24,7 @@ interface KibanaLogicProps { cloud: Partial; charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; + security: Partial; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; @@ -47,6 +49,7 @@ export const KibanaLogic = kea>({ }, {}, ], + security: [props.security || {}, {}], setBreadcrumbs: [props.setBreadcrumbs, {}], setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx index 92a936fcdbefe7..b1e190edade2b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx @@ -27,7 +27,7 @@ import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url' import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import { WORKPLACE_SEARCH_TITLE, ACCOUNT_NAV } from '../../../constants'; -import { PERSONAL_SOURCES_PATH, LOGOUT_ROUTE, KIBANA_ACCOUNT_ROUTE } from '../../../routes'; +import { PERSONAL_SOURCES_PATH, LOGOUT_ROUTE, PERSONAL_SETTINGS_PATH } from '../../../routes'; export const AccountHeader: React.FC = () => { const [isPopoverOpen, setPopover] = useState(false); @@ -44,8 +44,7 @@ export const AccountHeader: React.FC = () => { const accountNavItems = [ - {/* TODO: Once auth is completed, we need to have non-admins redirect to the self-hosted form */} - {ACCOUNT_NAV.SETTINGS} + {ACCOUNT_NAV.SETTINGS} , {ACCOUNT_NAV.LOGOUT} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index ba5fb7c9d377d4..0fc8a6e7c7c0d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -28,7 +28,9 @@ import { ORG_SETTINGS_PATH, ROLE_MAPPINGS_PATH, SECURITY_PATH, + PERSONAL_SETTINGS_PATH, } from './routes'; +import { AccountSettings } from './views/account_settings'; import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; @@ -103,6 +105,11 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + + + + } />} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 5e5d6e2c82b310..1fe8019c4b3646 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -13,7 +13,6 @@ export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; export const LOGOUT_ROUTE = '/logout'; -export const KIBANA_ACCOUNT_ROUTE = '/security/account'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx new file mode 100644 index 00000000000000..e28faaeec8993a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, useMemo } from 'react'; + +import { useValues } from 'kea'; + +import type { AuthenticatedUser } from '../../../../../../security/public'; +import { KibanaLogic } from '../../../shared/kibana/kibana_logic'; + +export const AccountSettings: React.FC = () => { + const { security } = useValues(KibanaLogic); + + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + security!.authc!.getCurrentUser().then(setCurrentUser); + }, [security.authc]); + + const PersonalInfo = useMemo(() => security!.uiApi!.components.getPersonalInfo, [security.uiApi]); + const ChangePassword = useMemo(() => security!.uiApi!.components.getChangePassword, [ + security.uiApi, + ]); + + if (!currentUser) { + return null; + } + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts new file mode 100644 index 00000000000000..016b8f721aea4c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AccountSettings } from './account_settings'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index e402d233da58db..6e521efc369df1 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -21,6 +21,7 @@ import { } from '../../../../src/plugins/home/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/public'; import { APP_SEARCH_PLUGIN, @@ -42,11 +43,13 @@ export interface ClientData extends InitialAppData { interface PluginsSetup { cloud?: CloudSetup; home?: HomePublicPluginSetup; + security?: SecurityPluginSetup; } export interface PluginsStart { cloud?: CloudSetup; licensing: LicensingPluginStart; charts: ChartsPluginStart; + security?: SecurityPluginStart; } export class EnterpriseSearchPlugin implements Plugin { diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 90d63d8b43bc79..ac0e284c8b9ad4 100644 --- a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -17,13 +17,16 @@ import { canUserChangePassword } from '../../../common/model'; import type { UserAPIClient } from '../../management/users'; import { ChangePasswordForm } from '../../management/users/components/change_password_form'; -interface Props { +export interface ChangePasswordProps { user: AuthenticatedUser; +} + +export interface ChangePasswordPropsInternal extends ChangePasswordProps { userAPIClient: PublicMethodsOf; notifications: NotificationsSetup; } -export class ChangePassword extends Component { +export class ChangePassword extends Component { public render() { const canChangePassword = canUserChangePassword(this.props.user); diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx new file mode 100644 index 00000000000000..a4ad769146e59c --- /dev/null +++ b/x-pack/plugins/security/public/account_management/change_password/change_password_async.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { CoreStart } from 'src/core/public'; + +import { UserAPIClient } from '../../management/users'; +import type { ChangePasswordProps } from './change_password'; + +export const getChangePasswordComponent = async ( + core: CoreStart +): Promise> => { + const { ChangePassword } = await import('./change_password'); + + return (props: ChangePasswordProps) => { + return ( + + ); + }; +}; diff --git a/x-pack/plugins/security/public/account_management/change_password/index.ts b/x-pack/plugins/security/public/account_management/change_password/index.ts index c73b497512cdf0..028d0f6cc74974 100644 --- a/x-pack/plugins/security/public/account_management/change_password/index.ts +++ b/x-pack/plugins/security/public/account_management/change_password/index.ts @@ -6,3 +6,5 @@ */ export { ChangePassword } from './change_password'; + +export type { ChangePasswordProps } from './change_password'; diff --git a/x-pack/plugins/security/public/account_management/index.ts b/x-pack/plugins/security/public/account_management/index.ts index bfba213c632d00..2d1045723a6e1b 100644 --- a/x-pack/plugins/security/public/account_management/index.ts +++ b/x-pack/plugins/security/public/account_management/index.ts @@ -6,3 +6,6 @@ */ export { accountManagementApp } from './account_management_app'; + +export type { ChangePasswordProps } from './change_password'; +export type { PersonalInfoProps } from './personal_info'; diff --git a/x-pack/plugins/security/public/account_management/personal_info/index.ts b/x-pack/plugins/security/public/account_management/personal_info/index.ts index a7d2873e853917..6dc6489afa8c5b 100644 --- a/x-pack/plugins/security/public/account_management/personal_info/index.ts +++ b/x-pack/plugins/security/public/account_management/personal_info/index.ts @@ -6,3 +6,5 @@ */ export { PersonalInfo } from './personal_info'; + +export type { PersonalInfoProps } from './personal_info'; diff --git a/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx index e9de2b8a69bfaa..20b21fc0c30ce6 100644 --- a/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx +++ b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx @@ -12,11 +12,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { AuthenticatedUser } from '../../../common/model'; -interface Props { +export interface PersonalInfoProps { user: AuthenticatedUser; } -export const PersonalInfo = (props: Props) => { +export const PersonalInfo = (props: PersonalInfoProps) => { return ( > => { + const { PersonalInfo } = await import('./personal_info'); + return (props: PersonalInfoProps) => { + return ; + }; +}; diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index 829c3ced9dddb3..b936f8d01cfd51 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -11,6 +11,7 @@ import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock'; import { authenticationMock } from './authentication/index.mock'; import { navControlServiceMock } from './nav_control/index.mock'; import { createSessionTimeoutMock } from './session/session_timeout.mock'; +import { getUiApiMock } from './ui_api/index.mock'; function createSetupMock() { return { @@ -23,6 +24,7 @@ function createStartMock() { return { authc: authenticationMock.createStart(), navControlService: navControlServiceMock.createStart(), + uiApi: getUiApiMock.createStart(), }; } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 9c31919bd5d295..c4c551e4bb5b5a 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -100,6 +100,12 @@ describe('Security Plugin', () => { features: {} as FeaturesPluginStart, }) ).toEqual({ + uiApi: { + components: { + getChangePassword: expect.any(Function), + getPersonalInfo: expect.any(Function), + }, + }, authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function), diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 69533ea534802b..fbb282ee246f93 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -30,6 +30,8 @@ import type { SecurityNavControlServiceStart } from './nav_control'; import { SecurityNavControlService } from './nav_control'; import { SecurityCheckupService } from './security_checkup'; import { SessionExpired, SessionTimeout, UnauthorizedResponseHttpInterceptor } from './session'; +import type { UiApi } from './ui_api'; +import { getUiApi } from './ui_api'; export interface PluginSetupDependencies { licensing: LicensingPluginSetup; @@ -150,6 +152,7 @@ export class SecurityPlugin } return { + uiApi: getUiApi({ core }), navControlService: this.navControlService.start({ core }), authc: this.authc as AuthenticationServiceStart, }; @@ -184,4 +187,8 @@ export interface SecurityPluginStart { * Exposes authentication information about the currently logged in user. */ authc: AuthenticationServiceStart; + /** + * Exposes UI components that will be loaded asynchronously. + */ + uiApi: UiApi; } diff --git a/x-pack/plugins/security/public/suspense_error_boundary/index.ts b/x-pack/plugins/security/public/suspense_error_boundary/index.ts new file mode 100644 index 00000000000000..061923c8445c2d --- /dev/null +++ b/x-pack/plugins/security/public/suspense_error_boundary/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SuspenseErrorBoundary } from './suspense_error_boundary'; diff --git a/x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx b/x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx new file mode 100644 index 00000000000000..313401ff45bd94 --- /dev/null +++ b/x-pack/plugins/security/public/suspense_error_boundary/suspense_error_boundary.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLoadingSpinner } from '@elastic/eui'; +import type { PropsWithChildren } from 'react'; +import React, { Component, Suspense } from 'react'; + +import { i18n } from '@kbn/i18n'; +import type { NotificationsStart } from 'src/core/public'; + +interface Props { + notifications: NotificationsStart; +} + +interface State { + error: Error | null; +} + +export class SuspenseErrorBoundary extends Component, State> { + state: State = { + error: null, + }; + + static getDerivedStateFromError(error: Error) { + // Update state so next render shows fallback UI. + return { error }; + } + + public componentDidCatch(error: Error) { + const { notifications } = this.props; + if (notifications) { + const title = i18n.translate('xpack.security.uiApi.errorBoundaryToastTitle', { + defaultMessage: 'Failed to load Kibana asset', + }); + const toastMessage = i18n.translate('xpack.security.uiApi.errorBoundaryToastMessage', { + defaultMessage: 'Reload page to continue.', + }); + notifications.toasts.addError(error, { title, toastMessage }); + } + } + + render() { + const { children, notifications } = this.props; + const { error } = this.state; + if (!notifications || error) { + return null; + } + return }>{children}; + } +} diff --git a/x-pack/plugins/security/public/ui_api/components.tsx b/x-pack/plugins/security/public/ui_api/components.tsx new file mode 100644 index 00000000000000..a488bc359b5383 --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/components.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC, PropsWithChildren, PropsWithRef } from 'react'; +import React from 'react'; + +import type { CoreStart } from 'src/core/public'; + +/** + * We're importing specific files here instead of passing them + * through the index file. It helps to keep the bundle size low. + * + * Importing async components through the index file increases the bundle size. + * It happens because the bundle starts to also include all the sync dependencies + * available through the index file. + */ +import { getChangePasswordComponent } from '../account_management/change_password/change_password_async'; +import { getPersonalInfoComponent } from '../account_management/personal_info/personal_info_async'; +import { LazyWrapper } from './lazy_wrapper'; + +export interface GetComponentsOptions { + core: CoreStart; +} + +export const getComponents = ({ core }: GetComponentsOptions) => { + /** + * Returns a function that creates a lazy-loading version of a component. + */ + function wrapLazy(fn: () => Promise>) { + return (props: JSX.IntrinsicAttributes & PropsWithRef>) => ( + + ); + } + + return { + getPersonalInfo: wrapLazy(getPersonalInfoComponent), + getChangePassword: wrapLazy(() => getChangePasswordComponent(core)), + }; +}; diff --git a/x-pack/plugins/security/public/ui_api/index.mock.ts b/x-pack/plugins/security/public/ui_api/index.mock.ts new file mode 100644 index 00000000000000..c35f9342be6caf --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UiApi } from './'; + +export const getUiApiMock = { + createStart: (): jest.Mocked => ({ + components: { + getPersonalInfo: jest.fn(), + getChangePassword: jest.fn(), + }, + }), +}; diff --git a/x-pack/plugins/security/public/ui_api/index.ts b/x-pack/plugins/security/public/ui_api/index.ts new file mode 100644 index 00000000000000..e53564074940ad --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ReactElement } from 'react'; + +import type { CoreStart } from 'src/core/public'; + +import type { ChangePasswordProps, PersonalInfoProps } from '../account_management'; +import { getComponents } from './components'; + +interface GetUiApiOptions { + core: CoreStart; +} + +type LazyComponentFn = (props: T) => ReactElement; + +export interface UiApi { + components: { + getPersonalInfo: LazyComponentFn; + getChangePassword: LazyComponentFn; + }; +} + +export const getUiApi = ({ core }: GetUiApiOptions): UiApi => { + const components = getComponents({ core }); + + return { + components, + }; +}; diff --git a/x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx b/x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx new file mode 100644 index 00000000000000..6a37b35df7327f --- /dev/null +++ b/x-pack/plugins/security/public/ui_api/lazy_wrapper.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC, PropsWithChildren, PropsWithRef, ReactElement } from 'react'; +import React, { lazy, useMemo } from 'react'; + +import type { CoreStart } from 'src/core/public'; + +import { SuspenseErrorBoundary } from '../suspense_error_boundary'; + +interface InternalProps { + fn: () => Promise>; + core: CoreStart; + props: JSX.IntrinsicAttributes & PropsWithRef>; +} + +export const LazyWrapper: (props: InternalProps) => ReactElement | null = ({ + fn, + core, + props, +}) => { + const { notifications } = core; + + const LazyComponent = useMemo(() => lazy(() => fn().then((x) => ({ default: x }))), [fn]); + + if (!notifications) { + return null; + } + + return ( + + + + ); +}; From d7d67df5ebebad90ce7d15c492950327a127b799 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 10 Jun 2021 12:36:11 -0500 Subject: [PATCH 24/99] [Enterprise Search] Create reusable Group and Engine assignment selectors (#101790) * Create AssignmentSelectors These components will be used in both the Role Mapping and User flyouts to create and edit role mappings and users, respectively * Implement AssignmentSelectors in components --- .../engine_assignment_selector.test.tsx | 101 ++++++++++++++++ .../engine_assignment_selector.tsx | 87 ++++++++++++++ .../role_mappings/role_mapping.test.tsx | 32 +---- .../components/role_mappings/role_mapping.tsx | 80 +------------ .../group_assignment_selector.test.tsx | 113 ++++++++++++++++++ .../group_assignment_selector.tsx | 78 ++++++++++++ .../views/role_mappings/role_mapping.test.tsx | 32 +---- .../views/role_mappings/role_mapping.tsx | 69 +---------- 8 files changed, 393 insertions(+), 199 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.test.tsx new file mode 100644 index 00000000000000..01c46e6423d706 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/react_router'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { engines } from '../../__mocks__/engines.mock'; + +import React from 'react'; + +import { waitFor } from '@testing-library/dom'; +import { shallow } from 'enzyme'; + +import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui'; + +import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { EngineAssignmentSelector } from './engine_assignment_selector'; + +describe('EngineAssignmentSelector', () => { + const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role; + const actions = { + initializeRoleMappings: jest.fn(), + initializeRoleMapping: jest.fn(), + handleSaveMapping: jest.fn(), + handleEngineSelectionChange: jest.fn(), + handleAccessAllEnginesChange: jest.fn(), + handleAttributeValueChange: jest.fn(), + handleAttributeSelectorChange: jest.fn(), + handleDeleteMapping: jest.fn(), + handleRoleChange: jest.fn(), + handleAuthProviderChange: jest.fn(), + resetState: jest.fn(), + }; + + const mockValues = { + attributes: [], + elasticsearchRoles: [], + hasAdvancedRoles: true, + dataLoading: false, + roleType: 'admin', + roleMappings: [asRoleMapping], + attributeValue: '', + attributeName: 'username', + availableEngines: engines, + selectedEngines: new Set(), + accessAllEngines: false, + availableAuthProviders: [], + multipleAuthProvidersConfig: true, + selectedAuthProviders: [], + myRole: { + availableRoleTypes: mockRole.ability.availableRoleTypes, + }, + roleMappingErrors: [], + }; + + beforeEach(() => { + setMockActions(actions); + setMockValues(mockValues); + }); + + it('renders', () => { + setMockValues({ ...mockValues, roleMapping: asRoleMapping }); + const wrapper = shallow(); + + expect(wrapper.find(EuiRadioGroup)).toHaveLength(1); + expect(wrapper.find(EuiComboBox)).toHaveLength(1); + }); + + it('sets initial selected state when accessAllEngines is true', () => { + setMockValues({ ...mockValues, accessAllEngines: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); + }); + + it('handles all/specific engines radio change', () => { + const wrapper = shallow(); + const radio = wrapper.find(EuiRadioGroup); + radio.simulate('change', { target: { checked: false } }); + + expect(actions.handleAccessAllEnginesChange).toHaveBeenCalledWith(false); + }); + + it('handles engine checkbox click', async () => { + const wrapper = shallow(); + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: engines[0].name, value: engines[0].name }]) + ); + wrapper.update(); + + expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith([engines[0].name]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.tsx new file mode 100644 index 00000000000000..bb40a1a4fa279d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/engine_assignment_selector.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup } from '@elastic/eui'; + +import { RoleOptionLabel } from '../../../shared/role_mapping'; + +import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; + +import { + ENGINE_REQUIRED_ERROR, + ALL_ENGINES_LABEL, + ALL_ENGINES_DESCRIPTION, + SPECIFIC_ENGINES_LABEL, + SPECIFIC_ENGINES_DESCRIPTION, + ENGINE_ASSIGNMENT_LABEL, +} from './constants'; +import { RoleMappingsLogic } from './role_mappings_logic'; + +export const EngineAssignmentSelector: React.FC = () => { + const { handleAccessAllEnginesChange, handleEngineSelectionChange } = useActions( + RoleMappingsLogic + ); + + const { + accessAllEngines, + availableEngines, + roleType, + selectedEngines, + selectedOptions, + } = useValues(RoleMappingsLogic); + + const hasEngineAssignment = selectedEngines.size > 0 || accessAllEngines; + + const engineOptions = [ + { + id: 'all', + label: , + }, + { + id: 'specific', + label: ( + + ), + }, + ]; + + return ( + <> + + + handleAccessAllEnginesChange(id === 'all')} + legend={{ + children: {ENGINE_ASSIGNMENT_LABEL}, + }} + /> + + + ({ label: name, value: name }))} + onChange={(options) => { + handleEngineSelectionChange(options.map(({ value }) => value as string)); + }} + fullWidth + isDisabled={accessAllEngines || !roleHasScopedEngines(roleType)} + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx index 0b7d4255fc3236..8932c17367b2c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx @@ -13,16 +13,14 @@ import { engines } from '../../__mocks__/engines.mock'; import React from 'react'; -import { waitFor } from '@testing-library/dom'; import { shallow } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui'; - import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping'; import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; import { STANDARD_ROLE_TYPES } from './constants'; +import { EngineAssignmentSelector } from './engine_assignment_selector'; import { RoleMapping } from './role_mapping'; describe('RoleMapping', () => { @@ -73,6 +71,7 @@ describe('RoleMapping', () => { expect(wrapper.find(AttributeSelector)).toHaveLength(1); expect(wrapper.find(RoleSelector)).toHaveLength(1); + expect(wrapper.find(EngineAssignmentSelector)).toHaveLength(1); }); it('only passes standard role options for non-advanced roles', () => { @@ -82,33 +81,6 @@ describe('RoleMapping', () => { expect(wrapper.find(RoleSelector).prop('roleOptions')).toHaveLength(STANDARD_ROLE_TYPES.length); }); - it('sets initial selected state when accessAllEngines is true', () => { - setMockValues({ ...mockValues, accessAllEngines: true }); - const wrapper = shallow(); - - expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); - }); - - it('handles all/specific engines radio change', () => { - const wrapper = shallow(); - const radio = wrapper.find(EuiRadioGroup); - radio.simulate('change', { target: { checked: false } }); - - expect(actions.handleAccessAllEnginesChange).toHaveBeenCalledWith(false); - }); - - it('handles engine checkbox click', async () => { - const wrapper = shallow(); - await waitFor(() => - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: engines[0].name, value: engines[0].name }]) - ); - wrapper.update(); - - expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith([engines[0].name]); - }); - it('enables flyout when attribute value is valid', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index 7962734f86e2dd..b6a9dd72cfd05e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -9,47 +9,23 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { - EuiComboBox, - EuiForm, - EuiFormRow, - EuiHorizontalRule, - EuiRadioGroup, - EuiSpacer, -} from '@elastic/eui'; +import { EuiForm, EuiSpacer } from '@elastic/eui'; -import { - AttributeSelector, - RoleSelector, - RoleOptionLabel, - RoleMappingFlyout, -} from '../../../shared/role_mapping'; +import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping'; import { AppLogic } from '../../app_logic'; import { AdvanceRoleType } from '../../types'; -import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; - -import { - ADVANCED_ROLE_TYPES, - STANDARD_ROLE_TYPES, - ENGINE_REQUIRED_ERROR, - ALL_ENGINES_LABEL, - ALL_ENGINES_DESCRIPTION, - SPECIFIC_ENGINES_LABEL, - SPECIFIC_ENGINES_DESCRIPTION, - ENGINE_ASSIGNMENT_LABEL, -} from './constants'; +import { ADVANCED_ROLE_TYPES, STANDARD_ROLE_TYPES } from './constants'; +import { EngineAssignmentSelector } from './engine_assignment_selector'; import { RoleMappingsLogic } from './role_mappings_logic'; export const RoleMapping: React.FC = () => { const { myRole } = useValues(AppLogic); const { - handleAccessAllEnginesChange, handleAttributeSelectorChange, handleAttributeValueChange, handleAuthProviderChange, - handleEngineSelectionChange, handleRoleChange, handleSaveMapping, closeRoleMappingFlyout, @@ -61,7 +37,6 @@ export const RoleMapping: React.FC = () => { attributeValue, attributes, availableAuthProviders, - availableEngines, elasticsearchRoles, hasAdvancedRoles, multipleAuthProvidersConfig, @@ -69,7 +44,6 @@ export const RoleMapping: React.FC = () => { roleType, selectedEngines, selectedAuthProviders, - selectedOptions, roleMappingErrors, } = useValues(RoleMappingsLogic); @@ -90,22 +64,6 @@ export const RoleMapping: React.FC = () => { ? [...standardRoleOptions, ...advancedRoleOptions] : standardRoleOptions; - const engineOptions = [ - { - id: 'all', - label: , - }, - { - id: 'specific', - label: ( - - ), - }, - ]; - return ( { onChange={handleRoleChange} label="Role" /> - - {hasAdvancedRoles && ( - <> - - - handleAccessAllEnginesChange(id === 'all')} - legend={{ - children: {ENGINE_ASSIGNMENT_LABEL}, - }} - /> - - - ({ label: name, value: name }))} - onChange={(options) => { - handleEngineSelectionChange(options.map(({ value }) => value as string)); - }} - fullWidth - isDisabled={accessAllEngines || !roleHasScopedEngines(roleType)} - /> - - - )} + {hasAdvancedRoles && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.test.tsx new file mode 100644 index 00000000000000..94c21f3b5524f6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/react_router'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { waitFor } from '@testing-library/dom'; +import { shallow } from 'enzyme'; + +import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui'; + +import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { GroupAssignmentSelector } from './group_assignment_selector'; + +describe('GroupAssignmentSelector', () => { + const initializeRoleMappings = jest.fn(); + const initializeRoleMapping = jest.fn(); + const handleSaveMapping = jest.fn(); + const handleGroupSelectionChange = jest.fn(); + const handleAllGroupsSelectionChange = jest.fn(); + const handleAttributeValueChange = jest.fn(); + const handleAttributeSelectorChange = jest.fn(); + const handleDeleteMapping = jest.fn(); + const handleRoleChange = jest.fn(); + const handleAuthProviderChange = jest.fn(); + const resetState = jest.fn(); + const groups = [ + { + name: 'Group 1', + id: 'g1', + }, + { + name: 'Group 2', + id: 'g2', + }, + ]; + const mockValues = { + attributes: [], + elasticsearchRoles: [], + dataLoading: false, + roleType: 'admin', + roleMappings: [wsRoleMapping], + attributeValue: '', + attributeName: 'username', + availableGroups: groups, + selectedGroups: new Set(), + includeInAllGroups: false, + availableAuthProviders: [], + multipleAuthProvidersConfig: true, + selectedAuthProviders: [], + roleMappingErrors: [], + }; + + beforeEach(() => { + setMockActions({ + initializeRoleMappings, + initializeRoleMapping, + handleSaveMapping, + handleGroupSelectionChange, + handleAllGroupsSelectionChange, + handleAttributeValueChange, + handleAttributeSelectorChange, + handleDeleteMapping, + handleRoleChange, + handleAuthProviderChange, + resetState, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + setMockValues({ ...mockValues, GroupAssignmentSelector: wsRoleMapping }); + const wrapper = shallow(); + + expect(wrapper.find(EuiRadioGroup)).toHaveLength(1); + expect(wrapper.find(EuiComboBox)).toHaveLength(1); + }); + + it('sets initial selected state when includeInAllGroups is true', () => { + setMockValues({ ...mockValues, includeInAllGroups: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); + }); + + it('handles all/specific groups radio change', () => { + const wrapper = shallow(); + const radio = wrapper.find(EuiRadioGroup); + radio.simulate('change', { target: { checked: false } }); + + expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(false); + }); + + it('handles group checkbox click', async () => { + const wrapper = shallow(); + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: groups[0].name, value: groups[0].name }]) + ); + wrapper.update(); + + expect(handleGroupSelectionChange).toHaveBeenCalledWith([groups[0].name]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx new file mode 100644 index 00000000000000..b53b092eced2db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup } from '@elastic/eui'; + +import { RoleOptionLabel } from '../../../shared/role_mapping'; + +import { + GROUP_ASSIGNMENT_INVALID_ERROR, + GROUP_ASSIGNMENT_LABEL, + ALL_GROUPS_LABEL, + ALL_GROUPS_DESCRIPTION, + SPECIFIC_GROUPS_LABEL, + SPECIFIC_GROUPS_DESCRIPTION, +} from './constants'; + +import { RoleMappingsLogic } from './role_mappings_logic'; + +export const GroupAssignmentSelector: React.FC = () => { + const { handleAllGroupsSelectionChange, handleGroupSelectionChange } = useActions( + RoleMappingsLogic + ); + + const { includeInAllGroups, availableGroups, selectedGroups, selectedOptions } = useValues( + RoleMappingsLogic + ); + + const hasGroupAssignment = selectedGroups.size > 0 || includeInAllGroups; + + const groupOptions = [ + { + id: 'all', + label: , + }, + { + id: 'specific', + label: ( + + ), + }, + ]; + + return ( + <> + + + handleAllGroupsSelectionChange(id === 'all')} + legend={{ + children: {GROUP_ASSIGNMENT_LABEL}, + }} + /> + + + ({ label: name, value: id }))} + onChange={(options) => { + handleGroupSelectionChange(options.map(({ value }) => value as string)); + }} + fullWidth + isDisabled={includeInAllGroups} + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx index 619a931864500a..d0f88984bd8c7b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx @@ -11,14 +11,12 @@ import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; import React from 'react'; -import { waitFor } from '@testing-library/dom'; import { shallow } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui'; - import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping'; import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { GroupAssignmentSelector } from './group_assignment_selector'; import { RoleMapping } from './role_mapping'; describe('RoleMapping', () => { @@ -83,33 +81,7 @@ describe('RoleMapping', () => { expect(wrapper.find(AttributeSelector)).toHaveLength(1); expect(wrapper.find(RoleSelector)).toHaveLength(1); - }); - - it('sets initial selected state when includeInAllGroups is true', () => { - setMockValues({ ...mockValues, includeInAllGroups: true }); - const wrapper = shallow(); - - expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); - }); - - it('handles all/specific groups radio change', () => { - const wrapper = shallow(); - const radio = wrapper.find(EuiRadioGroup); - radio.simulate('change', { target: { checked: false } }); - - expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(false); - }); - - it('handles group checkbox click', async () => { - const wrapper = shallow(); - await waitFor(() => - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ label: groups[0].name, value: groups[0].name }]) - ); - wrapper.update(); - - expect(handleGroupSelectionChange).toHaveBeenCalledWith([groups[0].name]); + expect(wrapper.find(GroupAssignmentSelector)).toHaveLength(1); }); it('enables flyout when attribute value is valid', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index e2f24ce392c397..cc773895bff1c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -9,35 +9,15 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { - EuiComboBox, - EuiForm, - EuiFormRow, - EuiHorizontalRule, - EuiRadioGroup, - EuiSpacer, -} from '@elastic/eui'; +import { EuiForm, EuiSpacer } from '@elastic/eui'; -import { - AttributeSelector, - RoleSelector, - RoleOptionLabel, - RoleMappingFlyout, -} from '../../../shared/role_mapping'; +import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping'; import { Role } from '../../types'; -import { - ADMIN_ROLE_TYPE_DESCRIPTION, - USER_ROLE_TYPE_DESCRIPTION, - GROUP_ASSIGNMENT_INVALID_ERROR, - GROUP_ASSIGNMENT_LABEL, - ALL_GROUPS_LABEL, - ALL_GROUPS_DESCRIPTION, - SPECIFIC_GROUPS_LABEL, - SPECIFIC_GROUPS_DESCRIPTION, -} from './constants'; +import { ADMIN_ROLE_TYPE_DESCRIPTION, USER_ROLE_TYPE_DESCRIPTION } from './constants'; +import { GroupAssignmentSelector } from './group_assignment_selector'; import { RoleMappingsLogic } from './role_mappings_logic'; interface RoleType { @@ -56,24 +36,9 @@ const roleOptions = [ }, ] as RoleType[]; -const groupOptions = [ - { - id: 'all', - label: , - }, - { - id: 'specific', - label: ( - - ), - }, -]; - export const RoleMapping: React.FC = () => { const { handleSaveMapping, - handleGroupSelectionChange, - handleAllGroupsSelectionChange, handleAttributeValueChange, handleAttributeSelectorChange, handleRoleChange, @@ -87,13 +52,11 @@ export const RoleMapping: React.FC = () => { roleType, attributeValue, attributeName, - availableGroups, selectedGroups, includeInAllGroups, availableAuthProviders, multipleAuthProvidersConfig, selectedAuthProviders, - selectedOptions, roleMapping, roleMappingErrors, } = useValues(RoleMappingsLogic); @@ -132,29 +95,7 @@ export const RoleMapping: React.FC = () => { onChange={handleRoleChange} label="Role" /> - - - handleAllGroupsSelectionChange(id === 'all')} - legend={{ - children: {GROUP_ASSIGNMENT_LABEL}, - }} - /> - - - ({ label: name, value: id }))} - onChange={(options) => { - handleGroupSelectionChange(options.map(({ value }) => value as string)); - }} - fullWidth - isDisabled={includeInAllGroups} - /> - + ); From 542fe02e1f0d72d7238d8b02680099de0d24f4f2 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 10 Jun 2021 13:42:55 -0400 Subject: [PATCH 25/99] Update aggregation reference docs for 7.13 (#101913) * Update aggregation reference docs for 7.13 * Add more reference about filtered metrics --- .../dashboard/aggregation-reference.asciidoc | 18 ++++++++++++------ docs/user/dashboard/lens.asciidoc | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc index 7d5547fe3c3c50..39e596df4af347 100644 --- a/docs/user/dashboard/aggregation-reference.asciidoc +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -188,6 +188,12 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | Type | Agg-based | Markdown | Lens | TSVB +| Metrics with filters +| +^| X +| +| + | Average ^| X ^| X @@ -221,7 +227,7 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | Percentiles ^| X ^| X -| +^| X ^| X | Percentiles Rank @@ -230,10 +236,10 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | ^| X -| Top hit +| Top hit (Last value) +^| X ^| X ^| X -| ^| X | Value count @@ -266,7 +272,7 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat | Derivative ^| X ^| X -| +^| X ^| X | Max bucket @@ -290,13 +296,13 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat | Moving average ^| X ^| X -| +^| X ^| X | Cumulative sum ^| X ^| X -| +^| X ^| X | Bucket script diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 3b3a7a9ee527d4..9f17a380bc209a 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -147,14 +147,24 @@ For the answers to common *Lens* questions, review the following. [float] [[kql-]] -===== When should I use the Filter function instead of KQL filters? +===== When should I use the top filter bar, filters function, or "Filter by"? -The easiest way to apply KQL filters is to use <>, but you can also use the *Filters* function in the following scenarios: +Using the top <> bar is best when you want to focus on a known set of +data for all the visualization results. These top level filters are combined with other filters +using AND logic. + +Use the *Filters* function in the following scenarios: * When you want to apply more than one KQL filter to the visualization. * When you want to apply the KQL filter to a single layer, which allows you to visualize filtered and unfiltered data. +Use the *Filter by* advanced option in the following scenarios: + +* When you want to assign a custom color to each filter in a bar, line or area chart. + +* When you want to build a complex table, such as showing both failure rate and overall. + [float] [[when-should-i-normalize-the-data-by-unit-or-use-a-custom-interval]] ===== When should I normalize the data by unit or use a custom interval? From ddccace87ead2edc24cf245f796c79a6618dbe4d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 10 Jun 2021 20:01:21 +0200 Subject: [PATCH 26/99] [Uptime] Unskip alert functional test (#101562) --- .../alerts/alert_query_bar/query_bar.tsx | 2 +- .../alert_monitor_status.tsx | 23 +++++++++++-------- .../apps/uptime/alert_flyout.ts | 11 +++++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx index 6293dc2ec1d18b..0a0bbadb6216f0 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx @@ -46,7 +46,7 @@ export const AlertQueryBar = ({ query = '', onChange }: Props) => { }} query={{ query: inputVal, language: 'kuery' }} aria-label={labels.ALERT_KUERY_BAR_ARIA} - data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar" + dataTestSubj="xpack.uptime.alerts.monitorStatus.filterBar" autoSubmit={true} disableLanguageSwitcher={true} isInvalid={!!(inputVal && !query)} diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx index d55f3fb336a9d8..ff2ef4d2359a8e 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx @@ -38,16 +38,19 @@ export const AlertMonitorStatus: React.FC = ({ }) => { const dispatch = useDispatch(); useEffect(() => { - dispatch( - fetchOverviewFilters({ - dateRangeStart: 'now-24h', - dateRangeEnd: 'now', - locations: alertParams.filters?.['observer.geo.name'] ?? [], - ports: alertParams.filters?.['url.port'] ?? [], - tags: alertParams.filters?.tags ?? [], - schemes: alertParams.filters?.['monitor.type'] ?? [], - }) - ); + if (!window.location.pathname.includes('/app/uptime')) { + // filters inside uptime app already loaded + dispatch( + fetchOverviewFilters({ + dateRangeStart: 'now-24h', + dateRangeEnd: 'now', + locations: alertParams.filters?.['observer.geo.name'] ?? [], + ports: alertParams.filters?.['url.port'] ?? [], + tags: alertParams.filters?.tags ?? [], + schemes: alertParams.filters?.['monitor.type'] ?? [], + }) + ); + } }, [alertParams, dispatch]); const overviewFilters = useSelector(overviewFiltersSelector); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index a631bf3781d09a..7d235d9e181082 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; +import { delay } from 'bluebird'; + import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - // FLAKY: https://github.com/elastic/kibana/issues/88177 - describe.skip('uptime alerts', () => { + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); @@ -91,11 +92,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // put the fetch code in a retry block with a timeout. let alert: any; await retry.tryForTime(60 * 1000, async () => { + // add a delay before next call to not overload the server + await delay(1500); const apiResponse = await supertest.get('/api/alerts/_find?search=uptime-test'); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === 'uptime-test' ); - expect(alertsFromThisTest).to.have.length(1); + expect(alertsFromThisTest.length >= 1).to.be(true); alert = alertsFromThisTest[0]; }); @@ -124,7 +127,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(timerangeUnit).to.be('h'); expect(timerangeCount).to.be(1); expect(JSON.stringify(filters)).to.eql( - `{"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"],"tags":[]}` + `{"tags":[],"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"]}` ); } finally { await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); From 61677f7a774e0abf493d818915c3d90346412806 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Thu, 10 Jun 2021 14:18:35 -0400 Subject: [PATCH 27/99] Fix AppContainer layout (#101793) * Fixing app_container * Use EUI mixin `euiHeaderAffordForFixed()` to maintain parity * Adding `aria-busy` when `showSpinner` * i18n for the loading aria-label --- .../integration_tests/router.test.tsx | 16 ++++++++-------- .../public/application/ui/app_container.scss | 2 +- .../public/application/ui/app_container.tsx | 18 ++++++++++++------ src/core/public/rendering/_base.scss | 17 +---------------- src/core/server/rendering/views/styles.tsx | 1 - .../functional/page_objects/security_page.ts | 5 +---- .../tests/oidc/url_capture.ts | 5 +---- .../tests/saml/url_capture.ts | 5 +---- 8 files changed, 25 insertions(+), 44 deletions(-) diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 55c18e49cba131..2543d22ee6d31b 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -96,7 +96,7 @@ describe('AppRouter', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -108,7 +108,7 @@ describe('AppRouter', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app2 html:
App 2
" @@ -122,7 +122,7 @@ describe('AppRouter', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -134,7 +134,7 @@ describe('AppRouter', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -146,7 +146,7 @@ describe('AppRouter', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -160,7 +160,7 @@ describe('AppRouter', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -172,7 +172,7 @@ describe('AppRouter', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-b/path html:
Chromeless B
" @@ -184,7 +184,7 @@ describe('AppRouter', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" diff --git a/src/core/public/application/ui/app_container.scss b/src/core/public/application/ui/app_container.scss index 4f8fec10a97e15..d30db740505d18 100644 --- a/src/core/public/application/ui/app_container.scss +++ b/src/core/public/application/ui/app_container.scss @@ -1,5 +1,5 @@ .appContainer__loading { - position: fixed; + position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 02d321095b3735..0312c707e10498 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import './app_container.scss'; import React, { Fragment, @@ -16,11 +17,12 @@ import React, { } from 'react'; import { EuiLoadingElastic } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; import { ScopedHistory } from '../scoped_history'; -import './app_container.scss'; +import { APP_WRAPPER_CLASS } from '../../../utils'; interface Props { /** Path application is mounted on without the global basePath */ @@ -107,12 +109,16 @@ export const AppContainer: FunctionComponent = ({ return ( {appNotFound && } - {showSpinner && ( -
- -
+ {showSpinner && !appNotFound && ( + )} -
+
); }; diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 3a748f3ceb6fd3..4bd6afe90d3429 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -35,27 +35,12 @@ position: relative; // This is temporary for apps that relied on this being present on `.application` } -// TODO: This is problematic because it doesn't stay in line with EUI: -// adapted from euiHeaderAffordForFixed as we need to handle the top banner @mixin kbnAffordForHeader($headerHeight) { - padding-top: $headerHeight; + @include euiHeaderAffordForFixed($headerHeight); #app-fixed-viewport { top: $headerHeight; } - - .euiFlyout, - .euiCollapsibleNav { - top: $headerHeight; - height: calc(100% - #{$headerHeight}); - } - - @include euiBreakpoint('m', 'l', 'xl') { - .euiPageSideBar--sticky { - max-height: calc(100vh - #{$headerHeight}); - top: #{$headerHeight}; - } - } } .kbnBody { diff --git a/src/core/server/rendering/views/styles.tsx b/src/core/server/rendering/views/styles.tsx index 105f94df9218fc..fbeab4fb4388fa 100644 --- a/src/core/server/rendering/views/styles.tsx +++ b/src/core/server/rendering/views/styles.tsx @@ -52,7 +52,6 @@ const InlineStyles: FC<{ darkMode: boolean }> = ({ darkMode }) => { .kbnWelcomeView { line-height: 1.5; - background-color: ${darkMode ? '#1D1E24' : '#FFF'}; height: 100%; display: -webkit-box; display: -webkit-flex; diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 437749d31c15e3..85382465d974ed 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -207,10 +207,7 @@ export class SecurityPageObject extends FtrService { } if (expectedResult === 'chrome') { - await this.find.byCssSelector( - '[data-test-subj="kibanaChrome"] .kbnAppWrapper:not(.kbnAppWrapper--hiddenChrome)', - 20000 - ); + await this.find.byCssSelector('[data-test-subj="userMenuButton"]', 20000); this.log.debug(`Finished login process currentUrl = ${await this.browser.getCurrentUrl()}`); } } diff --git a/x-pack/test/security_functional/tests/oidc/url_capture.ts b/x-pack/test/security_functional/tests/oidc/url_capture.ts index 4c6b68cc3757c6..706189dcb5a2a9 100644 --- a/x-pack/test/security_functional/tests/oidc/url_capture.ts +++ b/x-pack/test/security_functional/tests/oidc/url_capture.ts @@ -43,10 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { deployment.getHostPort() + '/app/management/security/users#some=hash-value' ); - await find.byCssSelector( - '[data-test-subj="kibanaChrome"] .kbnAppWrapper:not(.kbnAppWrapper--hiddenChrome)', - 20000 - ); + await find.byCssSelector('[data-test-subj="userMenuButton"]', 20000); // We need to make sure that both path and hash are respected. const currentURL = parse(await browser.getCurrentUrl()); diff --git a/x-pack/test/security_functional/tests/saml/url_capture.ts b/x-pack/test/security_functional/tests/saml/url_capture.ts index 65d7688472539b..2b0b8690ebab66 100644 --- a/x-pack/test/security_functional/tests/saml/url_capture.ts +++ b/x-pack/test/security_functional/tests/saml/url_capture.ts @@ -43,10 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { deployment.getHostPort() + '/app/management/security/users#some=hash-value' ); - await find.byCssSelector( - '[data-test-subj="kibanaChrome"] .kbnAppWrapper:not(.kbnAppWrapper--hiddenChrome)', - 20000 - ); + await find.byCssSelector('[data-test-subj="userMenuButton"]', 20000); // We need to make sure that both path and hash are respected. const currentURL = parse(await browser.getCurrentUrl()); From 797c0c90b08c973ff7df4d56be42d4678caee41f Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 10 Jun 2021 13:20:54 -0500 Subject: [PATCH 28/99] [Enterprise Search] Refactor RoleMappingsTable to use EuiInMemoryTable (#101918) * Add shared actions component Both tables use the same actions * Refactor RoleMappingsTable to use EuiInMemoryTable This is way better than the bespoke one I wrote and it comes with pagination for free - Also fixes a typo in the i18n id --- .../shared/role_mapping/constants.ts | 5 + .../applications/shared/role_mapping/index.ts | 1 + .../role_mapping/role_mappings_table.test.tsx | 54 ++--- .../role_mapping/role_mappings_table.tsx | 208 ++++++++---------- .../users_and_roles_row_actions.test.tsx | 46 ++++ .../users_and_roles_row_actions.tsx | 24 ++ .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 8 files changed, 192 insertions(+), 150 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index be0c860627f799..47d481630510e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -200,3 +200,8 @@ export const ROLE_MAPPINGS_HEADING_BUTTON = i18n.translate( 'xpack.enterpriseSearch.roleMapping.roleMappingsHeadingButton', { defaultMessage: 'Create a new role mapping' } ); + +export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.noResults.message', + { defaultMessage: 'Create a new role mapping' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index 0f9362157f50ad..b0d10e9692714f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -11,3 +11,4 @@ export { RoleOptionLabel } from './role_option_label'; export { RoleSelector } from './role_selector'; export { RoleMappingFlyout } from './role_mapping_flyout'; export { RoleMappingsHeading } from './role_mappings_heading'; +export { UsersAndRolesRowActions } from './users_and_roles_row_actions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 5ec84db478bc3d..156b52a4016c32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -9,13 +9,14 @@ import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; import React from 'react'; -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; -import { EuiFieldSearch, EuiTableRow } from '@elastic/eui'; +import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui'; import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; import { RoleMappingsTable } from './role_mappings_table'; +import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; describe('RoleMappingsTable', () => { const initializeRoleMapping = jest.fn(); @@ -41,55 +42,44 @@ describe('RoleMappingsTable', () => { handleDeleteMapping, }; - it('renders', () => { - const wrapper = shallow(); + it('renders with "shouldShowAuthProvider" true', () => { + const wrapper = mount(); - expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); - expect(wrapper.find(EuiTableRow)).toHaveLength(1); + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(6); }); - it('renders auth provider display names', () => { - const wrapper = shallow(); + it('renders with "shouldShowAuthProvider" false', () => { + const wrapper = mount(); - expect(wrapper.find('[data-test-subj="AuthProviderDisplay"]').prop('children')).toEqual( - `${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth` - ); + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(5); }); - it('handles input change', () => { - const wrapper = shallow(); - const input = wrapper.find(EuiFieldSearch); - const value = 'Query'; - input.simulate('change', { target: { value } }); + it('renders auth provider display names', () => { + const wrapper = mount(); - expect(wrapper.find(EuiTableRow)).toHaveLength(0); + expect(wrapper.find('[data-test-subj="AuthProviderDisplayValue"]').prop('children')).toEqual( + `${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth` + ); }); it('handles manage click', () => { - const wrapper = shallow(); - wrapper.find('[data-test-subj="ManageButton"]').simulate('click'); + const wrapper = mount(); + wrapper.find(UsersAndRolesRowActions).prop('onManageClick')(); expect(initializeRoleMapping).toHaveBeenCalled(); }); it('handles delete click', () => { - const wrapper = shallow(); - wrapper.find('[data-test-subj="DeleteButton"]').simulate('click'); + const wrapper = mount(); + wrapper.find(UsersAndRolesRowActions).prop('onDeleteClick')(); expect(handleDeleteMapping).toHaveBeenCalled(); }); - it('handles input change with special chars', () => { - const wrapper = shallow(); - const input = wrapper.find(EuiFieldSearch); - const value = '*//username'; - input.simulate('change', { target: { value } }); - - expect(wrapper.find(EuiTableRow)).toHaveLength(1); - }); - it('shows default message when "accessAllEngines" is true', () => { - const wrapper = shallow( + const wrapper = mount( ); @@ -100,7 +90,7 @@ describe('RoleMappingsTable', () => { const noItemsRoleMapping = { ...asRoleMapping, engines: [] }; noItemsRoleMapping.accessAllEngines = false; - const wrapper = shallow( + const wrapper = mount( —; const getAuthProviderDisplayValue = (authProvider: string) => @@ -73,114 +59,104 @@ export const RoleMappingsTable: React.FC = ({ initializeRoleMapping, handleDeleteMapping, }) => { - const [filterValue, updateValue] = useState(''); + const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0]; + const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1]; // This is needed because App Search has `engines` and Workplace Search has `groups`. - const standardizeRoleMapping = (roleMappings as SharedRoleMapping[]).map((rm) => { + const standardizedRoleMappings = (roleMappings as SharedRoleMapping[]).map((rm) => { const _rm = { ...rm } as SharedRoleMapping; _rm.accessItems = rm[accessItemKey]; return _rm; - }); - - const filterResults = (result: SharedRoleMapping) => { - // Filter out non-alphanumeric characters, except for underscores, hyphens, and spaces - const sanitizedValue = filterValue.replace(/[^\w\s-]/g, ''); - const values = Object.values(result); - const regexp = new RegExp(sanitizedValue, 'i'); - return values.filter((x) => regexp.test(x)).length > 0; + }) as SharedRoleMapping[]; + + const attributeNameCol: EuiBasicTableColumn = { + field: 'attribute', + name: EXTERNAL_ATTRIBUTE_LABEL, + render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules), }; - const filteredResults = standardizeRoleMapping.filter(filterResults); - const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0]; - const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1]; + const attributeValueCol: EuiBasicTableColumn = { + field: 'attributeValue', + name: ATTRIBUTE_VALUE_LABEL, + render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules), + }; - const rowActions = (id: string) => ( - <> - initializeRoleMapping(id)} - iconType="pencil" - aria-label={MANAGE_BUTTON_LABEL} - data-test-subj="ManageButton" - />{' '} - handleDeleteMapping(id)} - iconType="trash" - aria-label={DELETE_BUTTON_LABEL} - data-test-subj="DeleteButton" + const roleCol: EuiBasicTableColumn = { + field: 'roleType', + name: ROLE_LABEL, + render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules), + }; + + const accessItemsCol: EuiBasicTableColumn = { + field: 'accessItems', + name: accessHeader, + render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => ( + + {accessAllEngines ? ( + ALL_LABEL + ) : ( + <> + {accessItems.length === 0 + ? noItemsPlaceholder + : accessItems.map(({ name }) => ( + + {name} +
+
+ ))} + + )} +
+ ), + }; + + const authProviderCol: EuiBasicTableColumn = { + field: 'authProvider', + name: AUTH_PROVIDER_LABEL, + render: (_, { authProvider }: SharedRoleMapping) => ( + + {authProvider.map(getAuthProviderDisplayValue).join(', ')} + + ), + }; + + const actionsCol: EuiBasicTableColumn = { + field: 'id', + name: '', + align: 'right', + render: (_, { id }: SharedRoleMapping) => ( + initializeRoleMapping(id)} + onDeleteClick={() => handleDeleteMapping(id)} /> - - ); + ), + }; + const columns = shouldShowAuthProvider + ? [attributeNameCol, attributeValueCol, roleCol, accessItemsCol, authProviderCol, actionsCol] + : [attributeNameCol, attributeValueCol, roleCol, accessItemsCol, actionsCol]; + + const pagination = { + hidePerPageOptions: true, + }; + + const search = { + box: { + incremental: true, + fullWidth: false, + placeholder: FILTER_ROLE_MAPPINGS_PLACEHOLDER, + 'data-test-subj': 'RoleMappingsTableSearchInput', + }, + }; return ( - <> - updateValue(e.target.value)} - /> - - {filteredResults.length > 0 ? ( - - - {EXTERNAL_ATTRIBUTE_LABEL} - {ATTRIBUTE_VALUE_LABEL} - {ROLE_LABEL} - {accessHeader} - {shouldShowAuthProvider && ( - {AUTH_PROVIDER_LABEL} - )} - - - - {filteredResults.map( - ({ id, authProvider, rules, roleType, accessAllEngines, accessItems, toolTip }) => ( - - {getFirstAttributeName(rules)} - - {getFirstAttributeValue(rules)} - - {roleType} - - {accessAllEngines ? ( - ALL_LABEL - ) : ( - <> - {accessItems.length === 0 - ? noItemsPlaceholder - : accessItems.map(({ name }) => ( - - {name} -
-
- ))} - - )} -
- {shouldShowAuthProvider && ( - - {authProvider.map(getAuthProviderDisplayValue).join(', ')} - - )} - - {id && rowActions(id)} - {toolTip && } - -
- ) - )} -
-
- ) : ( -

- {i18n.translate('xpack.enterpriseSearch.roleMapping.moResults.message', { - defaultMessage: "No results found for '{filterValue}'", - values: { filterValue }, - })} -

- )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx new file mode 100644 index 00000000000000..dbb47b50d40669 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonIcon } from '@elastic/eui'; + +import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; + +describe('UsersAndRolesRowActions', () => { + const onManageClick = jest.fn(); + const onDeleteClick = jest.fn(); + + const props = { + onManageClick, + onDeleteClick, + }; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonIcon)).toHaveLength(2); + }); + + it('handles manage click', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButtonIcon).first(); + button.simulate('click'); + + expect(onManageClick).toHaveBeenCalled(); + }); + + it('handles delete click', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButtonIcon).last(); + button.simulate('click'); + + expect(onDeleteClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx new file mode 100644 index 00000000000000..3d956c0aabd688 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonIcon } from '@elastic/eui'; + +import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants'; + +interface Props { + onManageClick(): void; + onDeleteClick(): void; +} + +export const UsersAndRolesRowActions: React.FC = ({ onManageClick, onDeleteClick }) => ( + <> + {' '} + + +); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 13ec1efeb1bec1..ad68b7180c8966 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7966,7 +7966,7 @@ "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "ロールをフィルタリング...", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "個別の認証プロバイダーを選択", "xpack.enterpriseSearch.roleMapping.manageRoleMappingTitle": "ロールマッピングを管理", - "xpack.enterpriseSearch.roleMapping.moResults.message": "'{filterValue}'の結果が見つかりません。", + "xpack.enterpriseSearch.roleMapping.noResults.message": "の結果が見つかりません。", "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "ロールマッピングを追加", "xpack.enterpriseSearch.roleMapping.roleLabel": "ロール", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "ユーザーとロール", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index efa055e06bc405..a25a7438bcd167 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8034,7 +8034,7 @@ "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "筛选角色......", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "选择单个身份验证提供程序", "xpack.enterpriseSearch.roleMapping.manageRoleMappingTitle": "管理角色映射", - "xpack.enterpriseSearch.roleMapping.moResults.message": "找不到“{filterValue}”的结果", + "xpack.enterpriseSearch.roleMapping.noResults.message": "找不到的结果", "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "添加角色映射", "xpack.enterpriseSearch.roleMapping.roleLabel": "角色", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "用户和角色", From 9417b699b89678ceaa25350bf58eb7296e9da2e3 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 10 Jun 2021 14:47:55 -0400 Subject: [PATCH 29/99] remove uptime public API, it's not used. (#101799) --- x-pack/plugins/uptime/server/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/uptime/server/index.ts b/x-pack/plugins/uptime/server/index.ts index c5427997b60a83..4894c73c625c10 100644 --- a/x-pack/plugins/uptime/server/index.ts +++ b/x-pack/plugins/uptime/server/index.ts @@ -8,6 +8,5 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { Plugin } from './plugin'; -export { initServerWithKibana, KibanaServer } from './kibana.index'; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); From 5a1f370580d0c91a6e6d483f9f5d618d364cfa81 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Thu, 10 Jun 2021 15:14:07 -0400 Subject: [PATCH 30/99] [Alerting][Docs] Moving alerting setup to its own page (#101323) * Restructuring main alerting page. Adding separate setup page * Fixing links * Moving suppressing duplicate notifications section * Adding redirect * Reverting redirect. Adding placeholder link * Adding placeholder text * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Setup page PR fixes * Alerting page PR fixes * Update docs/user/alerting/alerting-setup.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/apm/apm-alerts.asciidoc | 2 +- .../alerting-getting-started.asciidoc | 102 +++--------------- docs/user/alerting/alerting-setup.asciidoc | 68 ++++++++++++ .../alerting-troubleshooting.asciidoc | 3 + docs/user/alerting/defining-rules.asciidoc | 19 ++++ docs/user/alerting/index.asciidoc | 1 + .../stack-rules/index-threshold.asciidoc | 2 +- .../public/doc_links/doc_links_service.ts | 2 +- .../components/health_check.test.tsx | 2 +- 9 files changed, 111 insertions(+), 90 deletions(-) create mode 100644 docs/user/alerting/alerting-setup.asciidoc diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index b4afc2788895ce..0cee0c04d3fb6d 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -126,4 +126,4 @@ See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more in NOTE: If you are using an **on-premise** Elastic Stack deployment with security, communication between Elasticsearch and Kibana must have TLS configured. -More information is in the alerting {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[prerequisites]. \ No newline at end of file +More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites]. \ No newline at end of file diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index bb11d2a0be4233..8c17f8ec93b965 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -11,7 +11,7 @@ image::images/alerting-overview.png[Rules and Connectors UI] [IMPORTANT] ============================================== -To make sure you can access alerting and actions, see the <> section. +To make sure you can access alerting and actions, see the <> section. ============================================== [float] @@ -22,7 +22,7 @@ Actions typically involve interaction with {kib} services or third party integra This section describes all of these elements and how they operate together. [float] -=== What is a rule? +=== Rules A rule specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: @@ -30,7 +30,10 @@ A rule specifies a background task that runs on the {kib} server to check for sp * *Schedule*: when/how often should detection checks run? * *Actions*: what happens when a condition is detected? -For example, when monitoring a set of servers, a rule might check for average CPU usage > 0.9 on each server for the last two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). +For example, when monitoring a set of servers, a rule might: +* Check for average CPU usage > 0.9 on each server for the last two minutes (condition). +* Check every minute (schedule). +* Send a warning email message via SMTP with subject `CPU on {{server}} is high` (action). image::images/what-is-a-rule.svg[Three components of a rule] @@ -40,7 +43,7 @@ The following sections describe each part of the rule in more detail. [[alerting-concepts-conditions]] ==== Conditions -Under the hood, {kib} rules detect conditions by running a javascript function on the {kib} server, which gives it the flexibility to support a wide range of conditions, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. +Under the hood, {kib} rules detect conditions by running a Javascript function on the {kib} server, which gives it the flexibility to support a wide range of conditions, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. These conditions are packaged and exposed as *rule types*. A rule type hides the underlying details of the condition, and exposes a set of parameters to control the details of the conditions to detect. @@ -68,22 +71,22 @@ Actions are invocations of connectors, which allow interaction with {kib} servic When defining actions in a rule, you specify: -* the *connector type*: the type of service or integration to use -* the connection for that type by referencing a <> -* a mapping of rule values to properties exposed for that type of action +* The *connector type*: the type of service or integration to use +* The connection for that type by referencing a <> +* A mapping of rule values to properties exposed for that type of action The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the rule condition is detected. In the server monitoring example, the `email` connector type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. -When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` connector type. +When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` connector type. image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] See <> for details on the types of connectors provided by {kib}. [float] -[[alerting-concepts-alert-instances]] +[[alerting-concepts-alerts]] === Alerts When checking for a condition, a rule might identify multiple occurrences of the condition. {kib} tracks each of these *alerts* separately and takes an action per alert. @@ -92,22 +95,6 @@ Using the server monitoring example, each server with average CPU > 0.9 is track image::images/alerts.svg[{kib} tracks each detected condition as an alert and takes action on each alert] -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -=== Suppressing duplicate notifications - -Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same rule. Often it's desirable to suppress frequent re-notification. Operations like muting and throttling can be applied at the alert level. If we set the rule re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - [float] [[alerting-concepts-connectors]] === Connectors @@ -120,7 +107,7 @@ Rather than repeatedly entering connection information and credentials for each image::images/rule-concepts-connectors.svg[Connectors provide a central place to store service connection settings] [float] -=== Summary +== Putting it all together A *rule* consists of conditions, *actions*, and a schedule. When conditions are met, *alerts* are created that render *actions* and invoke them. To make action setup and update easier, actions use *connectors* that centralize the information used to connect with {kib} services and third-party integrations. The following example ties these concepts together: @@ -131,7 +118,6 @@ image::images/rule-concepts-summary.svg[Rules, connectors, alerts and actions wo . {kib} invokes the actions, sending them to a third party *integration* like an email service. . If the third party integration has connection parameters or credentials, {kib} will fetch these from the *connector* referenced in the action. - [float] [[alerting-concepts-differences]] == Differences from Watcher @@ -152,63 +138,7 @@ Pre-packaged *rule types* simplify setup and hide the details of complex, domain [float] [[alerting-setup-prerequisites]] -== Setup and prerequisites - -If you are using an *on-premises* Elastic Stack deployment: - -* In the kibana.yml configuration file, add the <> setting. -* For emails to have a footer with a link back to {kib}, set the <> configuration setting. - -If you are using an *on-premises* Elastic Stack deployment with <>: - -* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background rule checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. - -[float] -[[alerting-setup-production]] -== Production considerations and scaling guidance - -When relying on alerting and actions as mission critical services, make sure you follow the <>. - -See <> for more information on the scalability of {kib} alerting. - -[float] -[[alerting-security]] -== Security +== Prerequisites +<> -To access alerting in a space, a user must have access to one of the following features: - -* Alerting -* <> -* <> -* <> -* <> -* <> -* <> - -See <> for more information on configuring roles that provide access to these features. -Also note that a user will need +read+ privileges for the *Actions and Connectors* feature to attach actions to a rule or to edit a rule that has an action attached to it. - -[float] -[[alerting-spaces]] -=== Space isolation - -Rules and connectors are isolated to the {kib} space in which they were created. A rule or connector created in one space will not be visible in another. - -[float] -[[alerting-authorization]] -=== Authorization - -Rules, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the rule. Upon creating or modifying a rule, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the rule including detection checks and executing actions. - -[IMPORTANT] -============================================== -If a rule requires certain privileges to run, such as index privileges, keep in mind that if a user without those privileges updates the rule, the rule will no longer function. -============================================== - -[float] -[[alerting-restricting-actions]] -=== Restricting actions - -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and allowlist the hostnames that {kib} can connect with. - --- +-- \ No newline at end of file diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc new file mode 100644 index 00000000000000..39f1af0030e0aa --- /dev/null +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -0,0 +1,68 @@ +[role="xpack"] +[[alerting-setup]] +== Alerting Setup +++++ +Setup +++++ + +The Alerting feature is automatically enabled in {kib}, but might require some additional configuration. + +[float] +[[alerting-prerequisites]] +=== Prerequisites +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. +* For emails to have a footer with a link back to {kib}, set the <> configuration setting. + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background rule checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. + +[float] +[[alerting-setup-production]] +=== Production considerations and scaling guidance + +When relying on alerting and actions as mission critical services, make sure you follow the <>. + +See <> for more information on the scalability of {kib} alerting. + +[float] +[[alerting-security]] +=== Security + +To access alerting in a space, a user must have access to one of the following features: + +* Alerting +* <> +* <> +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. +Also note that a user will need +read+ privileges for the *Actions and Connectors* feature to attach actions to a rule or to edit a rule that has an action attached to it. + +[float] +[[alerting-restricting-actions]] +==== Restrict actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and allowlist the hostnames that {kib} can connect with. + +[float] +[[alerting-spaces]] +=== Space isolation + +Rules and connectors are isolated to the {kib} space in which they were created. A rule or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Rules, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the rule. Upon creating or modifying a rule, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the rule including detection checks and executing actions. + +[IMPORTANT] +============================================== +If a rule requires certain privileges to run, such as index privileges, keep in mind that if a user without those privileges updates the rule, the rule will no longer function. +============================================== diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index b7fd98d1c674ec..b7b0c749dfe149 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[alerting-troubleshooting]] == Alerting Troubleshooting +++++ +Troubleshooting +++++ This page describes how to resolve common problems you might encounter with Alerting. If your problem isn’t described here, please review open issues in the following GitHub repositories: diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc index 05885f1af13ba2..c48108ca9acc0a 100644 --- a/docs/user/alerting/defining-rules.asciidoc +++ b/docs/user/alerting/defining-rules.asciidoc @@ -32,6 +32,25 @@ Notify:: This value limits how often actions are repeated when an alert rem - **Every time alert is active**: Actions are repeated when an alert remains active across checks. - **On a custom action interval**: Actions are suppressed for the throttle interval, but repeat when an alert remains active across checks for a duration longer than the throttle interval. +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +[NOTE] +============================================== +Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same rule. Often, it's desirable to suppress these re-notifications. If you set the rule **Notify** setting to **On a custom action interval** with an interval of 5 minutes, you reduce noise by only getting emails every 5 minutes for servers that continue to exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +To get notified **only once** when a server exceeds the threshold, you can set the rule's **Notify** setting to **Only on status change**. +============================================== + [float] [[defining-alerts-type-conditions]] diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index f8a5aacce8f0e1..a331f1d5606f7e 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,4 +1,5 @@ include::alerting-getting-started.asciidoc[] +include::alerting-setup.asciidoc[] include::defining-rules.asciidoc[] include::rule-management.asciidoc[] include::rule-details.asciidoc[] diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/stack-rules/index-threshold.asciidoc index 43b750b85fb3bd..e152ee7cb1deb2 100644 --- a/docs/user/alerting/stack-rules/index-threshold.asciidoc +++ b/docs/user/alerting/stack-rules/index-threshold.asciidoc @@ -19,7 +19,7 @@ image::user/alerting/images/rule-types-index-threshold-conditions.png[Five claus Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. When:: This clause specifies how the value to be compared to the threshold is calculated. The value is calculated by aggregating a numeric field a the *time window*. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used, and an aggregation field is not necessary. -Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. +Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. Threshold:: This clause defines a threshold value and a comparison operator (one of `is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The result of the aggregation is compared to this threshold. Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be to a value higher than the *check every* value in the <>, to avoid gaps in detection. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index d4ab8f624f7111..4912ae490b565b 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -265,7 +265,7 @@ export class DocLinksService { preconfiguredConnectors: `${KIBANA_DOCS}pre-configured-connectors.html`, preconfiguredAlertHistoryConnector: `${KIBANA_DOCS}index-action-type.html#preconfigured-connector-alert-history`, serviceNowAction: `${KIBANA_DOCS}servicenow-action-type.html#configuring-servicenow`, - setupPrerequisites: `${KIBANA_DOCS}alerting-getting-started.html#alerting-setup-prerequisites`, + setupPrerequisites: `${KIBANA_DOCS}alerting-setup.html#alerting-prerequisites`, slackAction: `${KIBANA_DOCS}slack-action-type.html#configuring-slack`, teamsAction: `${KIBANA_DOCS}teams-action-type.html#configuring-teams`, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 44c950a5000405..b998067424edd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -184,7 +184,7 @@ describe('health check', () => { const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot(`"Learn how.(opens in a new tab or window)"`); expect(action!.getAttribute('href')).toMatchInlineSnapshot( - `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-getting-started.html#alerting-setup-prerequisites"` + `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-setup.html#alerting-prerequisites"` ); }); }); From e9a4028005cb18bdf9789b8c3dcf177e3119c84f Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 10 Jun 2021 15:15:53 -0400 Subject: [PATCH 31/99] Add comments to some alerting plugin public API items (#101551) * Add comments. Remove ruleType as the second param, not needed. * Add comments. Remove ruleType as the second param, not needed. * Fix bad type check and update docs * update docs * Remove unused import * change exports to type to avoid increasing bundle size * Update x-pack/plugins/alerting/public/plugin.ts Co-authored-by: ymao1 * Update x-pack/plugins/alerting/public/plugin.ts Co-authored-by: ymao1 * Update x-pack/plugins/alerting/public/plugin.ts Co-authored-by: ymao1 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: ymao1 --- api_docs/alerting.json | 143 ++++++++++++------ api_docs/alerting.mdx | 3 + x-pack/plugins/alerting/README.md | 4 +- .../alert_navigation_registry.test.ts | 8 +- .../public/alert_navigation_registry/types.ts | 16 +- x-pack/plugins/alerting/public/index.ts | 3 +- x-pack/plugins/alerting/public/plugin.ts | 50 ++++-- .../fixtures/plugins/alerts/public/plugin.ts | 4 +- 8 files changed, 158 insertions(+), 73 deletions(-) diff --git a/api_docs/alerting.json b/api_docs/alerting.json index 13a150d0af00da..979f444659c208 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -5,7 +5,42 @@ "functions": [], "interfaces": [], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "alerting", + "id": "def-public.AlertNavigationHandler", + "type": "Type", + "tags": [], + "label": "AlertNavigationHandler", + "description": [ + "\nReturns information that can be used to navigate to a specific page to view the given rule.\n" + ], + "signature": [ + "(alert: Pick<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.Alert", + "text": "Alert" + }, + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">) => string | ", + { + "pluginId": "kibanaUtils", + "scope": "common", + "docId": "kibKibanaUtilsPluginApi", + "section": "def-common.JsonObject", + "text": "JsonObject" + } + ], + "source": { + "path": "x-pack/plugins/alerting/public/alert_navigation_registry/types.ts", + "lineNumber": 20 + }, + "deprecated": false, + "initialIsOpen": false + } + ], "objects": [], "setup": { "parentPluginId": "alerting", @@ -24,44 +59,58 @@ "parentPluginId": "alerting", "id": "def-public.PluginSetupContract.registerNavigation", "type": "Function", - "tags": [], + "tags": [ + "throws" + ], "label": "registerNavigation", - "description": [], + "description": [ + "\nRegister a customized view of the particular rule type. Stack Management provides a generic overview, but a developer can register a\ncustom navigation to provide the user an extra link to a more curated view. The alerting plugin doesn't actually do\nanything with this information, but it can be used by other plugins via the `getNavigation` functionality. Currently\nthe trigger_actions_ui plugin uses it to expose the link from the generic rule view in Stack Management.\n" + ], "signature": [ - "(consumer: string, alertType: string, handler: ", - "AlertNavigationHandler", + "(applicationId: string, ruleType: string, handler: ", + { + "pluginId": "alerting", + "scope": "public", + "docId": "kibAlertingPluginApi", + "section": "def-public.AlertNavigationHandler", + "text": "AlertNavigationHandler" + }, ") => void" ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 15 + "lineNumber": 30 }, "deprecated": false, "returnComment": [], "children": [ { "parentPluginId": "alerting", - "id": "def-public.consumer", + "id": "def-public.applicationId", "type": "string", "tags": [], - "label": "consumer", - "description": [], + "label": "applicationId", + "description": [ + "The application id that the user should be navigated to, to view a particular alert in a custom way." + ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 16 + "lineNumber": 31 }, "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-public.alertType", + "id": "def-public.ruleType", "type": "string", "tags": [], - "label": "alertType", - "description": [], + "label": "ruleType", + "description": [ + "The rule type that has been registered with Alerting.Server.PluginSetupContract.registerType. If\nno such rule with that id exists, a warning is output to the console log. It used to throw an error, but that was temporarily moved\nbecause it was causing flaky test failures with https://github.com/elastic/kibana/issues/59229 and needs to be\ninvestigated more." + ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 17 + "lineNumber": 32 }, "deprecated": false }, @@ -71,7 +120,9 @@ "type": "Function", "tags": [], "label": "handler", - "description": [], + "description": [ + "The navigation handler should return either a relative URL, or a state object. This information can be used,\nin conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details." + ], "signature": [ "(alert: Pick<", { @@ -81,15 +132,7 @@ "section": "def-common.Alert", "text": "Alert" }, - ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">, alertType: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.AlertType", - "text": "AlertType" - }, - "<\"default\", \"recovered\">) => string | ", + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">) => string | ", { "pluginId": "kibanaUtils", "scope": "common", @@ -100,7 +143,7 @@ ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 18 + "lineNumber": 33 }, "deprecated": false } @@ -112,29 +155,39 @@ "type": "Function", "tags": [], "label": "registerDefaultNavigation", - "description": [], + "description": [ + "\nRegister a customized view for all rule types. Stack Management provides a generic overview, but a developer can register a\ncustom navigation to provide the user an extra link to a more curated view. The alerting plugin doesn't actually do\nanything with this information, but it can be used by other plugins via the `getNavigation` functionality. Currently\nthe trigger_actions_ui plugin uses it to expose the link from the generic rule view in Stack Management.\n" + ], "signature": [ - "(consumer: string, handler: ", - "AlertNavigationHandler", + "(applicationId: string, handler: ", + { + "pluginId": "alerting", + "scope": "public", + "docId": "kibAlertingPluginApi", + "section": "def-public.AlertNavigationHandler", + "text": "AlertNavigationHandler" + }, ") => void" ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 20 + "lineNumber": 46 }, "deprecated": false, "returnComment": [], "children": [ { "parentPluginId": "alerting", - "id": "def-public.consumer", + "id": "def-public.applicationId", "type": "string", "tags": [], - "label": "consumer", - "description": [], + "label": "applicationId", + "description": [ + "The application id that the user should be navigated to, to view a particular alert in a custom way." + ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 20 + "lineNumber": 46 }, "deprecated": false }, @@ -144,7 +197,9 @@ "type": "Function", "tags": [], "label": "handler", - "description": [], + "description": [ + "The navigation handler should return either a relative URL, or a state object. This information can be used,\nin conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details." + ], "signature": [ "(alert: Pick<", { @@ -154,15 +209,7 @@ "section": "def-common.Alert", "text": "Alert" }, - ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">, alertType: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.AlertType", - "text": "AlertType" - }, - "<\"default\", \"recovered\">) => string | ", + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">) => string | ", { "pluginId": "kibanaUtils", "scope": "common", @@ -173,7 +220,7 @@ ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 20 + "lineNumber": 46 }, "deprecated": false } @@ -192,7 +239,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 22 + "lineNumber": 48 }, "deprecated": false, "children": [ @@ -224,7 +271,7 @@ ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 23 + "lineNumber": 49 }, "deprecated": false, "returnComment": [], @@ -238,7 +285,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 23 + "lineNumber": 49 }, "deprecated": false } @@ -3857,7 +3904,7 @@ "label": "ReservedActionGroups", "description": [], "signature": [ - "\"recovered\" | RecoveryActionGroupId" + "RecoveryActionGroupId | \"recovered\"" ], "source": { "path": "x-pack/plugins/alerting/common/builtin_action_groups.ts", diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 5dce4a9a2c7b17..c3c844148106f0 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -29,6 +29,9 @@ import alertingObj from './alerting.json'; ### Start +### Consts, variables and types + + ## Server ### Functions diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index cb43e534080905..9d314cc048b705 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -619,7 +619,7 @@ The _registerNavigation_ api allows you to register a handler for a specific ale alerting.registerNavigation( 'my-application-id', 'my-application-id.my-rule-type', - (alert: SanitizedAlert, alertType: AlertType) => `/my-unique-rule/${rule.id}` + (alert: SanitizedAlert) => `/my-unique-rule/${rule.id}` ); ``` @@ -635,7 +635,7 @@ The _registerDefaultNavigation_ API allows you to register a handler for any rul ``` alerting.registerDefaultNavigation( 'my-application-id', - (alert: SanitizedAlert, alertType: AlertType) => `/my-other-rules/${rule.id}` + (alert: SanitizedAlert) => `/my-other-rules/${rule.id}` ); ``` diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts index 3364593705301d..7eb59963113865 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts @@ -23,7 +23,7 @@ const mockAlertType = (id: string): AlertType => ({ }); describe('AlertNavigationRegistry', () => { - function handler(alert: SanitizedAlert, alertType: AlertType) { + function handler(alert: SanitizedAlert) { return {}; } @@ -143,7 +143,7 @@ describe('AlertNavigationRegistry', () => { test('returns registered handlers by consumer & Alert Type', () => { const registry = new AlertNavigationRegistry(); - function indexThresholdHandler(alert: SanitizedAlert, alertType: AlertType) { + function indexThresholdHandler(alert: SanitizedAlert) { return {}; } @@ -155,7 +155,7 @@ describe('AlertNavigationRegistry', () => { test('returns default handlers by consumer when there is no handler for requested alert type', () => { const registry = new AlertNavigationRegistry(); - function defaultHandler(alert: SanitizedAlert, alertType: AlertType) { + function defaultHandler(alert: SanitizedAlert) { return {}; } @@ -168,7 +168,7 @@ describe('AlertNavigationRegistry', () => { registry.register('siem', mockAlertType('indexThreshold'), () => ({})); - function defaultHandler(alert: SanitizedAlert, alertType: AlertType) { + function defaultHandler(alert: SanitizedAlert) { return {}; } diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts index bf00d2c1b6eaf9..53540facd9652e 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts @@ -6,9 +6,15 @@ */ import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; -import { AlertType, SanitizedAlert } from '../../common'; +import { SanitizedAlert } from '../../common'; -export type AlertNavigationHandler = ( - alert: SanitizedAlert, - alertType: AlertType -) => JsonObject | string; +/** + * Returns information that can be used to navigate to a specific page to view the given rule. + * + * @param rule The rule to view + * @returns A URL that is meant to be relative to your application id, or a state object that your application uses to render + * the rule. This information is intended to be used with cores NavigateToApp function, along with the application id that was + * originally registered to {@link PluginSetupContract.registerNavigation}. + * + */ +export type AlertNavigationHandler = (alert: SanitizedAlert) => JsonObject | string; diff --git a/x-pack/plugins/alerting/public/index.ts b/x-pack/plugins/alerting/public/index.ts index 5edca2f8c4c419..7c61ea1b6b1fd3 100644 --- a/x-pack/plugins/alerting/public/index.ts +++ b/x-pack/plugins/alerting/public/index.ts @@ -6,7 +6,8 @@ */ import { AlertingPublicPlugin } from './plugin'; -export { PluginSetupContract, PluginStartContract } from './plugin'; +export type { PluginSetupContract, PluginStartContract } from './plugin'; +export type { AlertNavigationHandler } from './alert_navigation_registry'; export function plugin() { return new AlertingPublicPlugin(); diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerting/public/plugin.ts index 025467d92a6ace..be7080f5df6dac 100644 --- a/x-pack/plugins/alerting/public/plugin.ts +++ b/x-pack/plugins/alerting/public/plugin.ts @@ -12,12 +12,38 @@ import { loadAlert, loadAlertType } from './alert_api'; import { Alert, AlertNavigation } from '../common'; export interface PluginSetupContract { + /** + * Register a customized view of the particular rule type. Stack Management provides a generic overview, but a developer can register a + * custom navigation to provide the user an extra link to a more curated view. The alerting plugin doesn't actually do + * anything with this information, but it can be used by other plugins via the `getNavigation` functionality. Currently + * the trigger_actions_ui plugin uses it to expose the link from the generic rule details view in Stack Management. + * + * @param applicationId The application id that the user should be navigated to, to view a particular alert in a custom way. + * @param ruleType The rule type that has been registered with Alerting.Server.PluginSetupContract.registerType. If + * no such rule with that id exists, a warning is output to the console log. It used to throw an error, but that was temporarily moved + * because it was causing flaky test failures with https://github.com/elastic/kibana/issues/59229 and needs to be + * investigated more. + * @param handler The navigation handler should return either a relative URL, or a state object. This information can be used, + * in conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details. + * @throws an error if the given applicationId and ruleType combination has already been registered. + */ registerNavigation: ( - consumer: string, - alertType: string, + applicationId: string, + ruleType: string, handler: AlertNavigationHandler ) => void; - registerDefaultNavigation: (consumer: string, handler: AlertNavigationHandler) => void; + + /** + * Register a customized view for all rule types with this application id. Stack Management provides a generic overview, but a developer can register a + * custom navigation to provide the user an extra link to a more curated view. The alerting plugin doesn't actually do + * anything with this information, but it can be used by other plugins via the `getNavigation` functionality. Currently + * the trigger_actions_ui plugin uses it to expose the link from the generic rule details view in Stack Management. + * + * @param applicationId The application id that the user should be navigated to, to view a particular alert in a custom way. + * @param handler The navigation handler should return either a relative URL, or a state object. This information can be used, + * in conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details. + */ + registerDefaultNavigation: (applicationId: string, handler: AlertNavigationHandler) => void; } export interface PluginStartContract { getNavigation: (alertId: Alert['id']) => Promise; @@ -29,23 +55,25 @@ export class AlertingPublicPlugin implements Plugin { - const alertType = await loadAlertType({ http: core.http, id: alertTypeId }); + const alertType = await loadAlertType({ http: core.http, id: ruleTypeId }); if (!alertType) { // eslint-disable-next-line no-console console.log( - `Unable to register navigation for alert type "${alertTypeId}" because it is not registered on the server side.` + `Unable to register navigation for rule type "${ruleTypeId}" because it is not registered on the server side.` ); return; } - this.alertNavigationRegistry!.register(consumer, alertType, handler); + this.alertNavigationRegistry!.register(applicationId, alertType, handler); }; - const registerDefaultNavigation = async (consumer: string, handler: AlertNavigationHandler) => - this.alertNavigationRegistry!.registerDefault(consumer, handler); + const registerDefaultNavigation = async ( + applicationId: string, + handler: AlertNavigationHandler + ) => this.alertNavigationRegistry!.registerDefault(applicationId, handler); return { registerNavigation, @@ -69,7 +97,7 @@ export class AlertingPublicPlugin implements Plugin `/rule/${alert.id}` + (alert: SanitizedAlert) => `/rule/${alert.id}` ); triggersActionsUi.alertTypeRegistry.register({ From befd30ff6c86be675533aafd5d390a96a994f07e Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 10 Jun 2021 15:27:11 -0400 Subject: [PATCH 32/99] [APM] Fleet support for merging input.config values with other nested properties in the policy input (#101690) * [APM] Improvments in the APM fleet integration (#95501) * added unit test and line comment * fixes eslint issues --- .../fleet/register_fleet_policy_callbacks.ts | 1 + .../package_policies_to_agent_inputs.test.ts | 77 +++++++++++++++++++ .../package_policies_to_agent_inputs.ts | 15 +++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts index e4306b4c2ec988..35c7f0dfdfd73d 100644 --- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts @@ -115,6 +115,7 @@ export function getPackagePolicyWithAgentConfigurations( { ...firstInput, config: { + ...firstInput.config, [APM_SERVER]: { value: { ...apmServerValue, diff --git a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts index 31c47bf9dc69de..a5acd823c20fd3 100644 --- a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts +++ b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts @@ -218,4 +218,81 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { }, ]); }); + + it('returns agent inputs with deeply merged config values', () => { + expect( + storedPackagePoliciesToAgentInputs([ + { + ...mockPackagePolicy, + inputs: [ + { + ...mockInput, + compiled_input: { + agent_input_template_group1_vars: { + inputVar: 'input-value', + }, + agent_input_template_group2_vars: { + inputVar3: { + testFieldGroup: { + subField1: 'subfield1', + }, + testField: 'test', + }, + }, + }, + config: { + agent_input_template_group1_vars: { + value: { + inputVar2: {}, + }, + }, + agent_input_template_group2_vars: { + value: { + inputVar3: { + testFieldGroup: { + subField2: 'subfield2', + }, + }, + inputVar4: '', + }, + }, + }, + }, + ], + }, + ]) + ).toEqual([ + { + id: 'some-uuid', + revision: 1, + name: 'mock-package-policy', + type: 'test-logs', + data_stream: { namespace: 'default' }, + use_output: 'default', + agent_input_template_group1_vars: { + inputVar: 'input-value', + inputVar2: {}, + }, + agent_input_template_group2_vars: { + inputVar3: { + testField: 'test', + testFieldGroup: { + subField1: 'subfield1', + subField2: 'subfield2', + }, + }, + inputVar4: '', + }, + streams: [ + { + id: 'test-logs-foo', + data_stream: { dataset: 'foo', type: 'logs' }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, + { id: 'test-logs-bar', data_stream: { dataset: 'bar', type: 'logs' } }, + ], + }, + ]); + }); }); diff --git a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts index 61d7764a832b16..f262521461b983 100644 --- a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts +++ b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { merge } from 'lodash'; + import type { PackagePolicy, FullAgentPolicyInput, FullAgentPolicyInputStream } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; @@ -31,10 +33,6 @@ export const storedPackagePoliciesToAgentInputs = ( namespace: packagePolicy.namespace || 'default', }, use_output: DEFAULT_OUTPUT.name, - ...Object.entries(input.config || {}).reduce((acc, [key, { value }]) => { - acc[key] = value; - return acc; - }, {} as { [k: string]: any }), ...(input.compiled_input || {}), ...(input.streams.length ? { @@ -56,6 +54,15 @@ export const storedPackagePoliciesToAgentInputs = ( : {}), }; + // deeply merge the input.config values with the full policy input + merge( + fullInput, + Object.entries(input.config || {}).reduce( + (acc, [key, { value }]) => ({ ...acc, [key]: value }), + {} + ) + ); + if (packagePolicy.package) { fullInput.meta = { package: { From e55a93ce584fcfa70654dadbc7155038b0394faa Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 10 Jun 2021 12:33:32 -0700 Subject: [PATCH 33/99] [Event Log] Populated rule.* ECS fields for alert events. (#101132) * [Event Log] Populated rule.* ECS fields for alert events. * added mappings * changed the params passing * fixed tests * fixed type checks * used kibanaVersion for version event rule * fixed typos * fixed tests * fixed tests * fixed tests * fixed tests * fixed jest tests * removed references * removed not populated fields * fixed tests * fixed tests * fixed tests --- .../create_execution_handler.test.ts | 34 +- .../task_runner/create_execution_handler.ts | 14 +- .../server/task_runner/task_runner.test.ts | 526 +++++++++++++++++- .../server/task_runner/task_runner.ts | 49 +- x-pack/plugins/event_log/README.md | 13 +- .../plugins/event_log/generated/mappings.json | 4 + x-pack/plugins/event_log/generated/schemas.ts | 1 + .../tests/alerting/alerts.ts | 15 +- .../tests/alerting/event_log.ts | 7 + .../spaces_only/tests/alerting/event_log.ts | 70 ++- 10 files changed, 696 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 5ab25fbfa39e7a..25f0656163f5d3 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -67,7 +67,7 @@ const createExecutionHandlerParams: jest.Mocked< > > = { actionsPlugin: mockActionsPlugin, - spaceId: 'default', + spaceId: 'test1', alertId: '1', alertName: 'name-of-alert', tags: ['tag-A', 'tag-B'], @@ -130,7 +130,7 @@ test('enqueues execution per selected action', async () => { "apiKey": "MTIzOmFiYw==", "id": "1", "params": Object { - "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", + "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here", "contextVal": "My goes here", "foo": true, "stateVal": "My goes here", @@ -142,7 +142,7 @@ test('enqueues execution per selected action', async () => { }, "type": "SAVED_OBJECT", }, - "spaceId": "default", + "spaceId": "test1", }, ] `); @@ -154,6 +154,10 @@ test('enqueues execution per selected action', async () => { Object { "event": Object { "action": "execute-action", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -164,18 +168,28 @@ test('enqueues execution per selected action', async () => { "saved_objects": Array [ Object { "id": "1", + "namespace": "test1", "rel": "primary", "type": "alert", "type_id": "test", }, Object { "id": "1", + "namespace": "test1", "type": "action", "type_id": "test", }, ], }, "message": "alert: test:1: 'name-of-alert' instanceId: '2' scheduled actionGroup: 'default' action: test:1", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "name-of-alert", + "namespace": "test1", + "ruleset": "alerts", + }, }, ], ] @@ -183,10 +197,10 @@ test('enqueues execution per selected action', async () => { expect(jest.requireMock('./inject_action_params').injectActionParams).toHaveBeenCalledWith({ ruleId: '1', - spaceId: 'default', + spaceId: 'test1', actionTypeId: 'test', actionParams: { - alertVal: 'My 1 name-of-alert default tag-A,tag-B 2 goes here', + alertVal: 'My 1 name-of-alert test1 tag-A,tag-B 2 goes here', contextVal: 'My goes here', foo: true, stateVal: 'My goes here', @@ -233,7 +247,7 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => id: '1', type: 'alert', }), - spaceId: 'default', + spaceId: 'test1', apiKey: createExecutionHandlerParams.apiKey, }); }); @@ -308,7 +322,7 @@ test('context attribute gets parameterized', async () => { "apiKey": "MTIzOmFiYw==", "id": "1", "params": Object { - "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", + "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here", "contextVal": "My context-val goes here", "foo": true, "stateVal": "My goes here", @@ -320,7 +334,7 @@ test('context attribute gets parameterized', async () => { }, "type": "SAVED_OBJECT", }, - "spaceId": "default", + "spaceId": "test1", }, ] `); @@ -341,7 +355,7 @@ test('state attribute gets parameterized', async () => { "apiKey": "MTIzOmFiYw==", "id": "1", "params": Object { - "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", + "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here", "contextVal": "My goes here", "foo": true, "stateVal": "My state-val goes here", @@ -353,7 +367,7 @@ test('state attribute gets parameterized', async () => { }, "type": "SAVED_OBJECT", }, - "spaceId": "default", + "spaceId": "test1", }, ] `); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index ef93179bdaba16..c3a36297c217ac 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -174,7 +174,11 @@ export function createExecutionHandler< const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; const event: IEvent = { - event: { action: EVENT_LOG_ACTIONS.executeAction }, + event: { + action: EVENT_LOG_ACTIONS.executeAction, + kind: 'alert', + category: [alertType.producer], + }, kibana: { alerting: { instance_id: alertInstanceId, @@ -192,6 +196,14 @@ export function createExecutionHandler< { type: 'action', id: action.id, type_id: action.actionTypeId, ...namespace }, ], }, + rule: { + id: alertId, + license: alertType.minimumLicenseRequired, + category: alertType.id, + ruleset: alertType.producer, + ...namespace, + name: alertName, + }, }; event.message = `alert: ${alertLabel} instanceId: '${alertInstanceId}' scheduled ${ diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 6847b17bcef4b8..4893e509f6b6a6 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -266,6 +266,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -283,6 +287,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, } `); @@ -373,6 +385,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + category: ['alerts'], + kind: 'alert', duration: 0, start: '1970-01-01T00:00:00.000Z', }, @@ -393,35 +407,45 @@ describe('Task Runner', () => { ], }, message: "test:1: 'alert-name' created new instance: '1'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + category: ['alerts'], duration: 0, + kind: 'alert', start: '1970-01-01T00:00:00.000Z', }, kibana: { - alerting: { - instance_id: '1', - action_group_id: 'default', - action_subgroup: 'subDefault', - }, + alerting: { action_group_id: 'default', action_subgroup: 'subDefault', instance_id: '1' }, saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - type_id: 'test', - }, + { id: '1', namespace: undefined, rel: 'primary', type: 'alert', type_id: 'test' }, ], }, message: "test:1: 'alert-name' active instance: '1' in actionGroup(subgroup): 'default(subDefault)'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { event: { action: 'execute-action', + category: ['alerts'], + kind: 'alert', }, kibana: { alerting: { @@ -447,13 +471,18 @@ describe('Task Runner', () => { }, message: "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup(subgroup): 'default(subDefault)' action: action:1", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { '@timestamp': '1970-01-01T00:00:00.000Z', - event: { - action: 'execute', - outcome: 'success', - }, + event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { alerting: { status: 'active', @@ -469,6 +498,14 @@ describe('Task Runner', () => { ], }, message: "alert executed: test:1: 'alert-name'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); }); @@ -529,6 +566,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + category: ['alerts'], + kind: 'alert', duration: 0, start: '1970-01-01T00:00:00.000Z', }, @@ -548,10 +587,20 @@ describe('Task Runner', () => { ], }, message: "test:1: 'alert-name' created new instance: '1'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + category: ['alerts'], + kind: 'alert', duration: 0, start: '1970-01-01T00:00:00.000Z', }, @@ -571,11 +620,21 @@ describe('Task Runner', () => { ], }, message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', + category: ['alerts'], + kind: 'alert', outcome: 'success', }, kibana: { @@ -593,6 +652,14 @@ describe('Task Runner', () => { ], }, message: "alert executed: test:1: 'alert-name'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'alert-name', + namespace: undefined, + ruleset: 'alerts', + }, }); }); @@ -711,7 +778,11 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 86400000000000, + "kind": "alert", "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { @@ -730,6 +801,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -737,6 +816,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -754,6 +837,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -944,7 +1035,11 @@ describe('Task Runner', () => { Object { "event": Object { "action": "new-instance", + "category": Array [ + "alerts", + ], "duration": 0, + "kind": "alert", "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { @@ -963,13 +1058,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' created new instance: '1'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 0, + "kind": "alert", "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { @@ -988,12 +1095,24 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "execute-action", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -1018,6 +1137,14 @@ describe('Task Runner', () => { ], }, "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -1025,6 +1152,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -1042,6 +1173,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -1146,8 +1285,12 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "category": Array [ + "alerts", + ], "duration": 64800000000000, "end": "1970-01-01T00:00:00.000Z", + "kind": "alert", "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { @@ -1165,13 +1308,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' instance '2' has recovered", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 86400000000000, + "kind": "alert", "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { @@ -1190,12 +1345,24 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "execute-action", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -1220,12 +1387,24 @@ describe('Task Runner', () => { ], }, "message": "alert: test:1: 'alert-name' instanceId: '2' scheduled actionGroup: 'recovered' action: action:2", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "execute-action", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -1250,6 +1429,14 @@ describe('Task Runner', () => { ], }, "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -1257,6 +1444,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -1274,6 +1465,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -1573,8 +1772,12 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "category": Array [ + "alerts", + ], "duration": 64800000000000, "end": "1970-01-01T00:00:00.000Z", + "kind": "alert", "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { @@ -1593,13 +1796,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' instance '2' has recovered", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 86400000000000, + "kind": "alert", "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { @@ -1618,6 +1833,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -1625,6 +1848,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -1642,6 +1869,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -1835,6 +2070,10 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "failure", "reason": "execute", }, @@ -1853,6 +2092,13 @@ describe('Task Runner', () => { ], }, "message": "alert execution failure: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -1895,6 +2141,10 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "failure", "reason": "decrypt", }, @@ -1913,6 +2163,13 @@ describe('Task Runner', () => { ], }, "message": "test:1: execution failed", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -1963,6 +2220,10 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "failure", "reason": "license", }, @@ -1981,6 +2242,13 @@ describe('Task Runner', () => { ], }, "message": "test:1: execution failed", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2031,6 +2299,10 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "failure", "reason": "unknown", }, @@ -2049,6 +2321,13 @@ describe('Task Runner', () => { ], }, "message": "test:1: execution failed", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2098,6 +2377,10 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "failure", "reason": "read", }, @@ -2116,6 +2399,13 @@ describe('Task Runner', () => { ], }, "message": "test:1: execution failed", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2329,7 +2619,11 @@ describe('Task Runner', () => { Object { "event": Object { "action": "new-instance", + "category": Array [ + "alerts", + ], "duration": 0, + "kind": "alert", "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { @@ -2348,13 +2642,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' created new instance: '1'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "new-instance", + "category": Array [ + "alerts", + ], "duration": 0, + "kind": "alert", "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { @@ -2373,13 +2679,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' created new instance: '2'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 0, + "kind": "alert", "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { @@ -2398,13 +2716,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 0, + "kind": "alert", "start": "1970-01-01T00:00:00.000Z", }, "kibana": Object { @@ -2423,6 +2753,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -2430,6 +2768,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -2447,6 +2789,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2521,7 +2871,11 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 86400000000000, + "kind": "alert", "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { @@ -2540,13 +2894,25 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], "duration": 64800000000000, + "kind": "alert", "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { @@ -2565,6 +2931,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -2572,6 +2946,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -2589,6 +2967,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2655,6 +3041,10 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -2672,12 +3062,24 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "active-instance", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -2695,6 +3097,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -2702,6 +3112,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -2719,6 +3133,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2780,8 +3202,12 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "category": Array [ + "alerts", + ], "duration": 86400000000000, "end": "1970-01-01T00:00:00.000Z", + "kind": "alert", "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { @@ -2799,14 +3225,26 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' instance '1' has recovered", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "recovered-instance", + "category": Array [ + "alerts", + ], "duration": 64800000000000, "end": "1970-01-01T00:00:00.000Z", + "kind": "alert", "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { @@ -2824,6 +3262,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' instance '2' has recovered", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -2831,6 +3277,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -2848,6 +3298,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] @@ -2911,6 +3369,10 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -2927,12 +3389,24 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' instance '1' has recovered", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ Object { "event": Object { "action": "recovered-instance", + "category": Array [ + "alerts", + ], + "kind": "alert", }, "kibana": Object { "alerting": Object { @@ -2949,6 +3423,14 @@ describe('Task Runner', () => { ], }, "message": "test:1: 'alert-name' instance '2' has recovered", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], Array [ @@ -2956,6 +3438,10 @@ describe('Task Runner', () => { "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", + "category": Array [ + "alerts", + ], + "kind": "alert", "outcome": "success", }, "kibana": Object { @@ -2973,6 +3459,14 @@ describe('Task Runner', () => { ], }, "message": "alert executed: test:1: 'alert-name'", + "rule": Object { + "category": "test", + "id": "1", + "license": "basic", + "name": "alert-name", + "namespace": undefined, + "ruleset": "alerts", + }, }, ], ] diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 4a214efceaa7d7..c9ca5d85e5116e 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -303,6 +303,10 @@ export class TaskRunner< event.message = `alert executed: ${alertLabel}`; event.event = event.event || {}; event.event.outcome = 'success'; + event.rule = { + ...event.rule, + name: alert.name, + }; // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object const instancesWithScheduledActions = pickBy( @@ -337,7 +341,8 @@ export class TaskRunner< alertId, alertLabel, namespace, - ruleTypeId: alert.alertTypeId, + ruleType: alertType, + rule: alert, }); if (!muteAll) { @@ -493,7 +498,11 @@ export class TaskRunner< // explicitly set execute timestamp so it will be before other events // generated here (new-instance, schedule-action, etc) '@timestamp': runDate, - event: { action: EVENT_LOG_ACTIONS.execute }, + event: { + action: EVENT_LOG_ACTIONS.execute, + kind: 'alert', + category: [this.alertType.producer], + }, kibana: { saved_objects: [ { @@ -505,6 +514,13 @@ export class TaskRunner< }, ], }, + rule: { + id: alertId, + license: this.alertType.minimumLicenseRequired, + category: this.alertType.id, + ruleset: this.alertType.producer, + namespace, + }, }; eventLogger.startTiming(event); @@ -665,7 +681,19 @@ interface GenerateNewAndRecoveredInstanceEventsParams< alertId: string; alertLabel: string; namespace: string | undefined; - ruleTypeId: string; + ruleType: NormalizedAlertType< + AlertTypeParams, + AlertTypeState, + { + [x: string]: unknown; + }, + { + [x: string]: unknown; + }, + string, + string + >; + rule: SanitizedAlert; } function generateNewAndRecoveredInstanceEvents< @@ -679,7 +707,8 @@ function generateNewAndRecoveredInstanceEvents< currentAlertInstances, originalAlertInstances, recoveredAlertInstances, - ruleTypeId, + rule, + ruleType, } = params; const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); @@ -746,6 +775,8 @@ function generateNewAndRecoveredInstanceEvents< const event: IEvent = { event: { action, + kind: 'alert', + category: [ruleType.producer], ...(state?.start ? { start: state.start as string } : {}), ...(state?.end ? { end: state.end as string } : {}), ...(state?.duration !== undefined ? { duration: state.duration as number } : {}), @@ -761,12 +792,20 @@ function generateNewAndRecoveredInstanceEvents< rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, - type_id: ruleTypeId, + type_id: ruleType.id, namespace, }, ], }, message, + rule: { + id: rule.id, + license: ruleType.minimumLicenseRequired, + category: ruleType.id, + ruleset: ruleType.producer, + namespace, + name: rule.name, + }, }; eventLogger.logEvent(event); } diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 2272341c65f5e9..032f77543acb97 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -101,11 +101,20 @@ Below is a document in the expected structure, with descriptions of the fields: logger: "name of the logger", }, - // Rule fields. All of them are supported. + // Rule fields. // https://www.elastic.co/guide/en/ecs/current/ecs-rule.html rule: { + // Fields currently are populated: + id: "a823fd56-5467-4727-acb1-66809737d943", // rule id + category: "test", // rule type id + license: "basic", // rule type minimumLicenseRequired + name: "rule-name", // + ruleset: "alerts", // rule type producer + // Fields currently are not populated: author: ["Elastic"], - id: "a823fd56-5467-4727-acb1-66809737d943", + description: "Some rule description", + version: '1', + uuid: "uuid" // etc }, diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index da04db1086aa89..3eadcc21257b08 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -214,6 +214,10 @@ "version": { "ignore_above": 1024, "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" } } }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index a13b304e8adab0..2a066ca0bd15bc 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -91,6 +91,7 @@ export const EventSchema = schema.maybe( ruleset: ecsString(), uuid: ecsString(), version: ecsString(), + namespace: ecsString(), }) ), user: schema.maybe( diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 4aa6ed830059e7..e9ed14fbcddcd7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -217,6 +217,7 @@ instanceStateValue: true ruleTypeId: 'test.always-firing', outcome: 'success', message: `alert executed: test.always-firing:${alertId}: 'abc'`, + ruleObject: alertSearchResultWithoutDates, }); break; default: @@ -1249,10 +1250,11 @@ instanceStateValue: true outcome: string; message: string; errorMessage?: string; + ruleObject: any; } async function validateEventLog(params: ValidateEventLogParams): Promise { - const { spaceId, alertId, ruleTypeId, outcome, message, errorMessage } = params; + const { spaceId, alertId, outcome, message, errorMessage, ruleObject } = params; const events: IValidatedEvent[] = await retry.try(async () => { return await getEventLog({ @@ -1293,10 +1295,19 @@ instanceStateValue: true type: 'alert', id: alertId, namespace: spaceId, - type_id: ruleTypeId, + type_id: ruleObject.alertInfo.ruleTypeId, }, ]); + expect(event?.rule).to.eql({ + id: alertId, + license: 'basic', + category: ruleObject.alertInfo.ruleTypeId, + ruleset: ruleObject.alertInfo.producer, + namespace: spaceId, + name: ruleObject.alertInfo.name, + }); + expect(event?.message).to.eql(message); if (errorMessage) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts index d5e55a66ecf086..5d13d641367a4c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -81,6 +81,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) { errorMessage: 'Unable to decrypt attribute "apiKey"', status: 'error', reason: 'decrypt', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: spaceId, + }, }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index c1f6bcb9e15100..781967ff5596a9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -134,6 +134,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) { outcome: 'success', message: `alert executed: test.patternFiring:${alertId}: 'abc'`, status: executeStatuses[executeCount++], + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + name: response.body.name, + }, }); break; case 'execute-action': @@ -146,6 +154,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, instanceId: 'instance', actionGroupId: 'default', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + name: response.body.name, + }, }); break; case 'new-instance': @@ -181,6 +197,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) { instanceId: 'instance', actionGroupId: 'default', shouldHaveEventEnd, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + name: response.body.name, + }, }); } }); @@ -279,6 +303,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) { outcome: 'success', message: `alert executed: test.patternFiring:${alertId}: 'abc'`, status: executeStatuses[executeCount++], + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + name: response.body.name, + }, }); break; case 'execute-action': @@ -294,6 +326,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`, instanceId: 'instance', actionGroupId: 'default', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + name: response.body.name, + }, }); break; case 'new-instance': @@ -332,6 +372,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) { instanceId: 'instance', actionGroupId: 'default', shouldHaveEventEnd, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + name: response.body.name, + }, }); } }); @@ -374,6 +422,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) { errorMessage: 'this alert is intended to fail', status: 'error', reason: 'execute', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + namespace: Spaces.space1.id, + }, }); }); }); @@ -397,10 +452,21 @@ interface ValidateEventLogParams { actionGroupId?: string; instanceId?: string; reason?: string; + rule: { + id: string; + name?: string; + version?: string; + category?: string; + reference?: string; + author?: string[]; + license?: string; + ruleset?: string; + namespace?: string; + }; } export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { - const { spaceId, savedObjects, outcome, message, errorMessage } = params; + const { spaceId, savedObjects, outcome, message, errorMessage, rule } = params; const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params; if (status) { @@ -456,6 +522,8 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa expect(event?.message).to.eql(message); + expect(event?.rule).to.eql(rule); + if (errorMessage) { expect(event?.error?.message).to.eql(errorMessage); } From 6df58dd7ca53b43c2f143823ebbe51083618032b Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 10 Jun 2021 16:32:23 -0400 Subject: [PATCH 34/99] [Fleet] Fix fleet server collector in case settings are not set (#101752) --- .../server/collectors/fleet_server_collector.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts index d861b211b88484..9616ba11545e0d 100644 --- a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { isBoom } from '@hapi/boom'; import type { SavedObjectsClient, ElasticsearchClient } from 'kibana/server'; import { packagePolicyService, settingsService } from '../services'; @@ -38,11 +40,18 @@ export const getFleetServerUsage = async ( return DEFAULT_USAGE; } - const numHostsUrls = - (await settingsService.getSettings(soClient)).fleet_server_hosts?.length ?? 0; + const numHostsUrls = await settingsService + .getSettings(soClient) + .then((settings) => settings.fleet_server_hosts?.length ?? 0) + .catch((err) => { + if (isBoom(error) && error.output.statusCode === 404) { + return 0; + } - // Find all policies with Fleet server than query agent status + throw err; + }); + // Find all policies with Fleet server than query agent status let hasMore = true; const policyIds = new Set(); let page = 1; From a51b4a12e8dfad6b297cba9f533ffba73a63f6a1 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 27 May 2021 14:30:32 -0700 Subject: [PATCH 35/99] temporarily disable build-buddy --- packages/kbn-pm/dist/index.js | 2 +- packages/kbn-pm/src/utils/bazel/run.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index e455f487d13843..1311eb4d7c6388 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline stdio: 'pipe' }); - if (offline) { + if (offline || !offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts index c030081e53daaf..5f3743876e0e4a 100644 --- a/packages/kbn-pm/src/utils/bazel/run.ts +++ b/packages/kbn-pm/src/utils/bazel/run.ts @@ -29,7 +29,7 @@ async function runBazelCommandWithRunner( stdio: 'pipe', }; - if (offline) { + if (offline || !offline) { bazelArgs = [...bazelArgs, '--config=offline']; } From b24276565483ac3ecc156d9eadb2e5647bcf1e37 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Thu, 10 Jun 2021 17:46:53 -0400 Subject: [PATCH 36/99] [Alerting][Docs] Combine rule creation and management pages (#101498) * Combining rule management stuff into single page * Cleaning up image widths and header sizes * Adding in placeholder pages * Adding in placeholder pages * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * PR and test fixes * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * PR fixes * PR fixes Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/api/alerting/list_rule_types.asciidoc | 2 +- docs/apm/apm-alerts.asciidoc | 2 +- .../connectors/action-types/index.asciidoc | 2 +- .../connectors/action-types/webhook.asciidoc | 2 +- docs/rule-type-template.asciidoc | 4 +- .../alerting/create-and-manage-rules.asciidoc | 184 ++++++++++++++++++ docs/user/alerting/defining-rules.asciidoc | 129 +----------- .../images/individual-mute-disable.png | Bin 68234 -> 206741 bytes .../rule-flyout-action-no-connector.png | Bin 0 -> 46309 bytes docs/user/alerting/index.asciidoc | 2 +- .../map-rules/geo-rule-types.asciidoc | 4 +- docs/user/alerting/rule-details.asciidoc | 33 ---- docs/user/alerting/rule-management.asciidoc | 60 +----- .../alerting/stack-rules/es-query.asciidoc | 6 +- .../stack-rules/index-threshold.asciidoc | 6 +- docs/user/introduction.asciidoc | 4 +- docs/user/management.asciidoc | 2 +- docs/user/monitoring/kibana-alerts.asciidoc | 2 +- ...lerting-production-considerations.asciidoc | 4 +- .../public/doc_links/doc_links_service.ts | 2 +- .../public/application/home.test.tsx | 2 +- 21 files changed, 211 insertions(+), 241 deletions(-) create mode 100644 docs/user/alerting/create-and-manage-rules.asciidoc create mode 100644 docs/user/alerting/images/rule-flyout-action-no-connector.png delete mode 100644 docs/user/alerting/rule-details.asciidoc diff --git a/docs/api/alerting/list_rule_types.asciidoc b/docs/api/alerting/list_rule_types.asciidoc index 98016c8cf82fa9..31c8416e750593 100644 --- a/docs/api/alerting/list_rule_types.asciidoc +++ b/docs/api/alerting/list_rule_types.asciidoc @@ -8,7 +8,7 @@ Retrieve a list of alerting rule types that the user is authorized to access. Each rule type includes a list of consumer features. Within these features, users are authorized to perform either `read` or `all` operations on rules of that type. This helps determine which rule types users can read, but not create or modify. -NOTE: Some rule types are limited to specific features. These rule types are not available when <> in <>. +NOTE: Some rule types are limited to specific features. These rule types are not available when <> in <>. [[list-rule-types-api-request]] ==== Request diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index 0cee0c04d3fb6d..3e3e2b178ff10b 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -15,7 +15,7 @@ and enables central management of all alerts from <>. +see Kibana's <>. The APM app supports four different types of alerts: diff --git a/docs/management/connectors/action-types/index.asciidoc b/docs/management/connectors/action-types/index.asciidoc index d3bd3d431748c9..7868085ef9c96f 100644 --- a/docs/management/connectors/action-types/index.asciidoc +++ b/docs/management/connectors/action-types/index.asciidoc @@ -119,7 +119,7 @@ When creating a new rule, add an <> and select [role="screenshot"] image::images/pre-configured-alert-history-connector.png[Select pre-configured alert history connectors] -Documents are indexed using a preconfigured schema that captures the <> available for the rule. By default, these documents are indexed into the `kibana-alert-history-default` index, but you can specify a different index. Index names must start with `kibana-alert-history-` to take advantage of the preconfigured alert history index template. +Documents are indexed using a preconfigured schema that captures the <> available for the rule. By default, these documents are indexed into the `kibana-alert-history-default` index, but you can specify a different index. Index names must start with `kibana-alert-history-` to take advantage of the preconfigured alert history index template. [IMPORTANT] ============================================== diff --git a/docs/management/connectors/action-types/webhook.asciidoc b/docs/management/connectors/action-types/webhook.asciidoc index aa52e8a3bdb433..02c3de139e0d5d 100644 --- a/docs/management/connectors/action-types/webhook.asciidoc +++ b/docs/management/connectors/action-types/webhook.asciidoc @@ -91,4 +91,4 @@ Body:: A JSON payload sent to the request URL. For example: Mustache template variables (the text enclosed in double braces, for example, `context.rule.name`) have their values escaped, so that the final JSON will be valid (escaping double quote characters). -For more information on Mustache template variables, refer to <>. +For more information on Mustache template variables, refer to <>. diff --git a/docs/rule-type-template.asciidoc b/docs/rule-type-template.asciidoc index 605bdd57c14920..5fe4de81bcddc1 100644 --- a/docs/rule-type-template.asciidoc +++ b/docs/rule-type-template.asciidoc @@ -6,7 +6,7 @@ Include a short description of the rule type. [float] ==== Create the rule -Fill in the <>, then select **. +Fill in the <>, then select **. [float] ==== Define the conditions @@ -25,7 +25,7 @@ Condition2:: This is another condition the user must define. [float] ==== Add action variables -<> to run when the rule condition is met. The following variables are specific to the rule. You can also specify <>. +<> to run when the rule condition is met. The following variables are specific to the rule. You can also specify <>. `context.variableA`:: A short description of the context variable defined by the rule type. `context.variableB`:: A short description of the context variable defined by the rule type with an example. Example: `this is what variableB outputs`. diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc new file mode 100644 index 00000000000000..af6714aef662ff --- /dev/null +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -0,0 +1,184 @@ +[role="xpack"] +[[create-and-manage-rules]] +== Create and manage rules + +The *Rules* UI provides a cross-app view of alerting. Different {kib} apps like {observability-guide}/create-alerts.html[*Observability*], {security-guide}/prebuilt-rules.html[*Security*], <> and <> can offer their own rules. The *Rules* UI provides a central place to: + +* <> rules +* <> including enabling/disabling, muting/unmuting, and deleting +* Drill-down to <> + +[role="screenshot"] +image:images/rules-and-connectors-ui.png[Example rule listing in the Rules and Connectors UI] + +For more information on alerting concepts and the types of rules and connectors available, see <>. + +[float] +=== Required permissions + +Access to rules is granted based on your privileges to alerting-enabled features. See <> for more information. + +[float] +[[create-edit-rules]] +=== Create and edit rules + +Many rules must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic rule types can be created in the *Rules* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting a rule type and configuring its conditions and action type. Refer to <> for details on what types of rules are available and how to configure them. + +After a rule is created, you can re-open the flyout and change a rule's properties by clicking the *Edit* button shown on each row of the rule listing. + +[float] +[[defining-rules-general-details]] +==== General rule details + +All rules share the following four properties. + +Name:: The name of the rule. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable rule listing in the *Management* UI. A distinctive name can help identify and find a rule. +Tags:: A list of tag names that can be applied to a rule. Tags can help you organize and find rules, because tags appear in the rule listing in the *Management* UI, which is searchable by tag. +Check every:: This value determines how frequently the rule conditions are checked. Note that the timing of background rule checks is not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. +Notify:: This value limits how often actions are repeated when an alert remains active across rule checks. See <> for more information. + +- **Only on status change**: Actions are not repeated when an alert remains active across checks. Actions run only when the alert status changes. +- **Every time alert is active**: Actions are repeated when an alert remains active across checks. +- **On a custom action interval**: Actions are suppressed for the throttle interval, but repeat when an alert remains active across checks for a duration longer than the throttle interval. + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +[NOTE] +============================================== +Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same rule. Often, it's desirable to suppress these re-notifications. If you set the rule **Notify** setting to **On a custom action interval** with an interval of 5 minutes, you reduce noise by only getting emails every 5 minutes for servers that continue to exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +To get notified **only once** when a server exceeds the threshold, you can set the rule's **Notify** setting to **Only on status change**. +============================================== + +[role="screenshot"] +image::images/rule-flyout-general-details.png[alt='All rules have name, tags, check every, and notify properties in common'] + +[float] +[[defining-rules-type-conditions]] +==== Rule type and conditions + +Depending upon the {kib} app and context, you might be prompted to choose the type of rule to create. Some apps will pre-select the type of rule for you. + +[role="screenshot"] +image::images/rule-flyout-rule-type-selection.png[Choosing the type of rule to create] + +Each rule type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold rule, the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. + +[role="screenshot"] +image::images/rule-flyout-rule-conditions.png[UI for defining rule conditions on an index threshold rule] + +[float] +[[defining-rules-actions-details]] +==== Action type and details + +To receive notifications when a rule meets the defined conditions, you must add one or more actions. Start by selecting a type of connector for your action: + +[role="screenshot"] +image::images/rule-flyout-connector-type-selection.png[UI for selecting an action type] + +Each action must specify a <> instance. If no connectors exist for the selected type, click **Add connector** to create one. + +[role="screenshot"] +image::images/rule-flyout-action-no-connector.png[UI for adding connector] + +Once you have selected a connector, use the **Run When** dropdown to choose the action group to associate with this action. When a rule meets the defined condition, it is marked as **Active** and alerts are created and assigned to an action group. In addition to the action groups defined by the selected rule type, each rule also has a **Recovered** action group that is assigned when a rule's conditions are no longer detected. + +Each action type exposes different properties. For example, an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. + +[role="screenshot"] +image::images/rule-flyout-action-details.png[UI for defining an email action] + +[float] +[[defining-rules-actions-variables]] +===== Action variables +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass rule values at the time a condition is detected to an action. You can access the list of available variables using the "add variable" button. Although available variables differ by rule type, all rule types pass the following variables: + +`rule.id`:: The ID of the rule. +`rule.name`:: The name of the rule. +`rule.spaceId`:: The ID of the space for the rule. +`rule.tags`:: The list of tags applied to the rule. +`date`:: The date the rule scheduled the action, in ISO format. +`alert.id`:: The ID of the alert that scheduled the action. +`alert.actionGroup`:: The ID of the action group of the alert that scheduled the action. +`alert.actionSubgroup`:: The action subgroup of the alert that scheduled the action. +`alert.actionGroupName`:: The name of the action group of the alert that scheduled the action. +`kibanaBaseUrl`:: The configured <>. If not configured, this will be empty. + +[role="screenshot"] +image::images/rule-flyout-action-variables.png[Passing rule values to an action] + +Some cases exist where the variable values will be "escaped", when used in a context where escaping is needed: + +- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Markdown. +- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Slack Markdown. +- For the <> connector, the `body` action configuration property escapes any characters that are invalid in JSON string values. + +Mustache also supports "triple braces" of the form `{{{variable name}}}`, which indicates no escaping should be done at all. Care should be used when using this form, as it could end up rendering the variable content in such a way as to make the resulting parameter invalid or formatted incorrectly. + +Each rule type defines additional variables as properties of the variable `context`. For example, if a rule type defines a variable `value`, it can be used in an action parameter as `{{context.value}}`. + +For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the rule type. + +You can attach more than one action. Clicking the "Add action" button will prompt you to select another rule type and repeat the above steps again. + +[role="screenshot"] +image::images/rule-flyout-add-action.png[You can add multiple actions on a rule] + +[NOTE] +============================================== +Actions are not required on rules. You can run a rule without actions to understand its behavior, and then <> later. +============================================== + +[float] +[[controlling-rules]] +=== Mute and disable rules + +The rule listing allows you to quickly mute/unmute, disable/enable, and delete individual rules by clicking menu button to open the actions menu. Muting means that the rule checks continue to run on a schedule, but that alert will not trigger any action. + +[role="screenshot"] +image:images/individual-mute-disable.png[The actions button allows an individual rule to be muted, disabled, or deleted] + +You can perform these operations in bulk by multi-selecting rules, and then clicking the *Manage rules* button: + +[role="screenshot"] +image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk,width=75%] + +[float] +[[rule-details]] +=== Drilldown to rule details + +Select a rule name from the rule listing to access the *Rule details* page, which tells you about the state of the rule and provides granular control over the actions it is taking. + +[role="screenshot"] +image::images/rule-details-alerts-active.png[Rule details page with three alerts] + +In this example, the rule detects when a site serves more than a threshold number of bytes in a 24 hour period. Three sites are above the threshold. These are called alerts - occurrences of the condition being detected - and the alert name, status, time of detection, and duration of the condition are shown in this view. + +Upon detection, each alert can trigger one or more actions. If the condition persists, the same actions will trigger either on the next scheduled rule check, or (if defined) after the re-notify period on the rule has passed. To prevent re-notification, you can suppress future actions by clicking on the switch to mute an individual alert. + +[role="screenshot"] +image::images/rule-details-alert-muting.png[Muting an alert,width=50%] + +Alerts will come and go from the list depending on whether they meet the rule conditions or not - unless they are muted. If a muted instance no longer meets the rule conditions, it will appear as inactive in the list. This prevents an alert from triggering actions if it reappears in the future. + +[role="screenshot"] +image::images/rule-details-alerts-inactive.png[Rule details page with three inactive alerts] + +If you want to suppress actions on all current and future alerts, you can mute the entire rule. Rule checks continue to run and the alert list will update as alerts activate or deactivate, but no actions will be triggered. + +[role="screenshot"] +image::images/rule-details-muting.png[Use the mute toggle to suppress all actions on current and future alerts,width=50%] + +You can also disable a rule altogether. When disabled, the rule stops running checks altogether and will clear any alerts it is tracking. You may want to disable rules that are not currently needed to reduce the load on {kib} and {es}. + +[role="screenshot"] +image::images/rule-details-disabling.png[Use the disable toggle to turn off rule checks and clear alerts tracked] diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc index c48108ca9acc0a..686a7bbc8a37b9 100644 --- a/docs/user/alerting/defining-rules.asciidoc +++ b/docs/user/alerting/defining-rules.asciidoc @@ -2,133 +2,10 @@ [[defining-alerts]] == Defining rules -{kib} alerting rules can be created in a variety of apps including <>, <>, <>, <>, <> and from the <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring rules that this section describes in more detail. - -[float] -=== Create a rule - -When you create a rule, you must define the rule details, conditions, and actions. - -. <> -. <> -. <> - -image::images/rule-flyout-sections.png[The three sections of a rule definition] +This content has been moved to <>. [float] [[defining-alerts-general-details]] -=== General rule details - -All rules share the following four properties. - -[role="screenshot"] -image::images/rule-flyout-general-details.png[alt='All rules have name, tags, check every, and notify properties in common'] - -Name:: The name of the rule. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable rule listing in the management UI. A distinctive name can help identify and find a rule. -Tags:: A list of tag names that can be applied to a rule. Tags can help you organize and find rules, because tags appear in the rule listing in the management UI which is searchable by tag. -Check every:: This value determines how frequently the rule conditions below are checked. Note that the timing of background rule checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. -Notify:: This value limits how often actions are repeated when an alert remains active across rule checks. See <> for more information. + -- **Only on status change**: Actions are not repeated when an alert remains active across checks. Actions run only when the alert status changes. -- **Every time alert is active**: Actions are repeated when an alert remains active across checks. -- **On a custom action interval**: Actions are suppressed for the throttle interval, but repeat when an alert remains active across checks for a duration longer than the throttle interval. - -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -[NOTE] -============================================== -Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same rule. Often, it's desirable to suppress these re-notifications. If you set the rule **Notify** setting to **On a custom action interval** with an interval of 5 minutes, you reduce noise by only getting emails every 5 minutes for servers that continue to exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - -To get notified **only once** when a server exceeds the threshold, you can set the rule's **Notify** setting to **Only on status change**. -============================================== - - -[float] -[[defining-alerts-type-conditions]] -=== Rule type and conditions - -Depending upon the {kib} app and context, you may be prompted to choose the type of rule you wish to create. Some apps will pre-select the type of rule for you. - -[role="screenshot"] -image::images/rule-flyout-rule-type-selection.png[Choosing the type of rule to create] - -Each rule type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold rule the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. - -[role="screenshot"] -image::images/rule-flyout-rule-conditions.png[UI for defining rule conditions on an index threshold rule] - -[float] -[[defining-alerts-actions-details]] -=== Action type and action details - -To add an action to a rule, you first select the type of connector: - -[role="screenshot"] -image::images/rule-flyout-connector-type-selection.png[UI for selecting an action type] - -When an alert matches a condition, the rule is marked as _Active_ and assigned an action group. The actions in that group are triggered. -When the condition is no longer detected, the rule is assigned to the _Recovered_ action group, which triggers any actions assigned to that group. - -**Run When** allows you to assign an action to an action group. This will trigger the action in accordance with your **Notify** setting. - -Each action must specify a <> instance. If no connectors exist for that action type, click *Add connector* to create one. - -Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. - -[role="screenshot"] -image::images/rule-flyout-action-details.png[UI for defining an email action] - -[float] -[[defining-alerts-actions-variables]] -==== Action variables -Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass rule values at the time a condition is detected to an action. You can access the list of available variables using the "add variable" button. Although available variables differ by rule type, all rule types pass the following variables: - -`rule.id`:: The ID of the rule. -`rule.name`:: The name of the rule. -`rule.spaceId`:: The ID of the space for the rule. -`rule.tags`:: The list of tags applied to the rule. -`date`:: The date the rule scheduled the action, in ISO format. -`alert.id`:: The ID of the alert that scheduled the action. -`alert.actionGroup`:: The ID of the action group of the alert that scheduled the action. -`alert.actionSubgroup`:: The action subgroup of the alert that scheduled the action. -`alert.actionGroupName`:: The name of the action group of the alert that scheduled the action. -`kibanaBaseUrl`:: The configured <>. If not configured, this will be empty. - -[role="screenshot"] -image::images/rule-flyout-action-variables.png[Passing rule values to an action] - -Some cases exist where the variable values will be "escaped", when used in a context where escaping is needed: - -- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Markdown. -- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Slack Markdown. -- For the <> connector, the `body` action configuration property escapes any characters that are invalid in JSON string values. - -Mustache also supports "triple braces" of the form `{{{variable name}}}`, which indicates no escaping should be done at all. Care should be used when using this form, as it could end up rendering the variable content in such a way as to make the resulting parameter invalid or formatted incorrectly. - -Each rule type defines additional variables as properties of the variable `context`. For example, if a rule type defines a variable `value`, it can be used in an action parameter as `{{context.value}}`. - -For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the rule type. - -You can attach more than one action. Clicking the "Add action" button will prompt you to select another rule type and repeat the above steps again. - -[role="screenshot"] -image::images/rule-flyout-add-action.png[You can add multiple actions on a rule] - -[NOTE] -============================================== -Actions are not required on rules. You can run a rule without actions to understand its behavior, and then <> later. -============================================== - -[float] -=== Manage rules +==== General rule details -To modify a rule after it was created, including muting or disabling it, use the <>. +This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/images/individual-mute-disable.png b/docs/user/alerting/images/individual-mute-disable.png index 0ed2bfc0186c0807805f8d2aa433cfe665838c66..c9d8cd666f1d849c54f6dbc8cb3c552fa4acd14f 100644 GIT binary patch literal 206741 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 z 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%0AT;x`tA@=H&z8czOQT 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 D-A4w| diff --git a/docs/user/alerting/images/rule-flyout-action-no-connector.png b/docs/user/alerting/images/rule-flyout-action-no-connector.png new file mode 100644 index 0000000000000000000000000000000000000000..b8b0864e04226dfdd13921049db20f66f1fca96b GIT binary patch literal 46309 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 literal 0 HcmV?d00001 diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index a331f1d5606f7e..68cf3ee070b089 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,8 +1,8 @@ include::alerting-getting-started.asciidoc[] include::alerting-setup.asciidoc[] +include::create-and-manage-rules.asciidoc[] include::defining-rules.asciidoc[] include::rule-management.asciidoc[] -include::rule-details.asciidoc[] include::stack-rules.asciidoc[] include::domain-specific-rules.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/map-rules/geo-rule-types.asciidoc b/docs/user/alerting/map-rules/geo-rule-types.asciidoc index 4b17145c2d1493..eee7b592522054 100644 --- a/docs/user/alerting/map-rules/geo-rule-types.asciidoc +++ b/docs/user/alerting/map-rules/geo-rule-types.asciidoc @@ -30,8 +30,8 @@ than the current time minus the amount of the interval. If data older than [float] ==== Creating a geo rule -Click the *Create* button in the <>. -Complete the <>. +Click the *Create* button in the <>. +Complete the <>. [role="screenshot"] image::user/alerting/images/alert-types-tracking-select.png[Choosing a tracking rule type] diff --git a/docs/user/alerting/rule-details.asciidoc b/docs/user/alerting/rule-details.asciidoc deleted file mode 100644 index 6e743595e5c33b..00000000000000 --- a/docs/user/alerting/rule-details.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[role="xpack"] -[[rule-details]] -== Rule details - - -The *Rule details* page tells you about the state of the rule and provides granular control over the actions it is taking. - -[role="screenshot"] -image::images/rule-details-alerts-active.png[Rule details page with three alerts] - -In this example, the rule detects when a site serves more than a threshold number of bytes in a 24 hour period. Three sites are above the threshold. These are called alerts - occurrences of the condition being detected - and the alert name, status, time of detection, and duration of the condition are shown in this view. - -Upon detection, each alert can trigger one or more actions. If the condition persists, the same actions will trigger either on the next scheduled rule check, or (if defined) after the re-notify period on the rule has passed. To prevent re-notification, you can suppress future actions by clicking on the eye icon to mute an individual alert. Muting means that the rule checks continue to run on a schedule, but that alert will not trigger any action. - -[role="screenshot"] -image::images/rule-details-alert-muting.png[Muting an alert] - -Alerts will come and go from the list depending on whether they meet the rule conditions or not - unless they are muted. If a muted instance no longer meets the rule conditions, it will appear as inactive in the list. This prevents an alert from triggering actions if it reappears in the future. - -[role="screenshot"] -image::images/rule-details-alerts-inactive.png[Rule details page with three inactive alerts] - -If you want to suppress actions on all current and future alerts, you can mute the entire rule. Rule checks continue to run and the alert list will update as alerts activate or deactivate, but no actions will be triggered. - -[role="screenshot"] -image::images/rule-details-muting.png[Use the mute toggle to suppress all actions on current and future alerts] - -You can also disable a rule altogether. When disabled, the rule stops running checks altogether and will clear any alerts it is tracking. You may want to disable rules that are not currently needed to reduce the load on {kib} and {es}. - -[role="screenshot"] -image::images/rule-details-disabling.png[Use the disable toggle to turn off rule checks and clear alerts tracked] - -* For further information on alerting concepts and examples, see <>. diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index b908bd03b09927..d6349a60e08eb7 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -2,62 +2,4 @@ [[alert-management]] == Managing rules - -The *Rules* tab provides a cross-app view of alerting. Different {kib} apps like {observability-guide}/create-alerts.html[*Observability*], {security-guide}/prebuilt-rules.html[*Security*], <> and <> can offer their own rules. The *Rules* tab provides a central place to: - -* <> rules -* <> including enabling/disabling, muting/unmuting, and deleting -* Drill-down to <> - -[role="screenshot"] -image:images/rules-and-connectors-ui.png[Example rule listing in the Rules and Connectors UI] - -For more information on alerting concepts and the types of rules and connectors available, see <>. - -[float] -=== Finding rules - -The *Rules* tab lists all rules in the current space, including summary information about their execution frequency, tags, and type. - -The *search bar* can be used to quickly find rules by name or tag. - -[role="screenshot"] -image::images/rules-filter-by-search.png[Filtering the rules list using the search bar] - -The *type* dropdown lets you filter to a subset of rule types. - -[role="screenshot"] -image::images/rules-filter-by-type.png[Filtering the rules list by types of rule] - -The *Action type* dropdown lets you filter by the type of action used in the rule. - -[role="screenshot"] -image::images/rules-filter-by-action-type.png[Filtering the rule list by type of action] - -[float] -[[create-edit-rules]] -=== Creating and editing rules - -Many rules must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic rule types can be created in the *Rules* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting a rule type and configuring its properties. Refer to <> for details on what types of rules are available and how to configure them. - -After a rule is created, you can re-open the flyout and change a rule's properties by clicking the *Edit* button shown on each row of the rule listing. - - -[float] -[[controlling-rules]] -=== Controlling rules - -The rule listing allows you to quickly mute/unmute, disable/enable, and delete individual rules by clicking the action button. - -[role="screenshot"] -image:images/individual-mute-disable.png[The actions button allows an individual rule to be muted, disabled, or deleted] - -These operations can also be performed in bulk by multi-selecting rules and clicking the *Manage rules* button: - -[role="screenshot"] -image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk] - -[float] -=== Required permissions - -Access to rules is granted based on your privileges to alerting-enabled features. See <> for more information. +This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/stack-rules/es-query.asciidoc b/docs/user/alerting/stack-rules/es-query.asciidoc index c62ebbf4bf2bcc..5615c79a6c9c74 100644 --- a/docs/user/alerting/stack-rules/es-query.asciidoc +++ b/docs/user/alerting/stack-rules/es-query.asciidoc @@ -7,7 +7,7 @@ The {es} query rule type runs a user-configured {es} query, compares the number [float] ==== Create the rule -Fill in the <>, then select *{es} query*. +Fill in the <>, then select *{es} query*. [float] ==== Define the conditions @@ -22,12 +22,12 @@ Size:: This clause specifies the number of documents to pass to the configured a {es} query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold condition. Aggregations are not supported at this time. Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. -Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <>, to avoid gaps in detection. +Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <>, to avoid gaps in detection. [float] ==== Add action variables -<> to run when the rule condition is met. The following variables are specific to the {es} query rule. You can also specify <>. +<> to run when the rule condition is met. The following variables are specific to the {es} query rule. You can also specify <>. `context.title`:: A preconstructed title for the rule. Example: `rule term match alert query matched`. `context.message`:: A preconstructed message for the rule. Example: + diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/stack-rules/index-threshold.asciidoc index e152ee7cb1deb2..8c45c158414f4b 100644 --- a/docs/user/alerting/stack-rules/index-threshold.asciidoc +++ b/docs/user/alerting/stack-rules/index-threshold.asciidoc @@ -7,7 +7,7 @@ The index threshold rule type runs an {es} query. It aggregates field values fro [float] ==== Create the rule -Fill in the <>, then select *Index Threshold*. +Fill in the <>, then select *Index Threshold*. [float] ==== Define the conditions @@ -21,7 +21,7 @@ Index:: This clause requires an *index or index pattern* and a *time field* that When:: This clause specifies how the value to be compared to the threshold is calculated. The value is calculated by aggregating a numeric field a the *time window*. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used, and an aggregation field is not necessary. Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. Threshold:: This clause defines a threshold value and a comparison operator (one of `is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The result of the aggregation is compared to this threshold. -Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be to a value higher than the *check every* value in the <>, to avoid gaps in detection. +Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be to a value higher than the *check every* value in the <>, to avoid gaps in detection. If data is available and all clauses have been defined, a preview chart will render the threshold value and display a line chart showing the value for the last 30 intervals. This can provide an indication of recent values and their proximity to the threshold, and help you tune the clauses. @@ -31,7 +31,7 @@ image::user/alerting/images/rule-types-index-threshold-preview.png[Five clauses [float] ==== Add action variables -<> to run when the rule condition is met. The following variables are specific to the index threshold rule. You can also specify <>. +<> to run when the rule condition is met. The following variables are specific to the index threshold rule. You can also specify <>. `context.title`:: A preconstructed title for the rule. Example: `rule kibana sites - high egress met threshold`. `context.message`:: A preconstructed message for the rule. Example: + diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 25780d303eec41..82ca11f2162fda 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -195,7 +195,7 @@ When the rule triggers, you can send a notification to a system that is part of your daily workflow. {kib} integrates with email, Slack, PagerDuty, and ServiceNow, to name a few. -A dedicated view for creating, searching, and editing rules is in <>. +A dedicated view for creating, searching, and editing rules is in <>. [role="screenshot"] image::images/rules-and-connectors.png[Rules and Connectors view] @@ -437,7 +437,7 @@ the <>. |< Data>> |Set up rules -|< Rules and Connectors>> +|< Rules and Connectors>> |Organize your workspace and users |< Spaces>> diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index c5fabb15dc4de2..b86fa82c30381b 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -75,7 +75,7 @@ You can add and remove remote clusters, and check their connectivity. |=== | <> -| Centrally <> across {kib}. Create and <> across {kib}. Create and <> for triggering actions. | <> diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 58bf419d8d54a9..4219a56a3d9b0b 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -20,7 +20,7 @@ analyze past performance. You can also modify active alerts. image::user/monitoring/images/monitoring-kibana-alerts.png["Kibana alerts in the Stack Monitoring app"] To review and modify all the available alerts, use -<> in *{stack-manage-app}*. +<> in *{stack-manage-app}*. [discrete] [[kibana-alerts-cpu-threshold]] diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index 6294a4fe6f14ae..bd19a11435a99d 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -19,7 +19,7 @@ When relying on rules and actions as mission critical services, make sure you fo By default, each {kib} instance polls for work at three second intervals, and can run a maximum of ten concurrent tasks. These tasks are then run on the {kib} server. -Rules are recurring background tasks which are rescheduled according to the <> on completion. +Rules are recurring background tasks which are rescheduled according to the <> on completion. Actions are non-recurring background tasks which are deleted on completion. For more details on Task Manager, see <>. @@ -42,7 +42,7 @@ As rules and actions leverage background tasks to perform the majority of work, When estimating the required task throughput, keep the following in mind: -* Each rule uses a single recurring task that is scheduled to run at the cadence defined by its <>. +* Each rule uses a single recurring task that is scheduled to run at the cadence defined by its <>. * Each action uses a single task. However, because <>, alerts can generate a large number of non-recurring tasks. It is difficult to predict how much throughput is needed to ensure all rules and actions are executed at consistent schedules. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 4912ae490b565b..53428edf4b345f 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -253,7 +253,7 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, }, alerting: { - guide: `${KIBANA_DOCS}alert-management.html`, + guide: `${KIBANA_DOCS}create-and-manage-rules.html`, actionTypes: `${KIBANA_DOCS}action-types.html`, emailAction: `${KIBANA_DOCS}email-action-type.html`, emailActionConfig: `${KIBANA_DOCS}email-action-type.html`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx index 0e1c27c1e67688..3594374a54f167 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx @@ -37,7 +37,7 @@ describe('home', () => { const documentationLink = wrapper.find('[data-test-subj="documentationLink"]'); expect(documentationLink.exists()).toBeTruthy(); expect(documentationLink.first().prop('href')).toEqual( - 'https://www.elastic.co/guide/en/kibana/mocked-test-branch/alert-management.html' + 'https://www.elastic.co/guide/en/kibana/mocked-test-branch/create-and-manage-rules.html' ); }); }); From de07e986630194ce5e3cb3ebad46e7de77fa7904 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 10 Jun 2021 16:39:27 -0600 Subject: [PATCH 37/99] [Observability] [Cases] Cases in the observability app (#101487) --- .../collectors/application_usage/schema.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 131 +++++++ test/functional/config.js | 3 + .../all_cases/all_cases_generic.tsx | 6 + .../public/components/all_cases/columns.tsx | 24 +- .../public/components/all_cases/header.tsx | 4 +- .../public/components/all_cases/index.tsx | 2 + .../components/callout/callout.test.tsx | 2 +- .../public/components/callout/callout.tsx | 6 +- .../public/components/callout/helpers.tsx | 2 +- .../case_action_bar/actions.test.tsx | 15 +- .../components/case_action_bar/actions.tsx | 9 +- .../components/case_action_bar/index.test.tsx | 5 + .../components/case_action_bar/index.tsx | 49 +-- .../components/case_view/does_not_exist.tsx | 32 ++ .../public/components/case_view/index.tsx | 23 +- .../components/case_view/translations.ts | 17 + .../cases/public/components/create/form.tsx | 13 +- .../public/components/create/form_context.tsx | 2 +- .../cases/public/components/create/index.tsx | 3 + .../cases/public/components/links/index.tsx | 2 +- .../components/use_push_to_service/index.tsx | 4 +- .../components/user_action_tree/index.tsx | 56 +-- .../cases/public/containers/api.test.tsx | 4 +- x-pack/plugins/cases/public/containers/api.ts | 2 +- x-pack/plugins/observability/common/const.ts | 9 + x-pack/plugins/observability/kibana.json | 10 +- .../components/app/cases/all_cases/index.tsx | 74 ++++ .../app/cases/callout/callout.test.tsx | 90 +++++ .../components/app/cases/callout/callout.tsx | 52 +++ .../app/cases/callout/helpers.test.tsx | 29 ++ .../components/app/cases/callout/helpers.tsx | 22 ++ .../app/cases/callout/index.test.tsx | 216 ++++++++++++ .../components/app/cases/callout/index.tsx | 104 ++++++ .../app/cases/callout/translations.ts | 30 ++ .../components/app/cases/callout/types.ts | 13 + .../components/app/cases/case_view/helpers.ts | 12 + .../components/app/cases/case_view/index.tsx | 122 +++++++ .../public/components/app/cases/constants.ts | 11 + .../app/cases/create/flyout.test.tsx | 55 +++ .../components/app/cases/create/flyout.tsx | 81 +++++ .../app/cases/create/index.test.tsx | 90 +++++ .../components/app/cases/create/index.tsx | 42 +++ .../components/app/cases/translations.ts | 203 +++++++++++ .../components/app/cases/wrappers/index.tsx | 21 ++ .../public/hooks/use_breadcrumbs.ts | 30 +- .../hooks/use_get_user_cases_permissions.tsx | 37 ++ .../public/hooks/use_messages_storage.tsx | 66 ++++ .../public/pages/cases/all_cases.tsx | 42 +++ .../public/pages/cases/case_details.tsx | 49 +++ .../public/pages/cases/cases.stories.tsx | 7 +- .../public/pages/cases/configure_cases.tsx | 71 ++++ .../public/pages/cases/create_case.tsx | 65 ++++ .../public/pages/cases/empty_page.tsx | 118 +++++++ .../pages/cases/feature_no_permissions.tsx | 38 +++ .../public/pages/cases/index.tsx | 51 --- .../observability/public/pages/cases/links.ts | 59 ++++ x-pack/plugins/observability/public/plugin.ts | 11 +- .../observability/public/routes/index.tsx | 46 ++- .../toggle_overview_link_in_nav.test.tsx | 5 +- .../public/toggle_overview_link_in_nav.tsx | 11 +- .../public/utils/kibana_react.ts | 19 ++ x-pack/plugins/observability/server/plugin.ts | 62 +++- x-pack/plugins/observability/tsconfig.json | 1 + .../cases/components/callout/callout.test.tsx | 2 +- .../cases/components/callout/callout.tsx | 6 +- .../cases/components/callout/helpers.tsx | 2 +- .../public/cases/pages/case.tsx | 6 +- .../public/cases/pages/case_details.tsx | 6 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../apis/features/features/features.ts | 1 + .../apis/security/privileges.ts | 1 + .../apis/security/privileges_basic.ts | 1 + .../observability/feature_controls/index.ts | 15 + .../observability_security.ts | 216 ++++++++++++ .../functional/apps/observability/index.ts | 15 + x-pack/test/functional/config.js | 2 + .../es_archives/cases/default/data.json.gz | Bin 0 -> 1355 bytes .../es_archives/cases/default/mappings.json | 322 ++++++++++++++++++ x-pack/test/functional/page_objects/index.ts | 2 + .../page_objects/observability_page.ts | 59 ++++ 82 files changed, 2949 insertions(+), 206 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx create mode 100644 x-pack/plugins/observability/common/const.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/translations.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/types.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/constants.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/translations.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx create mode 100644 x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx create mode 100644 x-pack/plugins/observability/public/hooks/use_messages_storage.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/all_cases.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/case_details.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/configure_cases.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/create_case.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/empty_page.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx delete mode 100644 x-pack/plugins/observability/public/pages/cases/index.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/links.ts create mode 100644 x-pack/plugins/observability/public/utils/kibana_react.ts create mode 100644 x-pack/test/functional/apps/observability/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/observability/feature_controls/observability_security.ts create mode 100644 x-pack/test/functional/apps/observability/index.ts create mode 100644 x-pack/test/functional/es_archives/cases/default/data.json.gz create mode 100644 x-pack/test/functional/es_archives/cases/default/mappings.json create mode 100644 x-pack/test/functional/page_objects/observability_page.ts 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 1fa7d8e846c9da..e7c6b53ff97b3c 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 @@ -148,6 +148,7 @@ export const applicationUsageSchema = { maps: commonSchema, ml: commonSchema, monitoring: commonSchema, + observabilityCases: commonSchema, 'observability-overview': commonSchema, osquery: commonSchema, security_account: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 7b6c4ba9788f14..51df1d3162b7c3 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -3970,6 +3970,137 @@ } } }, + "observabilityCases": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "observability-overview": { "properties": { "appId": { diff --git a/test/functional/config.js b/test/functional/config.js index 4a6791a3bc62fb..eac21e5a456184 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -97,6 +97,9 @@ export default async function ({ readConfigFile }) { pathname: '/app/home', hash: '/', }, + observabilityCases: { + pathname: '/app/observability/cases', + }, }, junit: { reportName: 'Chrome UI Functional Tests', diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 41509d9c0d1358..a364f8bf2b068a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -62,9 +62,11 @@ interface AllCasesGenericProps { caseDetailsNavigation?: CasesNavigation; // if not passed, case name is not displayed as a link (Formerly dependant on isSelectorView) configureCasesNavigation?: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelectorView) createCaseNavigation: CasesNavigation; + disableAlerts?: boolean; hiddenStatuses?: CaseStatusWithAllStatus[]; isSelectorView?: boolean; onRowClick?: (theCase?: Case | SubCase) => void; + showTitle?: boolean; updateCase?: (newCase: Case) => void; userCanCrud: boolean; } @@ -75,9 +77,11 @@ export const AllCasesGeneric = React.memo( caseDetailsNavigation, configureCasesNavigation, createCaseNavigation, + disableAlerts, hiddenStatuses = [], isSelectorView, onRowClick, + showTitle, updateCase, userCanCrud, }) => { @@ -190,6 +194,7 @@ export const AllCasesGeneric = React.memo( const columns = useCasesColumns({ caseDetailsNavigation, + disableAlerts, dispatchUpdateCaseProperty, filterStatus: filterOptions.status, handleIsLoading, @@ -271,6 +276,7 @@ export const AllCasesGeneric = React.memo( createCaseNavigation={createCaseNavigation} configureCasesNavigation={configureCasesNavigation} refresh={refresh} + showTitle={showTitle} userCanCrud={userCanCrud} /> )} diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index cf5da3928446e1..947d405d188cf0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -55,6 +55,7 @@ const renderStringField = (field: string, dataTestSubj: string) => export interface GetCasesColumn { caseDetailsNavigation?: CasesNavigation; + disableAlerts?: boolean; dispatchUpdateCaseProperty: (u: UpdateCase) => void; filterStatus: string; handleIsLoading: (a: boolean) => void; @@ -64,6 +65,7 @@ export interface GetCasesColumn { } export const useCasesColumns = ({ caseDetailsNavigation, + disableAlerts = false, dispatchUpdateCaseProperty, filterStatus, handleIsLoading, @@ -203,15 +205,19 @@ export const useCasesColumns = ({ }, truncateText: true, }, - { - align: RIGHT_ALIGNMENT, - field: 'totalAlerts', - name: ALERTS, - render: (totalAlerts: Case['totalAlerts']) => - totalAlerts != null - ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`) - : getEmptyTagValue(), - }, + ...(!disableAlerts + ? [ + { + align: RIGHT_ALIGNMENT, + field: 'totalAlerts', + name: ALERTS, + render: (totalAlerts: Case['totalAlerts']) => + totalAlerts != null + ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`) + : getEmptyTagValue(), + }, + ] + : []), { align: RIGHT_ALIGNMENT, field: 'totalComment', diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx index a6737b987e2c42..7452fe7e44b3c4 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx @@ -20,6 +20,7 @@ interface OwnProps { configureCasesNavigation: CasesNavigation; createCaseNavigation: CasesNavigation; refresh: number; + showTitle?: boolean; userCanCrud: boolean; } @@ -40,9 +41,10 @@ export const CasesTableHeader: FunctionComponent = ({ configureCasesNavigation, createCaseNavigation, refresh, + showTitle = true, userCanCrud, }) => ( - + ; // if not passed, case name is not displayed as a link (Formerly dependant on isSelector) configureCasesNavigation: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelector) createCaseNavigation: CasesNavigation; + disableAlerts?: boolean; + showTitle?: boolean; userCanCrud: boolean; } diff --git a/x-pack/plugins/cases/public/components/callout/callout.test.tsx b/x-pack/plugins/cases/public/components/callout/callout.test.tsx index 926fe7b63fb5af..0a0caa40a8783e 100644 --- a/x-pack/plugins/cases/public/components/callout/callout.test.tsx +++ b/x-pack/plugins/cases/public/components/callout/callout.test.tsx @@ -80,7 +80,7 @@ describe('Callout', () => { }); it('dismiss the callout correctly', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); wrapper.update(); diff --git a/x-pack/plugins/cases/public/components/callout/callout.tsx b/x-pack/plugins/cases/public/components/callout/callout.tsx index 8e2f439f02c4bd..4cd7fad10fe70c 100644 --- a/x-pack/plugins/cases/public/components/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/callout/callout.tsx @@ -35,11 +35,9 @@ const CallOutComponent = ({ type, ]); - return showCallOut ? ( + return showCallOut && !isEmpty(messages) ? ( - {!isEmpty(messages) && ( - - )} + {i18n.READ_ONLY_FEATURE_MSG}, diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx index 886e740d564470..ed8e238db75e75 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx @@ -26,7 +26,14 @@ jest.mock('react-router-dom', () => { }), }; }); - +const defaultProps = { + allCasesNavigation: { + href: 'all-cases-href', + onClick: () => {}, + }, + caseData: basicCase, + currentExternalIncident: null, +}; describe('CaseView actions', () => { const handleOnDeleteConfirm = jest.fn(); const handleToggleModal = jest.fn(); @@ -49,7 +56,7 @@ describe('CaseView actions', () => { it('clicking trash toggles modal', () => { const wrapper = mount( - + ); @@ -67,7 +74,7 @@ describe('CaseView actions', () => { })); const wrapper = mount( - + ); @@ -82,7 +89,7 @@ describe('CaseView actions', () => { const wrapper = mount( = ({ + allCasesNavigation, caseData, currentExternalIncident, disabled = false, }) => { - const history = useHistory(); // Delete case const { handleToggleModal, @@ -57,7 +58,7 @@ const ActionsComponent: React.FC = ({ ); if (isDeleted) { - history.push('/'); + allCasesNavigation.onClick(null); return null; } return ( diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 0d29335ea730e7..724d35b20df535 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -16,7 +16,12 @@ describe('CaseActionBar', () => { const onRefresh = jest.fn(); const onUpdateField = jest.fn(); const defaultProps = { + allCasesNavigation: { + href: 'all-cases-href', + onClick: () => {}, + }, caseData: basicCase, + disableAlerting: false, isLoading: false, onRefresh, onUpdateField, diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index a68ae4b3ca6a74..d8e012b0721065 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -16,16 +16,16 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import { CaseStatuses, CaseType } from '../../../common'; +import { Case, CaseStatuses, CaseType } from '../../../common'; import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../formatted_date'; import { Actions } from './actions'; -import { Case } from '../../containers/types'; import { CaseService } from '../../containers/use_get_case_user_actions'; import { StatusContextMenu } from './status_context_menu'; import { getStatusDate, getStatusTitle } from './helpers'; import { SyncAlertsSwitch } from '../case_settings/sync_alerts_switch'; import { OnUpdateFields } from '../case_view'; +import { CasesNavigation } from '../links'; const MyDescriptionList = styled(EuiDescriptionList)` ${({ theme }) => css` @@ -37,17 +37,21 @@ const MyDescriptionList = styled(EuiDescriptionList)` `; interface CaseActionBarProps { + allCasesNavigation: CasesNavigation; caseData: Case; currentExternalIncident: CaseService | null; disabled?: boolean; + disableAlerting: boolean; isLoading: boolean; onRefresh: () => void; onUpdateField: (args: OnUpdateFields) => void; } const CaseActionBarComponent: React.FC = ({ + allCasesNavigation, caseData, currentExternalIncident, disabled = false, + disableAlerting, isLoading, onRefresh, onUpdateField, @@ -104,25 +108,27 @@ const CaseActionBarComponent: React.FC = ({ - - - - - {i18n.SYNC_ALERTS} - - - - - - - - - - + {!disableAlerting && ( + + + + + {i18n.SYNC_ALERTS} + + + + + + + + + + + )} {i18n.CASE_REFRESH} @@ -130,6 +136,7 @@ const CaseActionBarComponent: React.FC = ({ ( + {i18n.DOES_NOT_EXIST_TITLE}

} + titleSize="xs" + body={

{i18n.DOES_NOT_EXIST_DESCRIPTION(caseId)}

} + actions={ + + {i18n.DOES_NOT_EXIST_BUTTON} + + } + /> +); diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 86b13ae5a863c4..df57e49073a604 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -// import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import { @@ -17,7 +16,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector } from '../../../common'; +import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector, Ecs } from '../../../common'; import { HeaderPage } from '../header_page'; import { EditableTitle } from '../header_page/editable_title'; import { TagList } from '../tag_list'; @@ -39,11 +38,11 @@ import { } from '../configure_cases/utils'; import { StatusActionButton } from '../status/button'; import * as i18n from './translations'; -import { Ecs } from '../../../common'; import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; +import { DoesNotExist } from './does_not_exist'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file export interface CaseViewComponentProps { @@ -53,8 +52,8 @@ export interface CaseViewComponentProps { configureCasesNavigation: CasesNavigation; getCaseDetailHrefWithCommentId: (commentId: string) => string; onComponentInitialized?: () => void; - ruleDetailsNavigation: CasesNavigation; - showAlertDetails: (alertId: string, index: string) => void; + ruleDetailsNavigation?: CasesNavigation; + showAlertDetails?: (alertId: string, index: string) => void; subCaseId?: string; useFetchAlertData: (alertIds: string[]) => [boolean, Record]; userCanCrud: boolean; @@ -327,7 +326,9 @@ export const CaseComponent = React.memo( const onShowAlertDetails = useCallback( (alertId: string, index: string) => { - showAlertDetails(alertId, index); + if (showAlertDetails) { + showAlertDetails(alertId, index); + } }, [showAlertDetails] ); @@ -359,9 +360,11 @@ export const CaseComponent = React.memo( title={caseData.title} > ( <> { const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); if (isError) { - return null; + return ; } if (isLoading) { return ( diff --git a/x-pack/plugins/cases/public/components/case_view/translations.ts b/x-pack/plugins/cases/public/components/case_view/translations.ts index 41ffbbd9342dad..3d4558ac3d4a00 100644 --- a/x-pack/plugins/cases/public/components/case_view/translations.ts +++ b/x-pack/plugins/cases/public/components/case_view/translations.ts @@ -128,3 +128,20 @@ export const CHANGED_CONNECTOR_FIELD = i18n.translate('xpack.cases.caseView.fiel export const SYNC_ALERTS = i18n.translate('xpack.cases.caseView.syncAlertsLabel', { defaultMessage: `Sync alerts`, }); + +export const DOES_NOT_EXIST_TITLE = i18n.translate('xpack.cases.caseView.doesNotExist.title', { + defaultMessage: 'This case does not exist', +}); + +export const DOES_NOT_EXIST_DESCRIPTION = (caseId: string) => + i18n.translate('xpack.cases.caseView.doesNotExist.description', { + values: { + caseId, + }, + defaultMessage: + 'A case with id {caseId} could not be found. This likely means the case has been deleted, or the id is incorrect.', + }); + +export const DOES_NOT_EXIST_BUTTON = i18n.translate('xpack.cases.caseView.doesNotExist.button', { + defaultMessage: 'Back to Cases', +}); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 83f759947ba65b..cbd4fd7654259d 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -38,6 +38,7 @@ const MySpinner = styled(EuiLoadingSpinner)` interface Props { connectors?: ActionConnector[]; + disableAlerts?: boolean; hideConnectorServiceNowSir?: boolean; isLoadingConnectors?: boolean; withSteps?: boolean; @@ -46,6 +47,7 @@ const empty: ActionConnector[] = []; export const CreateCaseForm: React.FC = React.memo( ({ connectors = empty, + disableAlerts = false, isLoadingConnectors = false, hideConnectorServiceNowSir = false, withSteps = true, @@ -99,11 +101,10 @@ export const CreateCaseForm: React.FC = React.memo( [connectors, hideConnectorServiceNowSir, isLoadingConnectors, isSubmitting] ); - const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ - firstStep, - secondStep, - thirdStep, - ]); + const allSteps = useMemo( + () => [firstStep, ...(!disableAlerts ? [secondStep] : []), thirdStep], + [disableAlerts, firstStep, secondStep, thirdStep] + ); return ( <> @@ -117,7 +118,7 @@ export const CreateCaseForm: React.FC = React.memo( ) : ( <> {firstStep.children} - {secondStep.children} + {!disableAlerts && secondStep.children} {thirdStep.children} )} diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 8584892e1286c0..30a60fb5c1e47f 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -73,7 +73,7 @@ export const FormContext: React.FC = ({ const submitCase = useCallback( async ( - { connectorId: dataConnectorId, fields, syncAlerts, ...dataWithoutConnectorId }, + { connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId }, isValid ) => { if (isValid) { diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index 3362aa6af2078d..139a2103f6042b 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -34,6 +34,7 @@ const Container = styled.div` export interface CreateCaseProps extends Owner { afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise; caseType?: CaseType; + disableAlerts?: boolean; hideConnectorServiceNowSir?: boolean; onCancel: () => void; onSuccess: (theCase: Case) => Promise; @@ -45,6 +46,7 @@ const CreateCaseComponent = ({ afterCaseCreated, caseType, hideConnectorServiceNowSir, + disableAlerts, onCancel, onSuccess, timelineIntegration, @@ -59,6 +61,7 @@ const CreateCaseComponent = ({ > diff --git a/x-pack/plugins/cases/public/components/links/index.tsx b/x-pack/plugins/cases/public/components/links/index.tsx index 310d700aa2a250..23eedc7c090bc6 100644 --- a/x-pack/plugins/cases/public/components/links/index.tsx +++ b/x-pack/plugins/cases/public/components/links/index.tsx @@ -16,7 +16,7 @@ import { import React, { useCallback } from 'react'; import * as i18n from './translations'; -export interface CasesNavigation { +export interface CasesNavigation { href: K extends 'configurable' ? (arg: T) => string : string; onClick: (arg: T) => void; } diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx index a4ce8e3d925227..1b8e01b15db8dc 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx @@ -38,7 +38,7 @@ export interface ReturnUsePushToService { } export const usePushToService = ({ - configureCasesNavigation: { onClick, href }, + configureCasesNavigation: { href }, connector, caseId, caseServices, @@ -82,7 +82,7 @@ export const usePushToService = ({ id="xpack.cases.caseView.pushToServiceDisableByNoConnectors" values={{ link: ( - + {i18n.LINK_CONNECTOR_CONFIGURE} ), diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 09b024fb2ca3d1..156e011a18d8d7 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -21,7 +21,7 @@ import { isRight } from 'fp-ts/Either'; import * as i18n from './translations'; -import { Case, CaseUserActions } from '../../containers/types'; +import { Case, CaseUserActions } from '../../../common'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; @@ -56,7 +56,7 @@ export interface UserActionTreeProps { caseUserActions: CaseUserActions[]; connectors: ActionConnector[]; data: Case; - getRuleDetailsHref: (ruleId: string | null | undefined) => string; + getRuleDetailsHref?: (ruleId: string | null | undefined) => string; fetchUserActions: () => void; isLoadingDescription: boolean; isLoadingUserActions: boolean; @@ -397,18 +397,22 @@ export const UserActionTree = React.memo( return [ ...comments, - getAlertAttachment({ - action, - alertId, - getCaseDetailHrefWithCommentId, - getRuleDetailsHref, - index: alertIndex, - loadingAlertData, - onRuleDetailsClick, - ruleId, - ruleName, - onShowAlertDetails, - }), + ...(getRuleDetailsHref != null + ? [ + getAlertAttachment({ + action, + alertId, + getCaseDetailHrefWithCommentId, + getRuleDetailsHref, + index: alertIndex, + loadingAlertData, + onRuleDetailsClick, + ruleId, + ruleName, + onShowAlertDetails, + }), + ] + : []), ]; } else if (comment != null && comment.type === CommentType.generatedAlert) { // TODO: clean this up @@ -422,16 +426,20 @@ export const UserActionTree = React.memo( return [ ...comments, - getGeneratedAlertsAttachment({ - action, - alertIds, - getCaseDetailHrefWithCommentId, - getRuleDetailsHref, - onRuleDetailsClick, - renderInvestigateInTimelineActionComponent, - ruleId: comment.rule?.id ?? '', - ruleName: comment.rule?.name ?? i18n.UNKNOWN_RULE, - }), + ...(getRuleDetailsHref != null + ? [ + getGeneratedAlertsAttachment({ + action, + alertIds, + getCaseDetailHrefWithCommentId, + getRuleDetailsHref, + onRuleDetailsClick, + renderInvestigateInTimelineActionComponent, + ruleId: comment.rule?.id ?? '', + ruleName: comment.rule?.name ?? i18n.UNKNOWN_RULE, + }), + ] + : []), ]; } } diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index afd6b51b5f35d0..f9e128e7f713d7 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -161,7 +161,7 @@ describe('Case Configuration API', () => { query: { ...DEFAULT_QUERY_PARAMS, reporters, - tags: ['"coke"', '"pepsi"'], + tags: ['coke', 'pepsi'], search: 'hello', status: CaseStatuses.open, owner: [SECURITY_SOLUTION_OWNER], @@ -190,7 +190,7 @@ describe('Case Configuration API', () => { query: { ...DEFAULT_QUERY_PARAMS, reporters, - tags: ['"("', '"\\"double\\""'], + tags: ['(', '"double"'], search: 'hello', status: CaseStatuses.open, owner: [SECURITY_SOLUTION_OWNER], diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 66a4d174b0ffb8..fc1dc34b4e1aca 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -189,7 +189,7 @@ export const getCases = async ({ }: FetchCasesProps): Promise => { const query = { reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), - tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`), + tags: filterOptions.tags, status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}), diff --git a/x-pack/plugins/observability/common/const.ts b/x-pack/plugins/observability/common/const.ts new file mode 100644 index 00000000000000..7065d8ccc6b344 --- /dev/null +++ b/x-pack/plugins/observability/common/const.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const CASES_APP_ID = 'observabilityCases'; +export const OBSERVABILITY = 'observability'; diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 52d5493ae69a49..d13140f0be16ce 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -7,14 +7,16 @@ "observability" ], "optionalPlugins": [ - "licensing", "home", - "usageCollection", - "lens" + "lens", + "licensing", + "usageCollection" ], "requiredPlugins": [ - "data", "alerting", + "cases", + "data", + "features", "ruleRegistry", "triggersActionsUi" ], diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx new file mode 100644 index 00000000000000..1636d08aa56e4d --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + getCaseDetailsUrl, + getConfigureCasesUrl, + getCreateCaseUrl, + useFormatUrl, +} from '../../../../pages/cases/links'; +import { useKibana } from '../../../../utils/kibana_react'; +import { CASES_APP_ID, CASES_OWNER } from '../constants'; + +export interface AllCasesNavProps { + detailName: string; + search?: string; + subCaseId?: string; +} + +interface AllCasesProps { + userCanCrud: boolean; +} +export const AllCases = React.memo(({ userCanCrud }) => { + const { + cases: casesUi, + application: { navigateToApp }, + } = useKibana().services; + const { formatUrl } = useFormatUrl(CASES_APP_ID); + + return casesUi.getAllCases({ + caseDetailsNavigation: { + href: ({ detailName, subCaseId }: AllCasesNavProps) => { + return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); + }, + onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) => + navigateToApp(`${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id: detailName, subCaseId }), + }), + }, + configureCasesNavigation: { + href: formatUrl(getConfigureCasesUrl()), + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToApp(`${CASES_APP_ID}`, { + path: getConfigureCasesUrl(), + }); + }, + }, + createCaseNavigation: { + href: formatUrl(getCreateCaseUrl()), + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToApp(`${CASES_APP_ID}`, { + path: getCreateCaseUrl(), + }); + }, + }, + disableAlerts: true, + showTitle: false, + userCanCrud, + owner: [CASES_OWNER], + }); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx new file mode 100644 index 00000000000000..b0b6fc0e3b7933 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { CallOut, CallOutProps } from './callout'; + +describe('Callout', () => { + const defaultProps: CallOutProps = { + id: 'md5-hex', + type: 'primary', + title: 'a tittle', + messages: [ + { + id: 'generic-error', + title: 'message-one', + description:

{'error'}

, + }, + ], + showCallOut: true, + handleDismissCallout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the callout', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); + }); + + it('hides the callout', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeFalsy(); + }); + + it('does not show any messages when the list is empty', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); + }); + + it('transform the button color correctly - primary', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--primary')).toBeTruthy(); + }); + + it('transform the button color correctly - success', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--secondary')).toBeTruthy(); + }); + + it('transform the button color correctly - warning', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--warning')).toBeTruthy(); + }); + + it('transform the button color correctly - danger', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--danger')).toBeTruthy(); + }); + + it('dismiss the callout correctly', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); + wrapper.update(); + + expect(defaultProps.handleDismissCallout).toHaveBeenCalledWith('md5-hex', 'primary'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx new file mode 100644 index 00000000000000..4cb3875f75acbb --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCallOut, EuiButton, EuiDescriptionList } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback } from 'react'; + +import { ErrorMessage } from './types'; +import * as i18n from './translations'; + +export interface CallOutProps { + id: string; + type: NonNullable; + title: string; + messages: ErrorMessage[]; + showCallOut: boolean; + handleDismissCallout: (id: string, type: NonNullable) => void; +} + +function CallOutComponent({ + id, + type, + title, + messages, + showCallOut, + handleDismissCallout, +}: CallOutProps) { + const handleCallOut = useCallback(() => handleDismissCallout(id, type), [ + handleDismissCallout, + id, + type, + ]); + + return showCallOut && !isEmpty(messages) ? ( + + + + {i18n.DISMISS_CALLOUT} + + + ) : null; +} + +export const CallOut = memo(CallOutComponent); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx new file mode 100644 index 00000000000000..b5b92a33748742 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.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 md5 from 'md5'; +import { createCalloutId } from './helpers'; + +describe('createCalloutId', () => { + it('creates id correctly with one id', () => { + const digest = md5('one'); + const id = createCalloutId(['one']); + expect(id).toBe(digest); + }); + + it('creates id correctly with multiples ids', () => { + const digest = md5('one|two|three'); + const id = createCalloutId(['one', 'two', 'three']); + expect(id).toBe(digest); + }); + + it('creates id correctly with multiples ids and delimiter', () => { + const digest = md5('one,two,three'); + const id = createCalloutId(['one', 'two', 'three'], ','); + expect(id).toBe(digest); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx new file mode 100644 index 00000000000000..29b17cd426c58b --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import md5 from 'md5'; + +import * as i18n from './translations'; +import { ErrorMessage } from './types'; + +export const permissionsReadOnlyErrorMessage: ErrorMessage = { + id: 'read-only-privileges-error', + title: i18n.READ_ONLY_FEATURE_TITLE, + description: <>{i18n.READ_ONLY_FEATURE_MSG}, + errorType: 'warning', +}; + +export const createCalloutId = (ids: string[], delimiter: string = '|'): string => + md5(ids.join(delimiter)); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx new file mode 100644 index 00000000000000..e7ed339d99e907 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import { useMessagesStorage } from '../../../../hooks/use_messages_storage'; +import { createCalloutId } from './helpers'; +import { CaseCallOut, CaseCallOutProps } from '.'; + +jest.mock('../../../../hooks/use_messages_storage'); +const useSecurityLocalStorageMock = useMessagesStorage as jest.Mock; +const securityLocalStorageMock = { + getMessages: jest.fn(() => []), + addMessage: jest.fn(), +}; + +describe('CaseCallOut ', () => { + beforeEach(() => { + jest.clearAllMocks(); + useSecurityLocalStorageMock.mockImplementation(() => securityLocalStorageMock); + }); + + it('renders a callout correctly', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + { id: 'message-two', title: 'title', description:

{'for real'}

}, + ], + }; + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one', 'message-two']); + expect(wrapper.find(`[data-test-subj="callout-messages-${id}"]`).last().exists()).toBeTruthy(); + }); + + it('groups the messages correctly', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'danger', + }, + { id: 'message-two', title: 'title two', description:

{'for real'}

}, + ], + }; + + const wrapper = mount( + + + + ); + + const idDanger = createCalloutId(['message-one']); + const idPrimary = createCalloutId(['message-two']); + + expect( + wrapper.find(`[data-test-subj="case-callout-${idPrimary}"]`).last().exists() + ).toBeTruthy(); + expect( + wrapper.find(`[data-test-subj="case-callout-${idDanger}"]`).last().exists() + ).toBeTruthy(); + }); + + it('dismisses the callout correctly', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + ], + }; + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + + expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeTruthy(); + wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); + expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).exists()).toBeFalsy(); + }); + + it('persist the callout of type primary when dismissed', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith('observability'); + wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); + expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith('observability', id); + }); + + it('do not show the callout if is in the localStorage', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + ], + }; + + const id = createCalloutId(['message-one']); + + useSecurityLocalStorageMock.mockImplementation(() => ({ + ...securityLocalStorageMock, + getMessages: jest.fn(() => [id]), + })); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeFalsy(); + }); + + it('do not persist a callout of type danger', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'danger', + }, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.update(); + expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); + }); + + it('do not persist a callout of type warning', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'warning', + }, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.update(); + expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); + }); + + it('do not persist a callout of type success', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'success', + }, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.update(); + expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx new file mode 100644 index 00000000000000..43cb6fd352a53e --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 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 { EuiSpacer } from '@elastic/eui'; +import React, { memo, useCallback, useState, useMemo } from 'react'; + +import { CallOut } from './callout'; +import { ErrorMessage } from './types'; +import { createCalloutId } from './helpers'; +import { useMessagesStorage } from '../../../../hooks/use_messages_storage'; +import { OBSERVABILITY } from '../../../../../common/const'; + +export * from './helpers'; + +export interface CaseCallOutProps { + title: string; + messages?: ErrorMessage[]; +} + +type GroupByTypeMessages = { + [key in NonNullable]: { + messagesId: string[]; + messages: ErrorMessage[]; + }; +}; + +interface CalloutVisibility { + [index: string]: boolean; +} + +function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { + const { getMessages, addMessage } = useMessagesStorage(); + + const caseMessages = useMemo(() => getMessages(OBSERVABILITY), [getMessages]); + const dismissedCallouts = useMemo( + () => + caseMessages.reduce( + (acc: CalloutVisibility, id) => ({ + ...acc, + [id]: false, + }), + {} + ), + [caseMessages] + ); + + const [calloutVisibility, setCalloutVisibility] = useState(dismissedCallouts); + const handleCallOut = useCallback( + (id, type) => { + setCalloutVisibility((prevState) => ({ ...prevState, [id]: false })); + if (type === 'primary') { + addMessage(OBSERVABILITY, id); + } + }, + [setCalloutVisibility, addMessage] + ); + + const groupedByTypeErrorMessages = useMemo( + () => + messages.reduce( + (acc: GroupByTypeMessages, currentMessage: ErrorMessage) => { + const type = currentMessage.errorType == null ? 'primary' : currentMessage.errorType; + return { + ...acc, + [type]: { + messagesId: [...(acc[type]?.messagesId ?? []), currentMessage.id], + messages: [...(acc[type]?.messages ?? []), currentMessage], + }, + }; + }, + {} as GroupByTypeMessages + ), + [messages] + ); + + return ( + <> + {(Object.keys(groupedByTypeErrorMessages) as Array).map( + (type: NonNullable) => { + const id = createCalloutId(groupedByTypeErrorMessages[type].messagesId); + return ( + + + + + ); + } + )} + + ); +} + +export const CaseCallOut = memo(CaseCallOutComponent); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts new file mode 100644 index 00000000000000..cb7236b445be12 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.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 { i18n } from '@kbn/i18n'; + +export const READ_ONLY_FEATURE_TITLE = i18n.translate( + 'xpack.observability.cases.readOnlyFeatureTitle', + { + defaultMessage: 'You cannot open new or update existing cases', + } +); + +export const READ_ONLY_FEATURE_MSG = i18n.translate( + 'xpack.observability.cases.readOnlyFeatureDescription', + { + defaultMessage: + 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.observability.cases.dismissErrorsPushServiceCallOutTitle', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/types.ts b/x-pack/plugins/observability/public/components/app/cases/callout/types.ts new file mode 100644 index 00000000000000..84d79ee391b8ff --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ErrorMessage { + id: string; + title: string; + description: JSX.Element; + errorType?: 'primary' | 'success' | 'warning' | 'danger'; +} diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts new file mode 100644 index 00000000000000..b180c15b4487ab --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.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. + */ + +import { Ecs } from '../../../../../../cases/common'; + +// no alerts in observability so far +// dummy hook for now as hooks cannot be called conditionally +export const useFetchAlertData = (): [boolean, Record] => [false, {}]; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx new file mode 100644 index 00000000000000..3267f7bb17cce6 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import { + getCaseDetailsUrl, + getCaseDetailsUrlWithCommentId, + getCaseUrl, + getConfigureCasesUrl, + useFormatUrl, +} from '../../../../pages/cases/links'; +import { Case } from '../../../../../../cases/common'; +import { useFetchAlertData } from './helpers'; +import { useKibana } from '../../../../utils/kibana_react'; +import { CASES_APP_ID } from '../constants'; +import { casesBreadcrumbs, useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; + +interface Props { + caseId: string; + subCaseId?: string; + userCanCrud: boolean; +} + +export interface OnUpdateFields { + key: keyof Case; + value: Case[keyof Case]; + onSuccess?: () => void; + onError?: () => void; +} + +export interface CaseProps extends Props { + fetchCase: () => void; + caseData: Case; + updateCase: (newCase: Case) => void; +} + +export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { + const [caseTitle, setCaseTitle] = useState(null); + + const { cases: casesUi, application } = useKibana().services; + const { navigateToApp } = application; + const allCasesLink = getCaseUrl(); + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const href = formatUrl(allCasesLink); + useBreadcrumbs([ + { ...casesBreadcrumbs.cases, href }, + ...(caseTitle !== null + ? [ + { + text: caseTitle, + }, + ] + : []), + ]); + + const onCaseDataSuccess = useCallback( + (data: Case) => { + if (caseTitle === null) { + setCaseTitle(data.title); + } + }, + [caseTitle] + ); + + const configureCasesLink = getConfigureCasesUrl(); + const allCasesHref = href; + const configureCasesHref = formatUrl(configureCasesLink); + const caseDetailsHref = formatUrl(getCaseDetailsUrl({ id: caseId }), { absolute: true }); + const getCaseDetailHrefWithCommentId = useCallback( + (commentId: string) => + formatUrl(getCaseDetailsUrlWithCommentId({ id: caseId, commentId, subCaseId }), { + absolute: true, + }), + [caseId, formatUrl, subCaseId] + ); + + return casesUi.getCaseView({ + allCasesNavigation: { + href: allCasesHref, + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToApp(`${CASES_APP_ID}`, { + path: allCasesLink, + }); + }, + }, + caseDetailsNavigation: { + href: caseDetailsHref, + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToApp(`${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id: caseId }), + }); + }, + }, + caseId, + configureCasesNavigation: { + href: configureCasesHref, + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToApp(`${CASES_APP_ID}`, { + path: configureCasesLink, + }); + }, + }, + getCaseDetailHrefWithCommentId, + onCaseDataSuccess, + subCaseId, + useFetchAlertData, + userCanCrud, + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/constants.ts b/x-pack/plugins/observability/public/components/app/cases/constants.ts new file mode 100644 index 00000000000000..3c1f868fec084f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/constants.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CASES_APP_ID } from '../../../../common/const'; + +export { CASES_APP_ID }; +export const CASES_OWNER = 'observability'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx new file mode 100644 index 00000000000000..c3580671237471 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; + +import { CreateCaseFlyout } from './flyout'; + +jest.mock('../../../../utils/kibana_react', () => ({ + useKibana: () => ({ + services: { + cases: { + getCreateCase: jest.fn(), + }, + }, + }), +})); +const onCloseFlyout = jest.fn(); +const onSuccess = jest.fn(); +const defaultProps = { + onCloseFlyout, + onSuccess, +}; + +describe('CreateCaseFlyout', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); + }); + + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + + + + ); + + wrapper.find('.euiFlyout__closeButton').first().simulate('click'); + expect(onCloseFlyout).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx new file mode 100644 index 00000000000000..df29d02e8d830e --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; + +import * as i18n from '../translations'; +import { Case } from '../../../../../../cases/common'; +import { CASES_OWNER } from '../constants'; +import { useKibana } from '../../../../utils/kibana_react'; + +export interface CreateCaseModalProps { + afterCaseCreated?: (theCase: Case) => Promise; + onCloseFlyout: () => void; + onSuccess: (theCase: Case) => Promise; +} + +const StyledFlyout = styled(EuiFlyout)` + ${({ theme }) => ` + z-index: ${theme.eui.euiZModal}; + `} +`; +// Adding bottom padding because timeline's +// bottom bar gonna hide the submit button. +// might not need for obs, test this when implementing this component +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + ${({ theme }) => ` + && .euiFlyoutBody__overflow { + overflow-y: auto; + overflow-x: hidden; + } + + && .euiFlyoutBody__overflowContent { + display: block; + padding: ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 70px; + height: auto; + } + `} +`; + +const FormWrapper = styled.div` + width: 100%; +`; + +function CreateCaseFlyoutComponent({ + afterCaseCreated, + onCloseFlyout, + onSuccess, +}: CreateCaseModalProps) { + const { cases } = useKibana().services; + return ( + + + +

{i18n.CREATE_TITLE}

+
+
+ + + {cases.getCreateCase({ + afterCaseCreated, + onCancel: onCloseFlyout, + onSuccess, + withSteps: false, + owner: [CASES_OWNER], + })} + + +
+ ); +} +// not yet used +// committing for use with alerting #RAC +export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent); + +CreateCaseFlyout.displayName = 'CreateCaseFlyout'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx new file mode 100644 index 00000000000000..ec7511836328b0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { waitFor } from '@testing-library/react'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import { Create } from '.'; +import { useKibana } from '../../../../utils/kibana_react'; +import { basicCase } from '../../../../../../cases/public/containers/mock'; +import { CASES_APP_ID, CASES_OWNER } from '../constants'; +import { Case } from '../../../../../../cases/common'; +import { getCaseDetailsUrl } from '../../../../pages/cases/links'; + +jest.mock('../../../../utils/kibana_react'); + +describe('Create case', () => { + const mockCreateCase = jest.fn(); + const mockNavigateToApp = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: mockCreateCase, + }, + application: { navigateToApp: mockNavigateToApp }, + }, + }); + }); + + it('it renders', () => { + mount( + + + + ); + + expect(mockCreateCase).toHaveBeenCalled(); + expect(mockCreateCase.mock.calls[0][0].owner).toEqual([CASES_OWNER]); + }); + + it('should redirect to all cases on cancel click', async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: ({ onCancel }: { onCancel: () => Promise }) => { + onCancel(); + }, + }, + application: { navigateToApp: mockNavigateToApp }, + }, + }); + mount( + + + + ); + + await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`)); + }); + + it('should redirect to new case when posting the case', async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: ({ onSuccess }: { onSuccess: (theCase: Case) => Promise }) => { + onSuccess(basicCase); + }, + }, + application: { navigateToApp: mockNavigateToApp }, + }, + }); + mount( + + + + ); + + await waitFor(() => + expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id: basicCase.id }), + }) + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx new file mode 100644 index 00000000000000..d7e2daea2490b4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { useKibana } from '../../../../utils/kibana_react'; +import { getCaseDetailsUrl } from '../../../../pages/cases/links'; +import { CASES_APP_ID, CASES_OWNER } from '../constants'; + +export const Create = React.memo(() => { + const { + cases, + application: { navigateToApp }, + } = useKibana().services; + const onSuccess = useCallback( + async ({ id }) => + navigateToApp(`${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id }), + }), + [navigateToApp] + ); + + const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]); + + return ( + + {cases.getCreateCase({ + disableAlerts: true, + onCancel: handleSetIsCancel, + onSuccess, + owner: [CASES_OWNER], + })} + + ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts new file mode 100644 index 00000000000000..1a5abe218edf52 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const CASES_FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.observability.cases.caseFeatureNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const CASES_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.observability.cases.caseFeatureNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Cases feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); +export const BACK_TO_ALL = i18n.translate('xpack.observability.cases.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.observability.cases.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_CASE = i18n.translate( + 'xpack.observability.cases.confirmDeleteCase.deleteCase', + { + defaultMessage: 'Delete case', + } +); + +export const DELETE_CASES = i18n.translate( + 'xpack.observability.cases.confirmDeleteCase.deleteCases', + { + defaultMessage: 'Delete cases', + } +); + +export const NAME = i18n.translate('xpack.observability.cases.caseView.name', { + defaultMessage: 'Name', +}); + +export const REPORTER = i18n.translate('xpack.observability.cases.caseView.reporterLabel', { + defaultMessage: 'Reporter', +}); + +export const PARTICIPANTS = i18n.translate('xpack.observability.cases.caseView.particpantsLabel', { + defaultMessage: 'Participants', +}); + +export const CREATE_TITLE = i18n.translate('xpack.observability.cases.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.observability.cases.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.observability.cases.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.observability.cases.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + +export const REQUIRED_FIELD = i18n.translate( + 'xpack.observability.cases.caseView.fieldRequiredError', + { + defaultMessage: 'Required field', + } +); + +export const EDIT = i18n.translate('xpack.observability.cases.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.observability.cases.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const PAGE_TITLE = i18n.translate('xpack.observability.cases.pageTitle', { + defaultMessage: 'Cases', +}); + +export const CREATE_CASE = i18n.translate('xpack.observability.cases.caseView.createCase', { + defaultMessage: 'Create case', +}); + +export const CLOSE_CASE = i18n.translate('xpack.observability.cases.caseView.closeCase', { + defaultMessage: 'Close case', +}); + +export const REOPEN_CASE = i18n.translate('xpack.observability.cases.caseView.reopenCase', { + defaultMessage: 'Reopen case', +}); + +export const CASE_NAME = i18n.translate('xpack.observability.cases.caseView.caseName', { + defaultMessage: 'Case name', +}); + +export const TO = i18n.translate('xpack.observability.cases.caseView.to', { + defaultMessage: 'to', +}); + +export const TAGS = i18n.translate('xpack.observability.cases.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const ACTIONS = i18n.translate('xpack.observability.cases.allCases.actions', { + defaultMessage: 'Actions', +}); + +export const NO_TAGS_AVAILABLE = i18n.translate( + 'xpack.observability.cases.allCases.noTagsAvailable', + { + defaultMessage: 'No tags available', + } +); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.observability.cases.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + +export const COMMENTS = i18n.translate('xpack.observability.cases.allCases.comments', { + defaultMessage: 'Comments', +}); + +export const TAGS_HELP = i18n.translate('xpack.observability.cases.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const NO_TAGS = i18n.translate('xpack.observability.cases.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.observability.cases.createCase.titleFieldRequiredError', + { + defaultMessage: 'A title is required.', + } +); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.observability.cases.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate( + 'xpack.observability.cases.configureCasesButton', + { + defaultMessage: 'Edit external connection', + } +); + +export const ADD_COMMENT = i18n.translate('xpack.observability.cases.caseView.comment.addComment', { + defaultMessage: 'Add comment', +}); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.observability.cases.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.observability.cases.caseView.description.save', { + defaultMessage: 'Save', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.observability.cases.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const CONNECTORS = i18n.translate('xpack.observability.cases.caseView.connectors', { + defaultMessage: 'External Incident Management System', +}); + +export const EDIT_CONNECTOR = i18n.translate('xpack.observability.cases.caseView.editConnector', { + defaultMessage: 'Change external incident management system', +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx b/x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx new file mode 100644 index 00000000000000..477fb77d98ee80 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const WhitePageWrapper = styled.div` + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + flex: 1 1 auto; +`; + +export const SectionWrapper = styled.div` + box-sizing: content-box; + margin: 0 auto; + max-width: 1175px; + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts index d31b6b52744c0c..090031e314fd1a 100644 --- a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -9,8 +9,8 @@ import { ChromeBreadcrumb } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { MouseEvent, useEffect } from 'react'; import { EuiBreadcrumb } from '@elastic/eui'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { useQueryParams } from './use_query_params'; +import { useKibana } from '../utils/kibana_react'; function handleBreadcrumbClick( breadcrumbs: ChromeBreadcrumb[], @@ -39,17 +39,35 @@ export const makeBaseBreadcrumb = (href: string): EuiBreadcrumb => { href, }; }; - +export const casesBreadcrumbs = { + cases: { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases', { + defaultMessage: 'Cases', + }), + }, + create: { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.create', { + defaultMessage: 'Create', + }), + }, + configure: { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.configure', { + defaultMessage: 'Configure', + }), + }, +}; export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { const params = useQueryParams(); const { - services: { chrome, application }, + services: { + chrome: { setBreadcrumbs }, + application: { getUrlForApp, navigateToUrl }, + }, } = useKibana(); - const setBreadcrumbs = chrome?.setBreadcrumbs; - const appPath = application?.getUrlForApp('observability-overview') ?? ''; - const navigate = application?.navigateToUrl; + const appPath = getUrlForApp('observability-overview') ?? ''; + const navigate = navigateToUrl; useEffect(() => { if (setBreadcrumbs) { diff --git a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx new file mode 100644 index 00000000000000..9f4ed59a45f2b6 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { useKibana } from '../utils/kibana_react'; +import { CASES_APP_ID } from '../../common/const'; + +export interface UseGetUserCasesPermissions { + crud: boolean; + read: boolean; +} + +export function useGetUserCasesPermissions() { + const [casesPermissions, setCasesPermissions] = useState(null); + const uiCapabilities = useKibana().services.application.capabilities; + + useEffect(() => { + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities[CASES_APP_ID].crud_cases === 'boolean' + ? (uiCapabilities[CASES_APP_ID].crud_cases as boolean) + : false; + const capabilitiesCanUserRead: boolean = + typeof uiCapabilities[CASES_APP_ID].read_cases === 'boolean' + ? (uiCapabilities[CASES_APP_ID].read_cases as boolean) + : false; + setCasesPermissions({ + crud: capabilitiesCanUserCRUD, + read: capabilitiesCanUserRead, + }); + }, [uiCapabilities]); + + return casesPermissions; +} diff --git a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx new file mode 100644 index 00000000000000..d67910f00dc769 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useKibana } from '../utils/kibana_react'; + +export interface UseMessagesStorage { + getMessages: (plugin: string) => string[]; + addMessage: (plugin: string, id: string) => void; + removeMessage: (plugin: string, id: string) => void; + clearAllMessages: (plugin: string) => void; + hasMessage: (plugin: string, id: string) => boolean; +} + +export const useMessagesStorage = (): UseMessagesStorage => { + const { storage } = useKibana().services; + + const getMessages = useCallback( + (plugin: string): string[] => storage.get(`${plugin}-messages`) ?? [], + [storage] + ); + + const addMessage = useCallback( + (plugin: string, id: string) => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + storage.set(`${plugin}-messages`, [...pluginStorage, id]); + }, + [storage] + ); + + const hasMessage = useCallback( + (plugin: string, id: string): boolean => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + return pluginStorage.includes((val: string) => val === id); + }, + [storage] + ); + + const removeMessage = useCallback( + (plugin: string, id: string) => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + storage.set( + `${plugin}-messages`, + pluginStorage.filter((val: string) => val !== id) + ); + }, + [storage] + ); + + const clearAllMessages = useCallback( + (plugin: string): string[] => storage.remove(`${plugin}-messages`), + [storage] + ); + + return { + getMessages, + addMessage, + clearAllMessages, + removeMessage, + hasMessage, + }; +}; diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx new file mode 100644 index 00000000000000..4131cdc40738f2 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { AllCases } from '../../components/app/cases/all_cases'; +import * as i18n from '../../components/app/cases/translations'; + +import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; +import { CaseFeatureNoPermissions } from './feature_no_permissions'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; +import { usePluginContext } from '../../hooks/use_plugin_context'; + +export const AllCasesPage = React.memo(() => { + const userPermissions = useGetUserCasesPermissions(); + const { ObservabilityPageTemplate } = usePluginContext(); + return userPermissions == null || userPermissions?.read ? ( + <> + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + {i18n.PAGE_TITLE}, + }} + > + + + + ) : ( + + ); +}); + +AllCasesPage.displayName = 'AllCasesPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx new file mode 100644 index 00000000000000..78f1cb313ea9b4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { CaseView } from '../../components/app/cases/case_view'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; +import { useKibana } from '../../utils/kibana_react'; +import { CASES_APP_ID } from '../../components/app/cases/constants'; +import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/app/cases/callout'; + +export const CaseDetailsPage = React.memo(() => { + const { + application: { navigateToApp }, + } = useKibana().services; + const userPermissions = useGetUserCasesPermissions(); + const { detailName: caseId, subCaseId } = useParams<{ + detailName?: string; + subCaseId?: string; + }>(); + + if (userPermissions != null && !userPermissions.read) { + navigateToApp(`${CASES_APP_ID}`); + return null; + } + + return caseId != null ? ( + <> + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + + + ) : null; +}); + +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx b/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx index 49df932766b335..13d8795193238d 100644 --- a/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx +++ b/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx @@ -6,12 +6,11 @@ */ import React, { ComponentType } from 'react'; -import { CasesPage } from '.'; -import { RouteParams } from '../../routes'; +import { AllCasesPage } from './all_cases'; export default { title: 'app/Cases', - component: CasesPage, + component: AllCasesPage, decorators: [ (Story: ComponentType) => { return ; @@ -20,5 +19,5 @@ export default { }; export function EmptyState() { - return } />; + return ; } diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx new file mode 100644 index 00000000000000..acc6bdf68fba75 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { EuiButtonEmpty } from '@elastic/eui'; +import * as i18n from '../../components/app/cases/translations'; +import { CASES_APP_ID, CASES_OWNER } from '../../components/app/cases/constants'; +import { useKibana } from '../../utils/kibana_react'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { casesBreadcrumbs, useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { getCaseUrl, useFormatUrl } from './links'; + +const ButtonEmpty = styled(EuiButtonEmpty)` + display: block; +`; +function ConfigureCasesPageComponent() { + const { + cases, + application: { navigateToApp }, + } = useKibana().services; + const userPermissions = useGetUserCasesPermissions(); + const { ObservabilityPageTemplate } = usePluginContext(); + const onClickGoToCases = useCallback( + async (ev) => { + ev.preventDefault(); + return navigateToApp(`${CASES_APP_ID}`); + }, + [navigateToApp] + ); + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const href = formatUrl(getCaseUrl()); + useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]); + if (userPermissions != null && !userPermissions.read) { + navigateToApp(`${CASES_APP_ID}`); + return null; + } + + return ( + + + {i18n.BACK_TO_ALL} + + {i18n.CONFIGURE_CASES_PAGE_TITLE} + + ), + }} + > + {cases.getConfigureCases({ + userCanCrud: userPermissions?.crud ?? false, + owner: [CASES_OWNER], + })} + + ); +} + +export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx new file mode 100644 index 00000000000000..d0e25e6263075b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import styled from 'styled-components'; +import * as i18n from '../../components/app/cases/translations'; +import { Create } from '../../components/app/cases/create'; +import { CASES_APP_ID } from '../../components/app/cases/constants'; +import { useKibana } from '../../utils/kibana_react'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { getCaseUrl, useFormatUrl } from './links'; +import { casesBreadcrumbs, useBreadcrumbs } from '../../hooks/use_breadcrumbs'; + +const ButtonEmpty = styled(EuiButtonEmpty)` + display: block; +`; +ButtonEmpty.displayName = 'ButtonEmpty'; +export const CreateCasePage = React.memo(() => { + const userPermissions = useGetUserCasesPermissions(); + const { ObservabilityPageTemplate } = usePluginContext(); + const { + application: { navigateToApp }, + } = useKibana().services; + + const goTo = useCallback( + async (ev) => { + ev.preventDefault(); + return navigateToApp(CASES_APP_ID); + }, + [navigateToApp] + ); + + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const href = formatUrl(getCaseUrl()); + useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]); + if (userPermissions != null && !userPermissions.crud) { + navigateToApp(`${CASES_APP_ID}`); + return null; + } + + return ( + + + {i18n.BACK_TO_ALL} + + {i18n.CREATE_TITLE} + + ), + }} + > + + + ); +}); + +CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx new file mode 100644 index 00000000000000..c6fc4b59ef77c3 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 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 { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + IconType, + EuiCard, +} from '@elastic/eui'; +import React, { MouseEventHandler, ReactNode, useMemo } from 'react'; +import styled from 'styled-components'; + +const EmptyPrompt = styled(EuiEmptyPrompt)` + align-self: center; /* Corrects horizontal centering in IE11 */ + max-width: 60em; +`; + +EmptyPrompt.displayName = 'EmptyPrompt'; + +interface EmptyPageActions { + icon?: IconType; + label: string; + target?: string; + url: string; + descriptionTitle?: string; + description?: string; + fill?: boolean; + onClick?: MouseEventHandler; +} + +export type EmptyPageActionsProps = Record; + +interface EmptyPageProps { + actions: EmptyPageActionsProps; + 'data-test-subj'?: string; + message?: ReactNode; + title: string; +} + +const EmptyPageComponent = React.memo(({ actions, message, title, ...rest }) => { + const titles = Object.keys(actions); + const maxItemWidth = 283; + const renderActions = useMemo( + () => + Object.values(actions) + .filter((a) => a.label && a.url) + .map( + ( + { icon, label, target, url, descriptionTitle, description, onClick, fill = true }, + idx + ) => + descriptionTitle != null || description != null ? ( + + + {label} + + } + /> + + ) : ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + {label} + + + ) + ), + [actions, titles] + ); + + return ( + {title}} + body={message &&

{message}

} + actions={{renderActions}} + {...rest} + /> + ); +}); + +EmptyPageComponent.displayName = 'EmptyPageComponent'; + +export const EmptyPage = React.memo(EmptyPageComponent); +EmptyPage.displayName = 'EmptyPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx new file mode 100644 index 00000000000000..5075570c15b3eb --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { EmptyPage } from './empty_page'; +import * as i18n from '../../components/app/cases/translations'; +import { useKibana } from '../../utils/kibana_react'; + +export const CaseFeatureNoPermissions = React.memo(() => { + const docLinks = useKibana().services.docLinks; + const actions = useMemo( + () => ({ + savedObject: { + icon: 'documents', + label: i18n.GO_TO_DOCUMENTATION, + url: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/security/${docLinks.DOC_LINK_VERSION}s`, + target: '_blank', + }, + }), + [docLinks] + ); + + return ( + + ); +}); + +CaseFeatureNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; diff --git a/x-pack/plugins/observability/public/pages/cases/index.tsx b/x-pack/plugins/observability/public/pages/cases/index.tsx deleted file mode 100644 index 7f6bce7d486f3a..00000000000000 --- a/x-pack/plugins/observability/public/pages/cases/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { ExperimentalBadge } from '../../components/shared/experimental_badge'; -import { RouteParams } from '../../routes'; -import { usePluginContext } from '../../hooks/use_plugin_context'; - -interface CasesProps { - routeParams: RouteParams<'/cases'>; -} - -export function CasesPage(props: CasesProps) { - const { ObservabilityPageTemplate } = usePluginContext(); - return ( - - {i18n.translate('xpack.observability.casesTitle', { defaultMessage: 'Cases' })}{' '} - - - ), - }} - > - - - -

- {i18n.translate('xpack.observability.casesDisclaimerText', { - defaultMessage: 'This is the future home of cases.', - })} -

-
-
-
-
- ); -} diff --git a/x-pack/plugins/observability/public/pages/cases/links.ts b/x-pack/plugins/observability/public/pages/cases/links.ts new file mode 100644 index 00000000000000..768d74ec4e7ee3 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/links.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { isEmpty } from 'lodash/fp'; +import { useKibana } from '../../utils/kibana_react'; + +export const getCaseDetailsUrl = ({ id, subCaseId }: { id: string; subCaseId?: string }) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}`; + } + return `/${encodeURIComponent(id)}`; +}; +interface FormatUrlOptions { + absolute: boolean; +} + +export type FormatUrl = (path: string, options?: Partial) => string; +export const useFormatUrl = (appId: string) => { + const { getUrlForApp } = useKibana().services.application; + const formatUrl = useCallback( + (path: string, { absolute = false } = {}) => { + const pathArr = path.split('?'); + const formattedPath = `${pathArr[0]}${isEmpty(pathArr[1]) ? '' : `?${pathArr[1]}`}`; + return getUrlForApp(`${appId}`, { + path: formattedPath, + absolute, + }); + }, + [appId, getUrlForApp] + ); + return { formatUrl }; +}; + +export const getCaseDetailsUrlWithCommentId = ({ + id, + commentId, + subCaseId, +}: { + id: string; + commentId: string; + subCaseId?: string; +}) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent( + subCaseId + )}/${encodeURIComponent(commentId)}`; + } + return `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}`; +}; + +export const getCreateCaseUrl = () => `/create`; + +export const getConfigureCasesUrl = () => `/configure`; +export const getCaseUrl = () => `/`; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index c1b18e37faee8a..03c3fb3c27e58e 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -36,6 +36,8 @@ import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; import { ConfigSchema } from '.'; import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; import { createLazyObservabilityPageTemplate } from './components/shared'; +import { CASES_APP_ID } from './components/app/cases/constants'; +import { CasesUiStart } from '../../cases/public'; export type ObservabilityPublicSetup = ReturnType; @@ -46,6 +48,7 @@ export interface ObservabilityPublicPluginsSetup { } export interface ObservabilityPublicPluginsStart { + cases: CasesUiStart; home?: HomePublicPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; @@ -63,6 +66,7 @@ export class Plugin ObservabilityPublicPluginsStart > { private readonly appUpdater$ = new BehaviorSubject(() => ({})); + private readonly casesAppUpdater$ = new BehaviorSubject(() => ({})); private readonly navigationRegistry = createNavigationRegistry(); constructor(private readonly initializerContext: PluginInitializerContext) { @@ -111,7 +115,6 @@ export class Plugin mount, updater$, }); - if (config.unsafe.alertingExperience.enabled) { coreSetup.application.register({ id: 'observability-alerts', @@ -127,14 +130,14 @@ export class Plugin if (config.unsafe.cases.enabled) { coreSetup.application.register({ - id: 'observability-cases', + id: CASES_APP_ID, title: 'Cases', appRoute: '/app/observability/cases', order: 8050, category, euiIconType, mount, - updater$, + updater$: this.casesAppUpdater$, }); } @@ -188,7 +191,7 @@ export class Plugin }; } public start({ application }: CoreStart) { - toggleOverviewLinkInNav(this.appUpdater$, application); + toggleOverviewLinkInNav(this.appUpdater$, this.casesAppUpdater$, application); const PageTemplate = createLazyObservabilityPageTemplate({ currentAppId$: application.currentAppId$, diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 6e180347106d6e..a2a67a42bd166a 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -13,8 +13,12 @@ import { LandingPage } from '../pages/landing'; import { OverviewPage } from '../pages/overview'; import { jsonRt } from './json_rt'; import { AlertsPage } from '../pages/alerts'; -import { CasesPage } from '../pages/cases'; +import { CreateCasePage } from '../pages/cases/create_case'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; +import { CaseDetailsPage } from '../pages/cases/case_details'; +import { ConfigureCasesPage } from '../pages/cases/configure_cases'; +import { AllCasesPage } from '../pages/cases/all_cases'; +import { casesBreadcrumbs } from '../hooks/use_breadcrumbs'; import { alertStatusRt } from '../../common/typings'; export type RouteParams = DecodeParams; @@ -78,24 +82,36 @@ export const routes = { ], }, '/cases': { - handler: (routeParams: any) => { - return ; + handler: () => { + return ; + }, + params: {}, + breadcrumb: [casesBreadcrumbs.cases], + }, + '/cases/create': { + handler: () => { + return ; + }, + params: {}, + breadcrumb: [casesBreadcrumbs.cases, casesBreadcrumbs.create], + }, + '/cases/configure': { + handler: () => { + return ; + }, + params: {}, + breadcrumb: [casesBreadcrumbs.cases, casesBreadcrumbs.configure], + }, + '/cases/:detailName': { + handler: () => { + return ; }, params: { - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - refreshPaused: jsonRt.pipe(t.boolean), - refreshInterval: jsonRt.pipe(t.number), + path: t.partial({ + detailName: t.string, }), }, - breadcrumb: [ - { - text: i18n.translate('xpack.observability.cases.breadcrumb', { - defaultMessage: 'Cases', - }), - }, - ], + breadcrumb: [casesBreadcrumbs.cases], }, '/alerts': { handler: (routeParams: any) => { diff --git a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx index bbcc12b4831830..caee692ced2c57 100644 --- a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx +++ b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx @@ -14,6 +14,7 @@ import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; describe('toggleOverviewLinkInNav', () => { let applicationStart: ReturnType; let subjectMock: jest.Mocked>; + let casesMock: jest.Mocked>; beforeEach(() => { applicationStart = applicationServiceMock.createStartContract(); @@ -34,7 +35,7 @@ describe('toggleOverviewLinkInNav', () => { }, }; - toggleOverviewLinkInNav(subjectMock, applicationStart); + toggleOverviewLinkInNav(subjectMock, casesMock, applicationStart); expect(subjectMock.next).toHaveBeenCalledTimes(1); const updater = subjectMock.next.mock.calls[0][0]!; @@ -54,7 +55,7 @@ describe('toggleOverviewLinkInNav', () => { }, }; - toggleOverviewLinkInNav(subjectMock, applicationStart); + toggleOverviewLinkInNav(subjectMock, casesMock, applicationStart); expect(subjectMock.next).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx index 5110db15def88f..fd30bf3b73f151 100644 --- a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx +++ b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx @@ -7,13 +7,22 @@ import { Subject } from 'rxjs'; import { AppNavLinkStatus, AppUpdater, ApplicationStart } from '../../../../src/core/public'; +import { CASES_APP_ID } from '../common/const'; export function toggleOverviewLinkInNav( updater$: Subject, + casesUpdater$: Subject, { capabilities }: ApplicationStart ) { - const { apm, logs, metrics, uptime } = capabilities.navLinks; + const { apm, logs, metrics, uptime, [CASES_APP_ID]: cases } = capabilities.navLinks; const someVisible = Object.values({ apm, logs, metrics, uptime }).some((visible) => visible); + + // if cases is enabled then we want to show it in the sidebar but not the navigation unless one of the other features + // is enabled + if (cases) { + casesUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.visible })); + } + if (!someVisible) { updater$.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden, diff --git a/x-pack/plugins/observability/public/utils/kibana_react.ts b/x-pack/plugins/observability/public/utils/kibana_react.ts new file mode 100644 index 00000000000000..532003e30a1601 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/kibana_react.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 { CoreStart } from 'kibana/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { ObservabilityPublicPluginsStart } from '../plugin'; + +export type StartServices = CoreStart & + ObservabilityPublicPluginsStart & { + storage: Storage; + }; +const useTypedKibana = () => useKibana(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 9eff1b08cead98..cfcaeb25d29e40 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/server'; import { RuleDataClient } from '../../rule_registry/server'; import { ObservabilityConfig } from '.'; import { @@ -14,23 +20,65 @@ import { AnnotationsAPI, } from './lib/annotations/bootstrap_annotations'; import type { RuleRegistryPluginSetupContract } from '../../rule_registry/server'; +import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { uiSettings } from './ui_settings'; import { registerRoutes } from './routes/register_routes'; import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; +import { CASES_APP_ID, OBSERVABILITY } from '../common/const'; export type ObservabilityPluginSetup = ReturnType; +interface PluginSetup { + features: FeaturesSetup; + ruleRegistry: RuleRegistryPluginSetupContract; +} + export class ObservabilityPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public setup( - core: CoreSetup, - plugins: { - ruleRegistry: RuleRegistryPluginSetupContract; - } - ) { + public setup(core: CoreSetup, plugins: PluginSetup) { + plugins.features.registerKibanaFeature({ + id: CASES_APP_ID, + name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { + defaultMessage: 'Cases', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.observability, + app: [CASES_APP_ID, 'kibana'], + catalogue: [OBSERVABILITY], + cases: [OBSERVABILITY], + privileges: { + all: { + app: [CASES_APP_ID, 'kibana'], + catalogue: [OBSERVABILITY], + cases: { + all: [OBSERVABILITY], + }, + api: [], + savedObject: { + all: [], + read: [], + }, + ui: ['crud_cases', 'read_cases'], // uiCapabilities[CASES_APP_ID].crud_cases or read_cases + }, + read: { + app: [CASES_APP_ID, 'kibana'], + catalogue: [OBSERVABILITY], + cases: { + read: [OBSERVABILITY], + }, + api: [], + savedObject: { + all: [], + read: [], + }, + ui: ['read_cases'], // uiCapabilities[uiCapabilities[CASES_APP_ID]].read_cases + }, + }, + }); + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 814d55bfd61fb2..b6ed0a0a3d17f6 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../rule_registry/tsconfig.json" }, { "path": "../translations/tsconfig.json" } diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx index 926fe7b63fb5af..0a0caa40a8783e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx @@ -80,7 +80,7 @@ describe('Callout', () => { }); it('dismiss the callout correctly', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx index 8e2f439f02c4bd..4cd7fad10fe70c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx @@ -35,11 +35,9 @@ const CallOutComponent = ({ type, ]); - return showCallOut ? ( + return showCallOut && !isEmpty(messages) ? ( - {!isEmpty(messages) && ( - - )} + {i18n.READ_ONLY_FEATURE_MSG}, diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index 4ec29b676afe6e..9613e327ebe9f3 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -12,7 +12,7 @@ import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AllCases } from '../components/all_cases'; -import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; +import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { SecurityPageName } from '../../app/types'; @@ -24,8 +24,8 @@ export const CasesPage = React.memo(() => { {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( )} diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 03407c7a5adaab..bbc29828731cb4 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -16,7 +16,7 @@ import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { getCaseUrl } from '../../common/components/link_to'; import { navTabs } from '../../app/home/home_navigations'; import { CaseView } from '../components/case_view'; -import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; +import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; export const CaseDetailsPage = React.memo(() => { const history = useHistory(); @@ -37,8 +37,8 @@ export const CaseDetailsPage = React.memo(() => { {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( )} { + await esArchiver.load('x-pack/test/functional/es_archives/cases/default'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/cases/default'); + }); + + describe('observability cases all privileges', () => { + before(async () => { + await security.role.create('cases_observability_all_role', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { spaces: ['*'], base: [], feature: { observabilityCases: ['all'], logs: ['all'] } }, + ], + }); + + await security.user.create('cases_observability_all_user', { + password: 'cases_observability_all_user-password', + roles: ['cases_observability_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'cases_observability_all_user', + 'cases_observability_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.forceLogout(); + await Promise.all([ + security.role.delete('cases_observability_all_role'), + security.user.delete('cases_observability_all_user'), + ]); + }); + + it('shows observability/cases navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text).slice(0, 2); + expect(navLinks).to.eql(['Overview', 'Cases']); + }); + + it(`landing page shows "Create new case" button`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectCreateCaseButtonEnabled(); + }); + + it(`doesn't show read-only badge`, async () => { + await PageObjects.observability.expectNoReadOnlyCallout(); + }); + + it(`allows a case to be created`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + + await testSubjects.click('createNewCaseBtn'); + + await PageObjects.observability.expectCreateCase(); + }); + + it(`allows a case to be edited`, async () => { + await PageObjects.common.navigateToUrl( + 'observabilityCases', + '4c32e6b0-c3c5-11eb-b389-3fadeeafa60f', + { + shouldUseHashForSubUrl: false, + } + ); + await PageObjects.observability.expectAddCommentButton(); + }); + }); + + describe('observability cases read-only privileges', () => { + before(async () => { + await security.role.create('cases_observability_read_role', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { observabilityCases: ['read'], logs: ['all'] }, + }, + ], + }); + + await security.user.create('cases_observability_read_user', { + password: 'cases_observability_read_user-password', + roles: ['cases_observability_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'cases_observability_read_user', + 'cases_observability_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('cases_observability_read_role'); + await security.user.delete('cases_observability_read_user'); + }); + + it('shows observability/cases navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text).slice(0, 2); + expect(navLinks).to.eql(['Overview', 'Cases']); + }); + + it(`landing page shows disabled "Create new case" button`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectCreateCaseButtonDisabled(); + }); + + it(`shows read-only callout`, async () => { + await PageObjects.observability.expectReadOnlyCallout(); + }); + + it(`does not allow a case to be created`, async () => { + await PageObjects.common.navigateToUrl('observabilityCases', 'create', { + shouldUseHashForSubUrl: false, + }); + + // expect redirection to observability cases landing + await PageObjects.observability.expectCreateCaseButtonDisabled(); + }); + + it(`does not allow a case to be edited`, async () => { + await PageObjects.common.navigateToUrl( + 'observabilityCases', + '4c32e6b0-c3c5-11eb-b389-3fadeeafa60f', + { + shouldUseHashForSubUrl: false, + } + ); + await PageObjects.observability.expectAddCommentButtonDisabled(); + }); + }); + + describe('no observability privileges', () => { + before(async () => { + await security.role.create('no_observability_privileges_role', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_observability_privileges_user', { + password: 'no_observability_privileges_user-password', + roles: ['no_observability_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_observability_privileges_user', + 'no_observability_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_observability_privileges_role'); + await security.user.delete('no_observability_privileges_user'); + }); + + it(`returns a 403`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectForbidden(); + }); + + it.skip(`create new case returns a 403`, async () => { + await PageObjects.common.navigateToUrl('observabilityCases', 'create', { + shouldUseHashForSubUrl: false, + }); + await PageObjects.observability.expectForbidden(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts new file mode 100644 index 00000000000000..b7f03b5f27bae4 --- /dev/null +++ b/x-pack/test/functional/apps/observability/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Observability specs', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 20487047a3a56d..2679bb55ad341c 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -60,6 +60,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/reporting_management'), resolve(__dirname, './apps/management'), resolve(__dirname, './apps/reporting'), + resolve(__dirname, './apps/observability'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), @@ -94,6 +95,7 @@ export default async function ({ readConfigFile }) { '--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true', '--timelion.ui.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects + '--xpack.observability.unsafe.cases.enabled=true', ], }, uiSettings: { diff --git a/x-pack/test/functional/es_archives/cases/default/data.json.gz b/x-pack/test/functional/es_archives/cases/default/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9ec4efad4bdfc74c556b4cee7a5a66595c56934e GIT binary patch literal 1355 zcmV-R1+@AfiwFP!000026YZKybK5o$$M60W3@`1$r19W8m!>^6Gi_%YPY-#F1_YKC z8j4g&FpehU?_PjTlI~YUiq%qTfF~f{7&NCXfLCkf9fRE-<_q|M6!6tZZ zsmrF~_jj;X(K`*MDKBdf6Q*=?(d7B%mMLBAFqK6CT$NQXm^Ne*a$#=qrqlZFvg@)O zXi*km_u30KgWZ;O>fF|}vD|e#Q~^`#ao7YA?4ZCw_!`Fvq6v*%FYsU6JFaMl*Bb-O zn9NO>nas-%lK%ubtCi#~FZ=z{R8y!dJ#4GnclfAG#C)Oy>sYWoLFH`$ReL|+O(oUhRhc(!*p5>TO3C7;9wwk( z6#PXFl`2_UCtH!CT}WcGV~Wz^(z07i(It1jjo3+ zm+q6adbsS+#h$)w>n;^!P(i_=o19H5HUnJ7*);b?w!5-J{Cat}y9@KXySVstEO8hEMI2et@IgvQjPl)e^7#~TT{+I`Z*)n#qvO?6OsIp*-Glq#QoOxB=R8ZH4m~vZ| z({jM%xAs3xso<^MZ+=$KflHP@>o-3y^njoD+kWi-eRBX?roumyQ}&9Jw^CZdr80{KMvA@L9%v3UrazC%JYFFs1V zP|F$mKQ7eD)cCMe<{Gfy7_47&v3CD#5>o?aRpYx6AxA>+DW&s!0NpT|CxQ-T8TDEA zwLv!+55WmWZWKp-Ky>i;HJ4S#vDNDNw0`el(LN+~-N3b(jLV_NdPZ!F5TSO@e0TY% z=yRrPO6?Eb*$^EUw~}SmN7cc|2c!mJ5r)(^n-CtXii}3FLo+4-SjGaB?Ute!EB?Ne z726|8e;dnu<1te+ttvq3?DjY%5Tq?A{D(m?F1sH( zt!U>3#R$;eYAOeB7WM+@N5A zGfK>H9~WdL9uTIE-n38}VSxnUyG2{$`Kk6L{xuE~pC*X7zUQB%X8-!bOfSYw9X~ro ztB+6jwpMQ?ep{+3d6ZO>_$j4&7;T4Ccjwx7!~Xm0^OWjydD}dNJTDJy8ege$ZmFrP zExt(gMp+l|Ocs5`K3>lsoV0Q0c${VrlX?2iF#{kT1noH{9bed-{=x>E7ir`yRcLyy z$6k*i*v$cYO~-Fz_A;Yao2ReyQy#`5^^n7(kS&?P^s>PTFho4yaYQ^ZGUE}aO;OA8 zC?V91#;@_lukpU0ukm`79i2=ZPQc?n9f|W)#G#%Wka#4{ITPn+c{9T17}%Jirh`v^ N{{?*4zRYVd003P)t>6Fv literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/cases/default/mappings.json b/x-pack/test/functional/es_archives/cases/default/mappings.json new file mode 100644 index 00000000000000..28d9daff50d94f --- /dev/null +++ b/x-pack/test/functional/es_archives/cases/default/mappings.json @@ -0,0 +1,322 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "properties": { + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "value": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "full_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "description": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "connector_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_url": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "owner": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, + "status": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "full_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "coreMigrationVersion": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "migrationVersion": { + "properties": { + "cases": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index a362fd7e4b7c2a..5c3d9b680fc412 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -16,6 +16,7 @@ import { GrokDebuggerPageObject } from './grok_debugger_page'; import { WatcherPageObject } from './watcher_page'; import { ReportingPageObject } from './reporting_page'; import { AccountSettingsPageObject } from './account_settings_page'; +import { ObservabilityPageProvider } from './observability_page'; import { InfraHomePageProvider } from './infra_home_page'; import { InfraLogsPageProvider } from './infra_logs_page'; import { GisPageObject } from './gis_page'; @@ -82,4 +83,5 @@ export const pageObjects = { navigationalSearch: NavigationalSearchPageObject, banners: BannersPageObject, detections: DetectionsPageObject, + observability: ObservabilityPageProvider, }; diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts new file mode 100644 index 00000000000000..95016c31d10541 --- /dev/null +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function ObservabilityPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + return { + async expectCreateCaseButtonEnabled() { + const button = await testSubjects.find('createNewCaseBtn', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be(null); + }, + + async expectCreateCaseButtonDisabled() { + const button = await testSubjects.find('createNewCaseBtn', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be('true'); + }, + + async expectReadOnlyCallout() { + await testSubjects.existOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); + }, + + async expectNoReadOnlyCallout() { + await testSubjects.missingOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); + }, + + async expectCreateCase() { + await testSubjects.existOrFail('case-creation-form-steps'); + }, + + async expectAddCommentButton() { + const button = await testSubjects.find('submit-comment', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be(null); + }, + + async expectAddCommentButtonDisabled() { + const button = await testSubjects.find('submit-comment', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be('true'); + }, + + async expectForbidden() { + const h2 = await find.byCssSelector('body', 20000); + const text = await h2.getVisibleText(); + expect(text).to.contain('Kibana feature privileges required'); + }, + }; +} From 95604fdd223f34aecc03244ff6858939d23fc336 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 10 Jun 2021 15:48:08 -0700 Subject: [PATCH 38/99] [DOCS] Fixes terminology in Stack Monitoring:Kibana alerts (#101696) --- ...onitoring-kibana-alerting-notification.png | Bin 0 -> 105706 bytes .../monitoring-kibana-alerting-setup-mode.png | Bin 0 -> 111618 bytes docs/user/monitoring/kibana-alerts.asciidoc | 107 ++++++++++-------- 3 files changed, 58 insertions(+), 49 deletions(-) create mode 100644 docs/user/monitoring/images/monitoring-kibana-alerting-notification.png create mode 100644 docs/user/monitoring/images/monitoring-kibana-alerting-setup-mode.png diff --git a/docs/user/monitoring/images/monitoring-kibana-alerting-notification.png b/docs/user/monitoring/images/monitoring-kibana-alerting-notification.png new file mode 100644 index 0000000000000000000000000000000000000000..90951d18e667b6fcc45467f283053669a9ad6c28 GIT binary patch literal 105706 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
O^=Qm=3u}DYZ4@B}b6YoA*t$ZE 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 H>ihm5n4}l8 literal 0 HcmV?d00001 diff --git a/docs/user/monitoring/images/monitoring-kibana-alerting-setup-mode.png b/docs/user/monitoring/images/monitoring-kibana-alerting-setup-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..146992da5837abdfb75107755cf5af38ffc0141d GIT binary patch literal 111618 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

cvn8_#JsY>&8Ua(-A;F{p~dkcoF< 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

{title || diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 38669d72474df1..1762e7ff20fabb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -57,6 +57,7 @@ export function createMockVisualization(): jest.Mocked { setDimension: jest.fn(), removeDimension: jest.fn(), getErrorMessages: jest.fn((_state) => undefined), + renderDimensionEditor: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss index bf833c4a369325..874291ae25e341 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.scss @@ -2,7 +2,27 @@ height: 100%; } -.lnsIndexPatternDimensionEditor__section { +.lnsIndexPatternDimensionEditor__header { + position: sticky; + top: 0; + background: $euiColorEmptyShade; + // Raise it above the elements that are after it in DOM order + z-index: $euiZLevel1; +} + +.lnsIndexPatternDimensionEditor-isFullscreen { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + + .lnsIndexPatternDimensionEditor__section { + height: 100%; + } +} + +.lnsIndexPatternDimensionEditor__section--padded { padding: $euiSizeS; } @@ -10,6 +30,14 @@ background-color: $euiColorLightestShade; } +.lnsIndexPatternDimensionEditor__section--top { + border-bottom: $euiBorderThin; +} + +.lnsIndexPatternDimensionEditor__section--bottom { + border-top: $euiBorderThin; +} + .lnsIndexPatternDimensionEditor__columns { column-count: 2; column-gap: $euiSizeXL; @@ -29,3 +57,9 @@ padding-top: 0; padding-bottom: 0; } + +.lnsIndexPatternDimensionEditor__warning { + @include kbnThemeStyle('v7') { + border: none; + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 2ae7b9403a46db..3dd2d4a4ba3f50 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -6,7 +6,7 @@ */ import './dimension_editor.scss'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiListGroup, @@ -17,6 +17,9 @@ import { EuiFormLabel, EuiToolTip, EuiText, + EuiTabs, + EuiTab, + EuiCallOut, } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; @@ -91,6 +94,8 @@ export function DimensionEditor(props: DimensionEditorProps) { hideGrouping, dateRange, dimensionGroups, + toggleFullscreen, + isFullscreen, } = props; const services = { data: props.data, @@ -101,30 +106,34 @@ export function DimensionEditor(props: DimensionEditorProps) { }; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; + const selectedOperationDefinition = + selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + const setStateWrapper = ( setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) ) => { + const prevOperationType = + operationDefinitionMap[state.layers[layerId].columns[columnId]?.operationType]?.input; + const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter; const hasIncompleteColumns = Boolean(hypotheticalLayer.incompleteColumns?.[columnId]); - const prevOperationType = - operationDefinitionMap[hypotheticalLayer.columns[columnId]?.operationType]?.input; setState( (prevState) => { const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter; return mergeLayer({ state: prevState, layerId, newLayer: layer }); }, { - shouldReplaceDimension: Boolean(hypotheticalLayer.columns[columnId]), - // clear the dimension if there's an incomplete column pending && previous operation was a fullReference operation - shouldRemoveDimension: Boolean( - hasIncompleteColumns && prevOperationType === 'fullReference' - ), + isDimensionComplete: + prevOperationType === 'fullReference' + ? !hasIncompleteColumns + : Boolean(hypotheticalLayer.columns[columnId]), } ); }; - const selectedOperationDefinition = - selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + const setIsCloseable = (isCloseable: boolean) => { + setState((prevState) => ({ ...prevState, isDimensionClosePrevented: !isCloseable })); + }; const incompleteInfo = (state.layers[layerId].incompleteColumns ?? {})[columnId]; const incompleteOperation = incompleteInfo?.operationType; @@ -132,14 +141,16 @@ export function DimensionEditor(props: DimensionEditorProps) { const ParamEditor = selectedOperationDefinition?.paramEditor; + const [temporaryQuickFunction, setQuickFunction] = useState(false); + const possibleOperations = useMemo(() => { return Object.values(operationDefinitionMap) .filter(({ hidden }) => !hidden) + .filter(({ type }) => fieldByOperation[type]?.size || operationWithoutField.has(type)) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) - .map((def) => def.type) - .filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type)); + .map((def) => def.type); }, [fieldByOperation, operationWithoutField]); const [filterByOpenInitially, setFilterByOpenInitally] = useState(false); @@ -245,37 +256,44 @@ export function DimensionEditor(props: DimensionEditorProps) { visualizationGroups: dimensionGroups, targetGroup: props.groupId, }); + if (temporaryQuickFunction && newLayer.columns[columnId].operationType !== 'formula') { + // Only switch the tab once the formula is fully removed + setQuickFunction(false); + } setStateWrapper(newLayer); trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } else if (!selectedColumn || !compatibleWithCurrentField) { const possibleFields = fieldByOperation[operationType] || new Set(); + let newLayer: IndexPatternLayer; if (possibleFields.size === 1) { - setStateWrapper( - insertOrReplaceColumn({ - layer: props.state.layers[props.layerId], - indexPattern: currentIndexPattern, - columnId, - op: operationType, - field: currentIndexPattern.getFieldByName(possibleFields.values().next().value), - visualizationGroups: dimensionGroups, - targetGroup: props.groupId, - }) - ); + newLayer = insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: currentIndexPattern.getFieldByName(possibleFields.values().next().value), + visualizationGroups: dimensionGroups, + targetGroup: props.groupId, + }); } else { - setStateWrapper( - insertOrReplaceColumn({ - layer: props.state.layers[props.layerId], - indexPattern: currentIndexPattern, - columnId, - op: operationType, - field: undefined, - visualizationGroups: dimensionGroups, - targetGroup: props.groupId, - }) - ); + newLayer = insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: undefined, + visualizationGroups: dimensionGroups, + targetGroup: props.groupId, + }); + // ); + } + if (temporaryQuickFunction && newLayer.columns[columnId].operationType !== 'formula') { + // Only switch the tab once the formula is fully removed + setQuickFunction(false); } + setStateWrapper(newLayer); trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } @@ -287,6 +305,9 @@ export function DimensionEditor(props: DimensionEditorProps) { return; } + if (temporaryQuickFunction) { + setQuickFunction(false); + } const newLayer = replaceColumn({ layer: props.state.layers[props.layerId], indexPattern: currentIndexPattern, @@ -315,9 +336,34 @@ export function DimensionEditor(props: DimensionEditorProps) { currentFieldIsInvalid ); - return ( -
-
+ const shouldDisplayExtraOptions = + !currentFieldIsInvalid && + !incompleteInfo && + selectedColumn && + selectedColumn.operationType !== 'formula'; + + const quickFunctions = ( + <> + {temporaryQuickFunction && selectedColumn?.operationType === 'formula' && ( + <> + +

+ {i18n.translate('xpack.lens.indexPattern.formulaWarningText', { + defaultMessage: 'To overwrite your formula, select a quick function', + })} +

+
+ + )} +
{i18n.translate('xpack.lens.indexPattern.functionsLabel', { defaultMessage: 'Select a function', @@ -336,7 +382,7 @@ export function DimensionEditor(props: DimensionEditorProps) { />
-
+
{!incompleteInfo && selectedColumn && 'references' in selectedColumn && @@ -375,6 +421,9 @@ export function DimensionEditor(props: DimensionEditorProps) { currentColumn: state.layers[layerId].columns[columnId], })} dimensionGroups={dimensionGroups} + isFullscreen={isFullscreen} + toggleFullscreen={toggleFullscreen} + setIsCloseable={setIsCloseable} {...services} /> ); @@ -385,7 +434,8 @@ export function DimensionEditor(props: DimensionEditorProps) { {!selectedColumn || selectedOperationDefinition?.input === 'field' || - (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') ? ( + (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') || + temporaryQuickFunction ? ( ) : null} - {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ParamEditor && ( - <> - - + {shouldDisplayExtraOptions && ParamEditor && ( + )} {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( @@ -546,9 +597,96 @@ export function DimensionEditor(props: DimensionEditorProps) {
+ + ); - {!currentFieldIsInvalid && ( -
+ const formulaTab = ParamEditor ? ( + + ) : null; + + const onFormatChange = useCallback( + (newFormat) => { + setState( + mergeLayer({ + state, + layerId, + newLayer: updateColumnParam({ + layer: state.layers[layerId], + columnId, + paramName: 'format', + value: newFormat, + }), + }) + ); + }, + [columnId, layerId, setState, state] + ); + + return ( +
+ {!isFullscreen && operationSupportMatrix.operationWithoutField.has('formula') ? ( + + { + if (selectedColumn?.operationType === 'formula') { + setQuickFunction(true); + } + }} + > + {i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', { + defaultMessage: 'Quick functions', + })} + + { + if (selectedColumn?.operationType !== 'formula') { + setQuickFunction(false); + const newLayer = insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: 'formula', + visualizationGroups: dimensionGroups, + }); + setStateWrapper(newLayer); + trackUiEvent(`indexpattern_dimension_operation_formula`); + return; + } else { + setQuickFunction(false); + } + }} + > + {i18n.translate('xpack.lens.indexPattern.formulaLabel', { + defaultMessage: 'Formula', + })} + + + ) : null} + + {isFullscreen + ? formulaTab + : selectedOperationDefinition?.type === 'formula' && !temporaryQuickFunction + ? formulaTab + : quickFunctions} + + {!isFullscreen && !currentFieldIsInvalid && !temporaryQuickFunction && ( +
{!incompleteInfo && selectedColumn && ( )} - {!incompleteInfo && !hideGrouping && ( + {!isFullscreen && !incompleteInfo && !hideGrouping && ( )} - {selectedColumn && + {!isFullscreen && + selectedColumn && (selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? ( - { - setState( - mergeLayer({ - state, - layerId, - newLayer: updateColumnParam({ - layer: state.layers[layerId], - columnId, - paramName: 'format', - value: newFormat, - }), - }) - ); - }} - /> + ) : null}
)}
); } + function getErrorMessage( selectedColumn: IndexPatternColumn | undefined, incompleteOperation: boolean, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 03db6141b917f9..7e45b295215631 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -208,6 +208,8 @@ describe('IndexPatternDimensionEditorPanel', () => { core: {} as CoreSetup, dimensionGroups: [], groupId: 'a', + isFullscreen: false, + toggleFullscreen: jest.fn(), }; jest.clearAllMocks(); @@ -500,10 +502,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...initialState, layers: { @@ -535,10 +534,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...state, layers: { @@ -569,10 +565,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, layers: { @@ -643,10 +636,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, layers: { @@ -681,10 +671,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, layers: { @@ -750,10 +737,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, layers: { @@ -879,7 +863,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(setState.mock.calls[0]).toEqual([ expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: false }, + { isDimensionComplete: false }, ]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, @@ -948,7 +932,7 @@ describe('IndexPatternDimensionEditorPanel', () => { // Now check that the dimension gets cleaned up on state update expect(setState.mock.calls[0]).toEqual([ expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: false }, + { isDimensionComplete: false }, ]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, @@ -1042,10 +1026,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); expect(setState.mock.calls.length).toEqual(2); - expect(setState.mock.calls[1]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[1]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[1][0](state)).toEqual({ ...state, layers: { @@ -1143,10 +1124,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('[data-test-subj="indexPattern-time-scaling-enable"]') .hostNodes() .simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1175,10 +1153,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') .simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1205,10 +1180,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1239,10 +1211,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!(({ target: { value: 'h' }, } as unknown) as ChangeEvent); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1269,10 +1238,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!(({ target: { value: 'h' }, } as unknown) as ChangeEvent); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1300,10 +1266,7 @@ describe('IndexPatternDimensionEditorPanel', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any ); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1593,10 +1556,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('[data-test-subj="indexPattern-filter-by-enable"]') .hostNodes() .simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1627,10 +1587,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') .simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1656,10 +1613,7 @@ describe('IndexPatternDimensionEditorPanel', () => { language: 'kuery', query: 'c: d', }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1688,10 +1642,7 @@ describe('IndexPatternDimensionEditorPanel', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any ); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, layers: { @@ -1743,10 +1694,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: false }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: false }]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...state, layers: { @@ -1810,10 +1758,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](initialState)).toEqual({ ...initialState, layers: { @@ -1838,10 +1783,7 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ ...state, layers: { @@ -1975,10 +1917,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { shouldRemoveDimension: false, shouldReplaceDimension: true }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...state, layers: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index a77a980257c88c..56d255ec02227c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -284,6 +284,8 @@ describe('IndexPatternDimensionEditorPanel', () => { } as unknown) as DataPublicPluginStart, core: {} as CoreSetup, dimensionGroups: [], + isFullscreen: false, + toggleFullscreen: () => {}, }; jest.clearAllMocks(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx index 3a57579583c90c..ff10810208e706 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/format_selector.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiComboBox, EuiSpacer, EuiRange } from '@elastic/eui'; import { IndexPatternColumn } from '../indexpattern'; @@ -28,6 +28,13 @@ const supportedFormats: Record = { }, }; +const defaultOption = { + value: '', + label: i18n.translate('xpack.lens.indexPattern.defaultFormatLabel', { + defaultMessage: 'Default', + }), +}; + interface FormatSelectorProps { selectedColumn: IndexPatternColumn; onChange: (newFormat?: { id: string; params?: Record }) => void; @@ -37,6 +44,8 @@ interface State { decimalPlaces: number; } +const singleSelectionOption = { asPlainText: true }; + export function FormatSelector(props: FormatSelectorProps) { const { selectedColumn, onChange } = props; @@ -51,13 +60,6 @@ export function FormatSelector(props: FormatSelectorProps) { const selectedFormat = currentFormat?.id ? supportedFormats[currentFormat.id] : undefined; - const defaultOption = { - value: '', - label: i18n.translate('xpack.lens.indexPattern.defaultFormatLabel', { - defaultMessage: 'Default', - }), - }; - const label = i18n.translate('xpack.lens.indexPattern.columnFormatLabel', { defaultMessage: 'Value format', }); @@ -66,6 +68,48 @@ export function FormatSelector(props: FormatSelectorProps) { defaultMessage: 'Decimals', }); + const stableOptions = useMemo( + () => [ + defaultOption, + ...Object.entries(supportedFormats).map(([id, format]) => ({ + value: id, + label: format.title ?? id, + })), + ], + [] + ); + + const onChangeWrapped = useCallback( + (choices) => { + if (choices.length === 0) { + return; + } + + if (!choices[0].value) { + onChange(); + return; + } + onChange({ + id: choices[0].value, + params: { decimals: state.decimalPlaces }, + }); + }, + [onChange, state.decimalPlaces] + ); + + const currentOption = useMemo( + () => + currentFormat + ? [ + { + value: currentFormat.id, + label: selectedFormat?.title ?? currentFormat.id, + }, + ] + : [defaultOption], + [currentFormat, selectedFormat?.title] + ); + return ( <> @@ -76,38 +120,10 @@ export function FormatSelector(props: FormatSelectorProps) { isClearable={false} data-test-subj="indexPattern-dimension-format" aria-label={label} - singleSelection={{ asPlainText: true }} - options={[ - defaultOption, - ...Object.entries(supportedFormats).map(([id, format]) => ({ - value: id, - label: format.title ?? id, - })), - ]} - selectedOptions={ - currentFormat - ? [ - { - value: currentFormat.id, - label: selectedFormat?.title ?? currentFormat.id, - }, - ] - : [defaultOption] - } - onChange={(choices) => { - if (choices.length === 0) { - return; - } - - if (!choices[0].value) { - onChange(); - return; - } - onChange({ - id: choices[0].value, - params: { decimals: state.decimalPlaces }, - }); - }} + singleSelection={singleSelectionOption} + options={stableOptions} + selectedOptions={currentOption} + onChange={onChangeWrapped} /> {currentFormat ? ( <> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index 645b6bfe70a97c..fd3ad9a4e5dd54 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -51,6 +51,9 @@ describe('reference editor', () => { http: {} as HttpSetup, data: {} as DataPublicPluginStart, dimensionGroups: [], + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index c473be05ba3154..b0cdf96f928f93 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -47,10 +47,14 @@ export interface ReferenceEditorProps { setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) ) => void; currentIndexPattern: IndexPattern; + existingFields: IndexPatternPrivateState['existingFields']; dateRange: DateRange; labelAppend?: EuiFormRowProps['labelAppend']; dimensionGroups: VisualizationDimensionGroupConfig[]; + isFullscreen: boolean; + toggleFullscreen: () => void; + setIsCloseable: (isCloseable: boolean) => void; // Services uiSettings: IUiSettingsClient; @@ -72,6 +76,9 @@ export function ReferenceEditor(props: ReferenceEditorProps) { dateRange, labelAppend, dimensionGroups, + isFullscreen, + toggleFullscreen, + setIsCloseable, ...services } = props; @@ -347,6 +354,9 @@ export function ReferenceEditor(props: ReferenceEditorProps) { indexPattern={currentIndexPattern} dateRange={dateRange} operationDefinitionMap={operationDefinitionMap} + isFullscreen={isFullscreen} + toggleFullscreen={toggleFullscreen} + setIsCloseable={setIsCloseable} {...services} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 7cb49de15c0665..bc2184bd9edb7b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -323,6 +323,11 @@ export function getIndexPatternDatasource({ domElement ); }, + + canCloseDimensionEditor: (state) => { + return !state.isDimensionClosePrevented; + }, + getDropProps, onDrop, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 864a3a6f089db7..93ea3069894d32 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -16,6 +16,7 @@ import { } from './indexpattern_suggestions'; import { documentField } from './document_field'; import { getFieldByNameFactory } from './pure_helpers'; +import { isEqual } from 'lodash'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -867,10 +868,7 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }); - expect(suggestions).toHaveLength(1); - // Check that the suggestion is a single metric - expect(suggestions[0].table.columns).toHaveLength(1); - expect(suggestions[0].table.columns[0].operation.isBucketed).toBeFalsy(); + expect(suggestions).toHaveLength(0); }); it('appends a terms column with default size on string field', () => { @@ -1025,6 +1023,24 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' })); }); + it('skips metric only suggestion when the field is already in use', () => { + const initialState = stateWithNonEmptyTables(); + const suggestions = getDatasourceSuggestionsForField(initialState, '1', { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }); + + expect( + suggestions.some( + (suggestion) => + suggestion.table.changeType === 'initial' && suggestion.table.columns.length === 1 + ) + ).toBeFalsy(); + }); + it('skips duplicates when the document-specific field is already in use', () => { const initialState = stateWithNonEmptyTables(); const modifiedState: IndexPatternPrivateState = { @@ -2344,7 +2360,7 @@ describe('IndexPattern Data Source suggestions', () => { ); }); - it('will skip a reduced suggestion when handling multiple references', () => { + it('will create reduced suggestions with all referenced children when handling references', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { ...initialState, @@ -2352,7 +2368,17 @@ describe('IndexPattern Data Source suggestions', () => { ...initialState.layers, first: { ...initialState.layers.first, - columnOrder: ['date', 'metric', 'metric2', 'ref', 'ref2'], + columnOrder: [ + 'date', + 'metric', + 'metric2', + 'ref', + 'ref2', + 'ref3', + 'ref4', + 'metric3', + 'metric4', + ], columns: { date: { @@ -2384,6 +2410,20 @@ describe('IndexPattern Data Source suggestions', () => { operationType: 'count', sourceField: 'Records', }, + metric3: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, + metric4: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, ref2: { label: '', dataType: 'number', @@ -2391,22 +2431,163 @@ describe('IndexPattern Data Source suggestions', () => { operationType: 'cumulative_sum', references: ['metric2'], }, + ref3: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'math', + references: ['ref4', 'metric3'], + params: { + tinymathAst: '', + }, + }, + ref4: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'math', + references: ['metric4'], + params: { + tinymathAst: '', + }, + }, }, }, }, }; - const result = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); - - expect(result).not.toContainEqual( - expect.objectContaining({ - table: expect.objectContaining({ - changeType: 'reduced', - }), - }) - ); + const result = getDatasourceSuggestionsFromCurrentState(state); + + // only generate suggestions for top level metrics + expect( + result.filter((suggestion) => suggestion.table.changeType === 'reduced').length + ).toEqual(3); + + // top level "ref" column + expect( + result.some( + (suggestion) => + suggestion.table.changeType === 'reduced' && + isEqual(suggestion.state.layers.first.columnOrder, ['ref', 'metric']) + ) + ).toBeTruthy(); + + // top level "ref2" column + expect( + result.some( + (suggestion) => + suggestion.table.changeType === 'reduced' && + isEqual(suggestion.state.layers.first.columnOrder, ['ref2', 'metric2']) + ) + ).toBeTruthy(); + + // top level "ref3" column + expect( + result.some( + (suggestion) => + suggestion.table.changeType === 'reduced' && + isEqual(suggestion.state.layers.first.columnOrder, [ + 'ref3', + 'ref4', + 'metric3', + 'metric4', + ]) + ) + ).toBeTruthy(); }); }); + + it('will leave dangling references in place', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + ...initialState.layers, + first: { + ...initialState.layers.first, + columnOrder: ['date', 'ref'], + + columns: { + date: { + label: '', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + ref: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['non_existing_metric'], + }, + }, + }, + }, + }; + + const result = getDatasourceSuggestionsFromCurrentState(state); + + // only generate suggestions for top level metrics + expect( + result.filter((suggestion) => suggestion.table.changeType === 'reduced').length + ).toEqual(1); + + // top level "ref" column + expect( + result.some( + (suggestion) => + suggestion.table.changeType === 'reduced' && + isEqual(suggestion.state.layers.first.columnOrder, ['ref']) + ) + ).toBeTruthy(); + }); + + it('will not suggest reduced tables if there is just a referenced top level metric', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + ...initialState, + layers: { + ...initialState.layers, + first: { + ...initialState.layers.first, + columnOrder: ['ref', 'metric'], + + columns: { + ref: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'math', + params: { + tinymathAst: '', + }, + references: ['metric'], + }, + metric: { + label: '', + dataType: 'number', + isBucketed: false, + operationType: 'count', + sourceField: 'Records', + }, + }, + }, + }, + }; + + const result = getDatasourceSuggestionsFromCurrentState(state); + + expect( + result.filter((suggestion) => suggestion.table.changeType === 'unchanged').length + ).toEqual(1); + + expect( + result.filter((suggestion) => suggestion.table.changeType === 'reduced').length + ).toEqual(0); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 803ba9f5bae5dc..cff036db4813bb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { flatten, minBy, pick, mapValues } from 'lodash'; +import { flatten, minBy, pick, mapValues, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import { generateId } from '../id_generator'; import { DatasourceSuggestion, TableChangeType } from '../types'; @@ -20,6 +20,7 @@ import { OperationType, getExistingColumnGroups, isReferenced, + getReferencedColumnIds, } from './operations'; import { hasField } from './utils'; import { @@ -254,9 +255,11 @@ function getExistingLayerSuggestionsForField( } } - const metricSuggestion = createMetricSuggestion(indexPattern, layerId, state, field); - if (metricSuggestion) { - suggestions.push(metricSuggestion); + if (!fieldInUse) { + const metricSuggestion = createMetricSuggestion(indexPattern, layerId, state, field); + if (metricSuggestion) { + suggestions.push(metricSuggestion); + } } return suggestions; @@ -514,8 +517,11 @@ function createAlternativeMetricSuggestions( ) { const layer = state.layers[layerId]; const suggestions: Array> = []; + const topLevelMetricColumns = layer.columnOrder.filter( + (columnId) => !isReferenced(layer, columnId) + ); - layer.columnOrder.forEach((columnId) => { + topLevelMetricColumns.forEach((columnId) => { const column = layer.columns[columnId]; if (!hasField(column)) { return; @@ -580,11 +586,13 @@ function createSuggestionWithDefaultDateHistogram( function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layerId: string) { const layer = state.layers[layerId]; - const [ - availableBucketedColumns, - availableMetricColumns, - availableReferenceColumns, - ] = getExistingColumnGroups(layer); + const [availableBucketedColumns, availableMetricColumns] = partition( + layer.columnOrder, + (colId) => layer.columns[colId].isBucketed + ); + const topLevelMetricColumns = availableMetricColumns.filter( + (columnId) => !isReferenced(layer, columnId) + ); return flatten( availableBucketedColumns.map((_col, index) => { @@ -593,46 +601,60 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer const allMetricsSuggestion = { ...layer, columnOrder: [...bucketedColumns, ...availableMetricColumns], + noBuckets: false, }; - if (availableBucketedColumns.length <= 1 || availableReferenceColumns.length) { - // Don't simplify when dealing with single-bucket table. Also don't break - // reference-based columns by removing buckets. + if (availableBucketedColumns.length <= 1) { + // Don't simplify when dealing with single-bucket table. return []; - } else if (availableMetricColumns.length > 1) { - return [{ ...layer, columnOrder: [...bucketedColumns, availableMetricColumns[0]] }]; + } else if (topLevelMetricColumns.length > 1) { + return [ + { + ...layer, + columnOrder: [ + ...bucketedColumns, + topLevelMetricColumns[0], + ...getReferencedColumnIds(layer, topLevelMetricColumns[0]), + ], + noBuckets: false, + }, + ]; } else { return allMetricsSuggestion; } }) ) .concat( - availableReferenceColumns.length - ? [] - : availableMetricColumns.map((columnId) => { - return { ...layer, columnOrder: [columnId] }; + // if there is just a single top level metric, the unchanged suggestion will take care of this case - only split up if there are multiple metrics or at least one bucket + availableBucketedColumns.length > 0 || topLevelMetricColumns.length > 1 + ? topLevelMetricColumns.map((columnId) => { + return { + ...layer, + columnOrder: [columnId, ...getReferencedColumnIds(layer, columnId)], + noBuckets: true, + }; }) + : [] ) - .map((updatedLayer) => { + .map(({ noBuckets, ...updatedLayer }) => { return buildSuggestion({ state, layerId, updatedLayer, changeType: 'reduced', - label: - updatedLayer.columnOrder.length === 1 - ? getMetricSuggestionTitle(updatedLayer, availableMetricColumns.length === 1) - : undefined, + label: noBuckets + ? getMetricSuggestionTitle(updatedLayer, availableMetricColumns.length === 1) + : undefined, }); }); } -function getMetricSuggestionTitle(layer: IndexPatternLayer, onlyMetric: boolean) { +function getMetricSuggestionTitle(layer: IndexPatternLayer, onlySimpleMetric: boolean) { const { operationType, label } = layer.columns[layer.columnOrder[0]]; return i18n.translate('xpack.lens.indexpattern.suggestions.overallLabel', { defaultMessage: '{operation} overall', values: { - operation: onlyMetric ? operationDefinitionMap[operationType].displayName : label, + operation: onlySimpleMetric ? operationDefinitionMap[operationType].displayName : label, }, description: 'Title of a suggested chart containing only a single numerical metric calculated over all available data', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 40d7e3ef94ad68..d6429fb67e9a1d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -17,6 +17,7 @@ jest.spyOn(actualHelpers, 'insertOrReplaceColumn'); jest.spyOn(actualHelpers, 'insertNewColumn'); jest.spyOn(actualHelpers, 'replaceColumn'); jest.spyOn(actualHelpers, 'getErrorMessages'); +jest.spyOn(actualHelpers, 'getColumnOrder'); export const { getAvailableOperationsByMetadata, @@ -48,6 +49,8 @@ export const { resetIncomplete, isOperationAllowedAsReference, canTransition, + isColumnValidAsReference, + getManagedColumnsFrom, } = actualHelpers; export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index 823ec3eb58a924..396eae9b39c412 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -121,5 +121,23 @@ export const counterRateOperation: OperationDefinition< }, timeScalingMode: 'mandatory', filterable: true, + documentation: { + section: 'calculation', + signature: i18n.translate('xpack.lens.indexPattern.counterRate.signature', { + defaultMessage: 'metric: number', + }), + description: i18n.translate('xpack.lens.indexPattern.counterRate.documentation', { + defaultMessage: ` +Calculates the rate of an ever increasing counter. This function will only yield helpful results on counter metric fields which contain a measurement of some kind monotonically growing over time. +If the value does get smaller, it will interpret this as a counter reset. To get most precise results, \`counter_rate\` should be calculated on the \`max\` of a field. + +This calculation will be done separately for separate series defined by filters or top values dimensions. +It uses the current interval when used in Formula. + +Example: Visualize the rate of bytes received over time by a memcached server: +\`counter_rate(max(memcached.stats.read.bytes))\` + `, + }), + }, shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index c4f01e27be886f..f39e5587b398c0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -117,5 +117,21 @@ export const cumulativeSumOperation: OperationDefinition< )?.join(', '); }, filterable: true, + documentation: { + section: 'calculation', + signature: i18n.translate('xpack.lens.indexPattern.cumulative_sum.signature', { + defaultMessage: 'metric: number', + }), + description: i18n.translate('xpack.lens.indexPattern.cumulativeSum.documentation', { + defaultMessage: ` +Calculates the cumulative sum of a metric over time, adding all previous values of a series to each value. To use this function, you need to configure a date histogram dimension as well. + +This calculation will be done separately for separate series defined by filters or top values dimensions. + +Example: Visualize the received bytes accumulated over time: +\`cumulative_sum(sum(bytes))\` + `, + }), + }, shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index 7c48b5742b8dbb..e103acd9ab6779 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -109,5 +109,22 @@ export const derivativeOperation: OperationDefinition< }, timeScalingMode: 'optional', filterable: true, + documentation: { + section: 'calculation', + signature: i18n.translate('xpack.lens.indexPattern.differences.signature', { + defaultMessage: 'metric: number', + }), + description: i18n.translate('xpack.lens.indexPattern.differences.documentation', { + defaultMessage: ` +Calculates the difference to the last value of a metric over time. To use this function, you need to configure a date histogram dimension as well. +Differences requires the data to be sequential. If your data is empty when using differences, try increasing the date histogram interval. + +This calculation will be done separately for separate series defined by filters or top values dimensions. + +Example: Visualize the change in bytes received over time: +\`differences(sum(bytes))\` + `, + }), + }, shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index a3d0241d4887e9..ee305bc043f1be 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -65,7 +65,9 @@ export const movingAverageOperation: OperationDefinition< validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], - operationParams: [{ name: 'window', type: 'number', required: true }], + operationParams: [ + { name: 'window', type: 'number', required: false, defaultValue: WINDOW_DEFAULT_VALUE }, + ], getPossibleOperation: (indexPattern) => { if (hasDateField(indexPattern)) { return { @@ -130,6 +132,28 @@ export const movingAverageOperation: OperationDefinition< }, timeScalingMode: 'optional', filterable: true, + documentation: { + section: 'calculation', + signature: i18n.translate('xpack.lens.indexPattern.moving_average.signature', { + defaultMessage: 'metric: number, [window]: number', + }), + description: i18n.translate('xpack.lens.indexPattern.movingAverage.documentation', { + defaultMessage: ` +Calculates the moving average of a metric over time, averaging the last n-th values to calculate the current value. To use this function, you need to configure a date histogram dimension as well. +The default window value is {defaultValue}. + +This calculation will be done separately for separate series defined by filters or top values dimensions. + +Takes a named parameter \`window\` which specifies how many last values to include in the average calculation for the current value. + +Example: Smooth a line of measurements: +\`moving_average(sum(bytes), window=5)\` + `, + values: { + defaultValue: WINDOW_DEFAULT_VALUE, + }, + }), + }, shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 1911af0a6f679b..4da8a3ca0eb24e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -116,4 +116,21 @@ export const cardinalityOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 46fddd9b1ffbf4..75068817c61237 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -28,6 +28,9 @@ const defaultProps = { http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), operationDefinitionMap: {}, + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), }; // mocking random id generator function diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss new file mode 100644 index 00000000000000..14b3fc33efb4e6 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss @@ -0,0 +1,167 @@ +.lnsFormula { + display: flex; + flex-direction: column; + + .lnsIndexPatternDimensionEditor-isFullscreen & { + height: 100%; + } + + & > * { + flex: 1; + min-height: 0; + } + + & > * + * { + border-top: $euiBorderThin; + } +} + +.lnsFormula__editor { + border-bottom: $euiBorderThin; + + .lnsIndexPatternDimensionEditor-isFullscreen & { + border-bottom: none; + display: flex; + flex-direction: column; + } + + & > * + * { + border-top: $euiBorderThin; + } +} + +.lnsFormula__editorHeader, +.lnsFormula__editorFooter { + padding: $euiSizeS; +} + +.lnsFormula__editorFooter { + // make sure docs are rendered in front of monaco + z-index: 1; + background-color: $euiColorLightestShade; +} + +.lnsFormula__editorHeaderGroup, +.lnsFormula__editorFooterGroup { + display: block; // Overrides EUI's styling of `display: flex` on `EuiFlexItem` components +} + +.lnsFormula__editorContent { + position: relative; + height: 201px; +} + +.lnsFormula__editorPlaceholder { + position: absolute; + top: 0; + left: $euiSize; + right: 0; + color: $euiTextSubduedColor; + // Matches monaco editor + font-family: Menlo, Monaco, 'Courier New', monospace; + pointer-events: none; +} + +.lnsIndexPatternDimensionEditor-isFullscreen .lnsFormula__editorContent { + flex: 1; + min-height: 201px; +} + +.lnsFormula__warningText + .lnsFormula__warningText { + margin-top: $euiSizeS; + border-top: $euiBorderThin; + padding-top: $euiSizeS; +} + +.lnsFormula__editorHelp--inline { + align-items: center; + display: flex; + padding: $euiSizeXS; + + & > * + * { + margin-left: $euiSizeXS; + } +} + +.lnsFormula__editorError { + white-space: nowrap; +} + +.lnsFormula__docs { + background: $euiColorEmptyShade; +} + +.lnsFormula__docs--inline { + display: flex; + flex-direction: column; + // make sure docs are rendered in front of monaco + z-index: 1; +} + +.lnsFormula__docsContent { + .lnsFormula__docs--overlay & { + height: 40vh; + width: #{'min(75vh, 90vw)'}; + } + + .lnsFormula__docs--inline & { + flex: 1; + min-height: 0; + } + + & > * + * { + border-left: $euiBorderThin; + } +} + +.lnsFormula__docsSidebar { + background: $euiColorLightestShade; +} + +.lnsFormula__docsSidebarInner { + min-height: 0; + + & > * + * { + border-top: $euiBorderThin; + } +} + +.lnsFormula__docsSearch { + padding: $euiSizeS; +} + +.lnsFormula__docsNav { + @include euiYScroll; +} + +.lnsFormula__docsNavGroup { + padding: $euiSizeS; + + & + & { + border-top: $euiBorderThin; + } +} + +.lnsFormula__docsNavGroupLink { + font-weight: inherit; +} + +.lnsFormula__docsText { + @include euiYScroll; + padding: $euiSize; +} + +.lnsFormula__docsTextGroup, +.lnsFormula__docsTextItem { + margin-top: $euiSizeXXL; +} + +.lnsFormula__docsTextGroup { + border-top: $euiBorderThin; + padding-top: $euiSizeXXL; +} + +.lnsFormulaOverflow { + // Needs to be higher than the modal and all flyouts + z-index: $euiZLevel9 + 1; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx new file mode 100644 index 00000000000000..312ceb116dcede --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -0,0 +1,791 @@ +/* + * Copyright 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, useEffect, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiPopover, + EuiText, + EuiToolTip, + EuiSpacer, +} from '@elastic/eui'; +import useUnmount from 'react-use/lib/useUnmount'; +import { monaco } from '@kbn/monaco'; +import classNames from 'classnames'; +import { CodeEditor } from '../../../../../../../../../src/plugins/kibana_react/public'; +import type { CodeEditorProps } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useDebounceWithOptions } from '../../../../../shared_components'; +import { ParamEditorProps } from '../../index'; +import { getManagedColumnsFrom } from '../../../layer_helpers'; +import { ErrorWrapper, runASTValidation, tryToParse } from '../validation'; +import { + LensMathSuggestion, + SUGGESTION_TYPE, + suggest, + getSuggestion, + getSignatureHelp, + getHover, + getTokenInfo, + offsetToRowColumn, + monacoPositionToOffset, +} from './math_completion'; +import { LANGUAGE_ID } from './math_tokenization'; +import { MemoizedFormulaHelp } from './formula_help'; +import { trackUiEvent } from '../../../../../lens_ui_telemetry'; + +import './formula.scss'; +import { FormulaIndexPatternColumn } from '../formula'; +import { regenerateLayerFromAst } from '../parse'; +import { filterByVisibleOperation } from '../util'; + +export const MemoizedFormulaEditor = React.memo(FormulaEditor); + +export function FormulaEditor({ + layer, + updateLayer, + currentColumn, + columnId, + indexPattern, + operationDefinitionMap, + data, + toggleFullscreen, + isFullscreen, + setIsCloseable, +}: ParamEditorProps) { + const [text, setText] = useState(currentColumn.params.formula); + const [warnings, setWarnings] = useState< + Array<{ severity: monaco.MarkerSeverity; message: string }> + >([]); + const [isHelpOpen, setIsHelpOpen] = useState(isFullscreen); + const [isWarningOpen, setIsWarningOpen] = useState(false); + const [isWordWrapped, toggleWordWrap] = useState(true); + const editorModel = React.useRef(); + const overflowDiv1 = React.useRef(); + const disposables = React.useRef([]); + const editor1 = React.useRef(); + + const visibleOperationsMap = useMemo(() => filterByVisibleOperation(operationDefinitionMap), [ + operationDefinitionMap, + ]); + + // The Monaco editor needs to have the overflowDiv in the first render. Using an effect + // requires a second render to work, so we are using an if statement to guarantee it happens + // on first render + if (!overflowDiv1?.current) { + const node1 = (overflowDiv1.current = document.createElement('div')); + node1.setAttribute('data-test-subj', 'lnsFormulaWidget'); + // Monaco CSS is targeted on the monaco-editor class + node1.classList.add('lnsFormulaOverflow', 'monaco-editor'); + document.body.appendChild(node1); + } + + // Clean up the monaco editor and DOM on unmount + useEffect(() => { + const model = editorModel; + const allDisposables = disposables; + const editor1ref = editor1; + return () => { + model.current?.dispose(); + overflowDiv1.current?.parentNode?.removeChild(overflowDiv1.current); + editor1ref.current?.dispose(); + allDisposables.current?.forEach((d) => d.dispose()); + }; + }, []); + + useUnmount(() => { + setIsCloseable(true); + // If the text is not synced, update the column. + if (text !== currentColumn.params.formula) { + updateLayer((prevLayer) => { + return regenerateLayerFromAst( + text || '', + prevLayer, + columnId, + currentColumn, + indexPattern, + operationDefinitionMap + ).newLayer; + }); + } + }); + + useDebounceWithOptions( + () => { + if (!editorModel.current) return; + + if (!text) { + setWarnings([]); + monaco.editor.setModelMarkers(editorModel.current, 'LENS', []); + if (currentColumn.params.formula) { + // Only submit if valid + const { newLayer } = regenerateLayerFromAst( + text || '', + layer, + columnId, + currentColumn, + indexPattern, + operationDefinitionMap + ); + updateLayer(newLayer); + } + + return; + } + + let errors: ErrorWrapper[] = []; + + const { root, error } = tryToParse(text, visibleOperationsMap); + if (error) { + errors = [error]; + } else if (root) { + const validationErrors = runASTValidation(root, layer, indexPattern, visibleOperationsMap); + if (validationErrors.length) { + errors = validationErrors; + } + } + + if (errors.length) { + if (currentColumn.params.isFormulaBroken) { + // If the formula is already broken, show the latest error message in the workspace + if (currentColumn.params.formula !== text) { + updateLayer( + regenerateLayerFromAst( + text || '', + layer, + columnId, + currentColumn, + indexPattern, + visibleOperationsMap + ).newLayer + ); + } + } + + const markers = errors.flatMap((innerError) => { + if (innerError.locations.length) { + return innerError.locations.map((location) => { + const startPosition = offsetToRowColumn(text, location.min); + const endPosition = offsetToRowColumn(text, location.max); + return { + message: innerError.message, + startColumn: startPosition.column + 1, + startLineNumber: startPosition.lineNumber, + endColumn: endPosition.column + 1, + endLineNumber: endPosition.lineNumber, + severity: + innerError.severity === 'warning' + ? monaco.MarkerSeverity.Warning + : monaco.MarkerSeverity.Error, + }; + }); + } else { + // Parse errors return no location info + const startPosition = offsetToRowColumn(text, 0); + const endPosition = offsetToRowColumn(text, text.length - 1); + return [ + { + message: innerError.message, + startColumn: startPosition.column + 1, + startLineNumber: startPosition.lineNumber, + endColumn: endPosition.column + 1, + endLineNumber: endPosition.lineNumber, + severity: + innerError.severity === 'warning' + ? monaco.MarkerSeverity.Warning + : monaco.MarkerSeverity.Error, + }, + ]; + } + }); + + monaco.editor.setModelMarkers(editorModel.current, 'LENS', markers); + setWarnings(markers.map(({ severity, message }) => ({ severity, message }))); + } else { + monaco.editor.setModelMarkers(editorModel.current, 'LENS', []); + + // Only submit if valid + const { newLayer, locations } = regenerateLayerFromAst( + text || '', + layer, + columnId, + currentColumn, + indexPattern, + visibleOperationsMap + ); + updateLayer(newLayer); + + const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns); + const markers: monaco.editor.IMarkerData[] = managedColumns + .flatMap(([id, column]) => { + if (locations[id]) { + const def = visibleOperationsMap[column.operationType]; + if (def.getErrorMessage) { + const messages = def.getErrorMessage( + newLayer, + id, + indexPattern, + visibleOperationsMap + ); + if (messages) { + const startPosition = offsetToRowColumn(text, locations[id].min); + const endPosition = offsetToRowColumn(text, locations[id].max); + return [ + { + message: messages.join(', '), + startColumn: startPosition.column + 1, + startLineNumber: startPosition.lineNumber, + endColumn: endPosition.column + 1, + endLineNumber: endPosition.lineNumber, + severity: monaco.MarkerSeverity.Warning, + }, + ]; + } + } + } + return []; + }) + .filter((marker) => marker); + setWarnings(markers.map(({ severity, message }) => ({ severity, message }))); + monaco.editor.setModelMarkers(editorModel.current, 'LENS', markers); + } + }, + // Make it validate on flyout open in case of a broken formula left over + // from a previous edit + { skipFirstRender: false }, + 256, + [text] + ); + + const errorCount = warnings.filter((marker) => marker.severity === monaco.MarkerSeverity.Error) + .length; + const warningCount = warnings.filter( + (marker) => marker.severity === monaco.MarkerSeverity.Warning + ).length; + + /** + * The way that Monaco requests autocompletion is not intuitive, but the way we use it + * we fetch new suggestions in these scenarios: + * + * - If the user types one of the trigger characters, suggestions are always fetched + * - When the user selects the kql= suggestion, we tell Monaco to trigger new suggestions after + * - When the user types the first character into an empty text box, Monaco requests suggestions + * + * Monaco also triggers suggestions automatically when there are no suggestions being displayed + * and the user types a non-whitespace character. + * + * While suggestions are being displayed, Monaco uses an in-memory cache of the last known suggestions. + */ + const provideCompletionItems = useCallback( + async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + context: monaco.languages.CompletionContext + ) => { + const innerText = model.getValue(); + let aSuggestions: { list: LensMathSuggestion[]; type: SUGGESTION_TYPE } = { + list: [], + type: SUGGESTION_TYPE.FIELD, + }; + const offset = monacoPositionToOffset(innerText, position); + + if (context.triggerCharacter === '(') { + // Monaco usually inserts the end quote and reports the position is after the end quote + if (innerText.slice(offset - 1, offset + 1) === '()') { + position = position.delta(0, -1); + } + const wordUntil = model.getWordAtPosition(position.delta(0, -3)); + if (wordUntil) { + // Retrieve suggestions for subexpressions + aSuggestions = await suggest({ + expression: innerText, + zeroIndexedOffset: offset, + context, + indexPattern, + operationDefinitionMap: visibleOperationsMap, + data, + }); + } + } else { + aSuggestions = await suggest({ + expression: innerText, + zeroIndexedOffset: offset, + context, + indexPattern, + operationDefinitionMap: visibleOperationsMap, + data, + }); + } + + return { + suggestions: aSuggestions.list.map((s) => + getSuggestion(s, aSuggestions.type, visibleOperationsMap, context.triggerCharacter) + ), + }; + }, + [indexPattern, visibleOperationsMap, data] + ); + + const provideSignatureHelp = useCallback( + async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken, + context: monaco.languages.SignatureHelpContext + ) => { + const innerText = model.getValue(); + const textRange = model.getFullModelRange(); + + const lengthAfterPosition = model.getValueLengthInRange({ + startLineNumber: position.lineNumber, + startColumn: position.column, + endLineNumber: textRange.endLineNumber, + endColumn: textRange.endColumn, + }); + return getSignatureHelp( + model.getValue(), + innerText.length - lengthAfterPosition, + visibleOperationsMap + ); + }, + [visibleOperationsMap] + ); + + const provideHover = useCallback( + async ( + model: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken + ) => { + const innerText = model.getValue(); + const textRange = model.getFullModelRange(); + + const lengthAfterPosition = model.getValueLengthInRange({ + startLineNumber: position.lineNumber, + startColumn: position.column, + endLineNumber: textRange.endLineNumber, + endColumn: textRange.endColumn, + }); + return getHover( + model.getValue(), + innerText.length - lengthAfterPosition, + visibleOperationsMap + ); + }, + [visibleOperationsMap] + ); + + const onTypeHandler = useCallback( + (e: monaco.editor.IModelContentChangedEvent, editor: monaco.editor.IStandaloneCodeEditor) => { + if (e.isFlush || e.isRedoing || e.isUndoing) { + return; + } + if (e.changes.length === 1) { + const char = e.changes[0].text; + if (char !== '=' && char !== "'") { + return; + } + const currentPosition = e.changes[0].range; + if (currentPosition) { + const currentText = editor.getValue(); + const offset = monacoPositionToOffset( + currentText, + new monaco.Position(currentPosition.startLineNumber, currentPosition.startColumn) + ); + let tokenInfo = getTokenInfo(currentText, offset + 1); + + if (!tokenInfo && char === "'") { + // try again this time replacing the current quote with an escaped quote + const line = currentText; + const lineEscaped = line.substring(0, offset) + "\\'" + line.substring(offset + 1); + tokenInfo = getTokenInfo(lineEscaped, offset + 2); + } + + const isSingleQuoteCase = /'LENS_MATH_MARKER/; + // Make sure that we are only adding kql='' or lucene='', and also + // check that the = sign isn't inside the KQL expression like kql='=' + if ( + !tokenInfo || + typeof tokenInfo.ast === 'number' || + tokenInfo.ast.type !== 'namedArgument' || + (tokenInfo.ast.name !== 'kql' && tokenInfo.ast.name !== 'lucene') || + (tokenInfo.ast.value !== 'LENS_MATH_MARKER' && + !isSingleQuoteCase.test(tokenInfo.ast.value)) + ) { + return; + } + + let editOperation: monaco.editor.IIdentifiedSingleEditOperation | null = null; + const cursorOffset = 2; + if (char === '=') { + editOperation = { + range: { + ...currentPosition, + // Insert after the current char + startColumn: currentPosition.startColumn + 1, + endColumn: currentPosition.startColumn + 1, + }, + text: `''`, + }; + } + if (char === "'") { + editOperation = { + range: { + ...currentPosition, + // Insert after the current char + startColumn: currentPosition.startColumn, + endColumn: currentPosition.startColumn + 1, + }, + text: `\\'`, + }; + } + + if (editOperation) { + setTimeout(() => { + editor.executeEdits( + 'LENS', + [editOperation!], + [ + // After inserting, move the cursor in between the single quotes or after the escaped quote + new monaco.Selection( + currentPosition.startLineNumber, + currentPosition.startColumn + cursorOffset, + currentPosition.startLineNumber, + currentPosition.startColumn + cursorOffset + ), + ] + ); + + // Need to move these sync to prevent race conditions between a fast user typing a single quote + // after an = char + // Timeout is required because otherwise the cursor position is not updated. + editor.setPosition({ + column: currentPosition.startColumn + cursorOffset, + lineNumber: currentPosition.startLineNumber, + }); + editor.trigger('lens', 'editor.action.triggerSuggest', {}); + }, 0); + } + } + } + }, + [] + ); + + const codeEditorOptions: CodeEditorProps = { + languageId: LANGUAGE_ID, + value: text ?? '', + onChange: setText, + options: { + automaticLayout: false, + fontSize: 14, + folding: false, + lineNumbers: 'off', + scrollBeyondLastLine: false, + minimap: { enabled: false }, + wordWrap: isWordWrapped ? 'on' : 'off', + // Disable suggestions that appear when we don't provide a default suggestion + wordBasedSuggestions: false, + autoIndent: 'brackets', + wrappingIndent: 'none', + dimension: { width: 320, height: 200 }, + fixedOverflowWidgets: true, + matchBrackets: 'always', + }, + }; + + useEffect(() => { + // Because the monaco model is owned by Lens, we need to manually attach and remove handlers + const { dispose: dispose1 } = monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, { + triggerCharacters: ['.', '(', '=', ' ', ':', `'`], + provideCompletionItems, + }); + const { dispose: dispose2 } = monaco.languages.registerSignatureHelpProvider(LANGUAGE_ID, { + signatureHelpTriggerCharacters: ['(', '='], + provideSignatureHelp, + }); + const { dispose: dispose3 } = monaco.languages.registerHoverProvider(LANGUAGE_ID, { + provideHover, + }); + return () => { + dispose1(); + dispose2(); + dispose3(); + }; + }, [provideCompletionItems, provideSignatureHelp, provideHover]); + + // The Monaco editor will lazily load Monaco, which takes a render cycle to trigger. This can cause differences + // in the behavior of Monaco when it's first loaded and then reloaded. + return ( +
+
+
+
+
+ + + {/* TODO: Replace `bolt` with `wordWrap` icon (after latest EUI is deployed) and hook up button to enable/disable word wrapping. */} + + { + editor1.current?.updateOptions({ + wordWrap: isWordWrapped ? 'off' : 'on', + }); + toggleWordWrap(!isWordWrapped); + }} + /> + + + + + {/* TODO: Replace `bolt` with `fullScreenExit` icon (after latest EUI is deployed). */} + { + toggleFullscreen(); + // Help text opens when entering full screen, and closes when leaving full screen + setIsHelpOpen(!isFullscreen); + trackUiEvent('toggle_formula_fullscreen'); + }} + iconType={isFullscreen ? 'bolt' : 'fullScreen'} + size="xs" + color="text" + flush="right" + data-test-subj="lnsFormula-fullscreen" + > + {isFullscreen + ? i18n.translate('xpack.lens.formula.fullScreenExitLabel', { + defaultMessage: 'Collapse', + }) + : i18n.translate('xpack.lens.formula.fullScreenEnterLabel', { + defaultMessage: 'Expand', + })} + + + +
+ +
+ { + editor1.current = editor; + const model = editor.getModel(); + if (model) { + editorModel.current = model; + } + disposables.current.push( + editor.onDidFocusEditorWidget(() => { + setTimeout(() => { + setIsCloseable(false); + }); + }) + ); + disposables.current.push( + editor.onDidBlurEditorWidget(() => { + setTimeout(() => { + setIsCloseable(true); + }); + }) + ); + // If we ever introduce a second Monaco editor, we need to toggle + // the typing handler to the active editor to maintain the cursor + disposables.current.push( + editor.onDidChangeModelContent((e) => { + onTypeHandler(e, editor); + }) + ); + }} + /> + + {!text ? ( +
+ + {i18n.translate('xpack.lens.formulaPlaceholderText', { + defaultMessage: 'Type a formula by combining functions with math, like:', + })} + + +
count() + 1
+
+ ) : null} +
+ +
+ + + {isFullscreen ? ( + + setIsHelpOpen(!isHelpOpen)} + > + + + + + ) : ( + + setIsHelpOpen(false)} + ownFocus={false} + button={ + setIsHelpOpen(!isHelpOpen)} + iconType="help" + color="text" + size="s" + aria-label={i18n.translate( + 'xpack.lens.formula.editorHelpInlineShowToolTip', + { + defaultMessage: 'Show function reference', + } + )} + /> + } + > + + + + )} + + + {errorCount || warningCount ? ( + + setIsWarningOpen(false)} + button={ + { + setIsWarningOpen(!isWarningOpen); + }} + > + {errorCount + ? i18n.translate('xpack.lens.formulaErrorCount', { + defaultMessage: + '{count} {count, plural, one {error} other {errors}}', + values: { count: errorCount }, + }) + : null} + {warningCount + ? i18n.translate('xpack.lens.formulaWarningCount', { + defaultMessage: + '{count} {count, plural, one {warning} other {warnings}}', + values: { count: warningCount }, + }) + : null} + + } + > + {warnings.map(({ message, severity }, index) => ( +
+ + {message} + +
+ ))} +
+
+ ) : null} +
+
+
+ + {isFullscreen && isHelpOpen ? ( +
+ +
+ ) : null} +
+
+
+ ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx new file mode 100644 index 00000000000000..afe5471666b22b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx @@ -0,0 +1,469 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPopoverTitle, + EuiText, + EuiListGroupItem, + EuiListGroup, + EuiTitle, + EuiFieldSearch, + EuiHighlight, +} from '@elastic/eui'; +import { Markdown } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { IndexPattern } from '../../../../types'; +import { tinymathFunctions } from '../util'; +import { getPossibleFunctions } from './math_completion'; +import { hasFunctionFieldArgument } from '../validation'; + +import type { + GenericOperationDefinition, + IndexPatternColumn, + OperationDefinition, + ParamEditorProps, +} from '../../index'; +import type { FormulaIndexPatternColumn } from '../formula'; + +function FormulaHelp({ + indexPattern, + operationDefinitionMap, + isFullscreen, +}: { + indexPattern: IndexPattern; + operationDefinitionMap: Record; + isFullscreen: boolean; +}) { + const [selectedFunction, setSelectedFunction] = useState(); + const scrollTargets = useRef>({}); + + useEffect(() => { + if (selectedFunction && scrollTargets.current[selectedFunction]) { + scrollTargets.current[selectedFunction].scrollIntoView(); + } + }, [selectedFunction]); + + const helpGroups: Array<{ + label: string; + description?: string; + items: Array<{ label: string; description?: JSX.Element }>; + }> = []; + + helpGroups.push({ + label: i18n.translate('xpack.lens.formulaDocumentationHeading', { + defaultMessage: 'How it works', + }), + items: [], + }); + + helpGroups.push({ + label: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSection', { + defaultMessage: 'Elasticsearch', + }), + description: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSectionDescription', { + defaultMessage: + 'These functions will be executed on the raw documents for each row of the resulting table, aggregating all documents matching the break down dimensions into a single value.', + }), + items: [], + }); + + const availableFunctions = getPossibleFunctions(indexPattern); + + // Es aggs + helpGroups[1].items.push( + ...availableFunctions + .filter( + (key) => + key in operationDefinitionMap && + operationDefinitionMap[key].documentation?.section === 'elasticsearch' + ) + .sort() + .map((key) => ({ + label: key, + description: ( + <> +

+ {key}({operationDefinitionMap[key].documentation?.signature}) +

+ + {operationDefinitionMap[key].documentation?.description ? ( + + ) : null} + + ), + })) + ); + + helpGroups.push({ + label: i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSection', { + defaultMessage: 'Column-wise calculation', + }), + description: i18n.translate( + 'xpack.lens.formulaDocumentation.columnCalculationSectionDescription', + { + defaultMessage: + 'These functions will be executed for reach row of the resulting table, using data from cells from other rows as well as the current value.', + } + ), + items: [], + }); + + // Calculations aggs + helpGroups[2].items.push( + ...availableFunctions + .filter( + (key) => + key in operationDefinitionMap && + operationDefinitionMap[key].documentation?.section === 'calculation' + ) + .sort() + .map((key) => ({ + label: key, + description: ( + <> +

+ {key}({operationDefinitionMap[key].documentation?.signature}) +

+ + {operationDefinitionMap[key].documentation?.description ? ( + + ) : null} + + ), + checked: + selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}` + ? ('on' as const) + : undefined, + })) + ); + + helpGroups.push({ + label: i18n.translate('xpack.lens.formulaDocumentation.mathSection', { + defaultMessage: 'Math', + }), + description: i18n.translate('xpack.lens.formulaDocumentation.mathSectionDescription', { + defaultMessage: + 'These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.', + }), + items: [], + }); + + const tinymathFns = useMemo(() => { + return getPossibleFunctions(indexPattern) + .filter((key) => key in tinymathFunctions) + .sort() + .map((key) => { + const [description, examples] = tinymathFunctions[key].help.split(`\`\`\``); + return { + label: key, + description: description.replace(/\n/g, '\n\n'), + examples: examples ? `\`\`\`${examples}\`\`\`` : '', + }; + }); + }, [indexPattern]); + + helpGroups[3].items.push( + ...tinymathFns.map(({ label, description, examples }) => { + return { + label, + description: ( + <> +

{getFunctionSignatureLabel(label, operationDefinitionMap)}

+ + + + ), + }; + }) + ); + + const [searchText, setSearchText] = useState(''); + + const normalizedSearchText = searchText.trim().toLocaleLowerCase(); + + const filteredHelpGroups = helpGroups + .map((group) => { + const items = group.items.filter((helpItem) => { + return ( + !normalizedSearchText || helpItem.label.toLocaleLowerCase().includes(normalizedSearchText) + ); + }); + return { ...group, items }; + }) + .filter((group) => { + if (group.items.length > 0 || !normalizedSearchText) { + return true; + } + return group.label.toLocaleLowerCase().includes(normalizedSearchText); + }); + + return ( + <> + + {i18n.translate('xpack.lens.formulaDocumentation.header', { + defaultMessage: 'Formula reference', + })} + + + + + + + { + setSearchText(e.target.value); + }} + placeholder={i18n.translate('xpack.lens.formulaSearchPlaceholder', { + defaultMessage: 'Search functions', + })} + /> + + + + {filteredHelpGroups.map((helpGroup, index) => { + return ( + + ); + })} + + + + + + +
{ + if (el) { + scrollTargets.current[helpGroups[0].label] = el; + } + }} + > + +
+ + {helpGroups.slice(1).map((helpGroup, index) => { + return ( +
{ + if (el) { + scrollTargets.current[helpGroup.label] = el; + } + }} + > +

{helpGroup.label}

+ +

{helpGroup.description}

+ + {helpGroups[index + 1].items.map((helpItem) => { + return ( +
{ + if (el) { + scrollTargets.current[helpItem.label] = el; + } + }} + > + {helpItem.description} +
+ ); + })} +
+ ); + })} +
+
+
+ + ); +} + +export const MemoizedFormulaHelp = React.memo(FormulaHelp); + +export function getFunctionSignatureLabel( + name: string, + operationDefinitionMap: ParamEditorProps['operationDefinitionMap'], + firstParam?: { label: string | [number, number] } | null +): string { + if (tinymathFunctions[name]) { + return `${name}(${tinymathFunctions[name].positionalArguments + .map(({ name: argName, optional, type }) => `[${argName}]${optional ? '?' : ''}: ${type}`) + .join(', ')})`; + } + if (operationDefinitionMap[name]) { + const def = operationDefinitionMap[name]; + let extraArgs = ''; + if (def.filterable) { + extraArgs += hasFunctionFieldArgument(name) || 'operationParams' in def ? ',' : ''; + extraArgs += i18n.translate('xpack.lens.formula.kqlExtraArguments', { + defaultMessage: '[kql]?: string, [lucene]?: string', + }); + } + return `${name}(${def.documentation?.signature}${extraArgs})`; + } + return ''; +} + +function getFunctionArgumentsStringified( + params: Required< + OperationDefinition + >['operationParams'] +) { + return params + .map( + ({ name, type: argType, defaultValue = 5 }) => + `${name}=${argType === 'string' ? `"${defaultValue}"` : defaultValue}` + ) + .join(', '); +} + +/** + * Get an array of strings containing all possible information about a specific + * operation type: examples and infos. + */ +export function getHelpTextContent( + type: string, + operationDefinitionMap: ParamEditorProps['operationDefinitionMap'] +): { description: string; examples: string[] } { + const definition = operationDefinitionMap[type]; + const description = definition.documentation?.description ?? ''; + + // as for the time being just add examples text. + // Later will enrich with more information taken from the operation definitions. + const examples: string[] = []; + // If the description already contain examples skip it + if (!/Example/.test(description)) { + if (!hasFunctionFieldArgument(type)) { + // ideally this should have the same example automation as the operations below + examples.push(`${type}()`); + return { description, examples }; + } + if (definition.input === 'field') { + const mandatoryArgs = definition.operationParams?.filter(({ required }) => required) || []; + if (mandatoryArgs.length === 0) { + examples.push(`${type}(bytes)`); + } + if (mandatoryArgs.length) { + const additionalArgs = getFunctionArgumentsStringified(mandatoryArgs); + examples.push(`${type}(bytes, ${additionalArgs})`); + } + if ( + definition.operationParams && + mandatoryArgs.length !== definition.operationParams.length + ) { + const additionalArgs = getFunctionArgumentsStringified(definition.operationParams); + examples.push(`${type}(bytes, ${additionalArgs})`); + } + } + if (definition.input === 'fullReference') { + const mandatoryArgs = definition.operationParams?.filter(({ required }) => required) || []; + if (mandatoryArgs.length === 0) { + examples.push(`${type}(sum(bytes))`); + } + if (mandatoryArgs.length) { + const additionalArgs = getFunctionArgumentsStringified(mandatoryArgs); + examples.push(`${type}(sum(bytes), ${additionalArgs})`); + } + if ( + definition.operationParams && + mandatoryArgs.length !== definition.operationParams.length + ) { + const additionalArgs = getFunctionArgumentsStringified(definition.operationParams); + examples.push(`${type}(sum(bytes), ${additionalArgs})`); + } + } + } + return { description, examples }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts new file mode 100644 index 00000000000000..4b6acefa6b30ad --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './formula_editor'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts new file mode 100644 index 00000000000000..9cd748f5759c98 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts @@ -0,0 +1,386 @@ +/* + * Copyright 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 { parse } from '@kbn/tinymath'; +import { monaco } from '@kbn/monaco'; +import { createMockedIndexPattern } from '../../../../mocks'; +import { GenericOperationDefinition } from '../../index'; +import type { IndexPatternField } from '../../../../types'; +import type { OperationMetadata } from '../../../../../types'; +import { dataPluginMock } from '../../../../../../../../../src/plugins/data/public/mocks'; +import { tinymathFunctions } from '../util'; +import { + getSignatureHelp, + getHover, + suggest, + monacoPositionToOffset, + getInfoAtZeroIndexedPosition, +} from './math_completion'; + +const buildGenericColumn = (type: string) => { + return ({ field }: { field?: IndexPatternField }) => { + return { + label: type, + dataType: 'number', + operationType: type, + sourceField: field?.name ?? undefined, + isBucketed: false, + scale: 'ratio', + timeScale: false, + }; + }; +}; + +const numericOperation = () => ({ dataType: 'number', isBucketed: false }); +const stringOperation = () => ({ dataType: 'string', isBucketed: true }); + +// Only one of each type is needed +const operationDefinitionMap: Record = { + sum: ({ + type: 'sum', + input: 'field', + buildColumn: buildGenericColumn('sum'), + getPossibleOperationForField: (field: IndexPatternField) => + field.type === 'number' ? numericOperation() : null, + documentation: { + section: 'elasticsearch', + signature: 'field: string', + description: 'description', + }, + } as unknown) as GenericOperationDefinition, + count: ({ + type: 'count', + input: 'field', + buildColumn: buildGenericColumn('count'), + getPossibleOperationForField: (field: IndexPatternField) => + field.name === 'Records' ? numericOperation() : null, + } as unknown) as GenericOperationDefinition, + last_value: ({ + type: 'last_value', + input: 'field', + buildColumn: buildGenericColumn('last_value'), + getPossibleOperationForField: (field: IndexPatternField) => ({ + dataType: field.type, + isBucketed: false, + }), + } as unknown) as GenericOperationDefinition, + moving_average: ({ + type: 'moving_average', + input: 'fullReference', + requiredReferences: [ + { + input: ['field', 'managedReference'], + validateMetadata: (meta: OperationMetadata) => + meta.dataType === 'number' && !meta.isBucketed, + }, + ], + operationParams: [{ name: 'window', type: 'number', required: true }], + buildColumn: buildGenericColumn('moving_average'), + getPossibleOperation: numericOperation, + } as unknown) as GenericOperationDefinition, + cumulative_sum: ({ + type: 'cumulative_sum', + input: 'fullReference', + buildColumn: buildGenericColumn('cumulative_sum'), + getPossibleOperation: numericOperation, + } as unknown) as GenericOperationDefinition, + terms: ({ + type: 'terms', + input: 'field', + getPossibleOperationForField: stringOperation, + } as unknown) as GenericOperationDefinition, +}; + +describe('math completion', () => { + describe('signature help', () => { + function unwrapSignatures(signatureResult: monaco.languages.SignatureHelpResult) { + return signatureResult.value.signatures[0]; + } + + it('should silently handle parse errors', () => { + expect(unwrapSignatures(getSignatureHelp('sum(', 4, operationDefinitionMap))).toBeUndefined(); + }); + + it('should return a signature for a field-based ES function', () => { + expect(unwrapSignatures(getSignatureHelp('sum()', 4, operationDefinitionMap))).toEqual({ + label: 'sum(field: string)', + documentation: { value: 'description' }, + parameters: [{ label: 'field' }], + }); + }); + + it('should return a signature for count', () => { + expect(unwrapSignatures(getSignatureHelp('count()', 6, operationDefinitionMap))).toEqual({ + label: 'count(undefined)', + documentation: { value: '' }, + parameters: [], + }); + }); + + it('should return a signature for a function with named parameters', () => { + expect( + unwrapSignatures( + getSignatureHelp('2 * moving_average(count(), window=)', 35, operationDefinitionMap) + ) + ).toEqual({ + label: expect.stringContaining('moving_average('), + documentation: { value: '' }, + parameters: [ + { label: 'function' }, + { + label: 'window=number', + documentation: 'Required', + }, + ], + }); + }); + + it('should return a signature for an inner function', () => { + expect( + unwrapSignatures( + getSignatureHelp('2 * moving_average(count())', 25, operationDefinitionMap) + ) + ).toEqual({ + label: expect.stringContaining('count('), + parameters: [], + documentation: { value: '' }, + }); + }); + + it('should return a signature for a complex tinymath function', () => { + // 15 is the whitespace between the two arguments + expect( + unwrapSignatures(getSignatureHelp('clamp(count(), 5)', 15, operationDefinitionMap)) + ).toEqual({ + label: expect.stringContaining('clamp('), + documentation: { value: '' }, + parameters: [ + { label: 'value', documentation: '' }, + { label: 'min', documentation: '' }, + { label: 'max', documentation: '' }, + ], + }); + }); + }); + + describe('hover provider', () => { + it('should silently handle parse errors', () => { + expect(getHover('sum(', 2, operationDefinitionMap)).toEqual({ contents: [] }); + }); + + it('should show signature for a field-based ES function', () => { + expect(getHover('sum()', 2, operationDefinitionMap)).toEqual({ + contents: [{ value: 'sum(field: string)' }], + }); + }); + + it('should show signature for count', () => { + expect(getHover('count()', 2, operationDefinitionMap)).toEqual({ + contents: [{ value: expect.stringContaining('count(') }], + }); + }); + + it('should show signature for a function with named parameters', () => { + expect(getHover('2 * moving_average(count())', 10, operationDefinitionMap)).toEqual({ + contents: [{ value: expect.stringContaining('moving_average(') }], + }); + }); + + it('should show signature for an inner function', () => { + expect(getHover('2 * moving_average(count())', 22, operationDefinitionMap)).toEqual({ + contents: [{ value: expect.stringContaining('count(') }], + }); + }); + + it('should show signature for a complex tinymath function', () => { + expect(getHover('clamp(count(), 5)', 2, operationDefinitionMap)).toEqual({ + contents: [{ value: expect.stringContaining('clamp([value]: number') }], + }); + }); + }); + + describe('autocomplete', () => { + it('should list all valid functions at the top level (fake test)', async () => { + // This test forces an invalid scenario, since the autocomplete actually requires + // some typing + const results = await suggest({ + expression: '', + zeroIndexedOffset: 1, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: '', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); + ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'operation' }])); + }); + Object.keys(tinymathFunctions).forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'operation' }])); + }); + }); + + it('should list all valid sub-functions for a fullReference', async () => { + const results = await suggest({ + expression: 'moving_average()', + zeroIndexedOffset: 15, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: '(', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toHaveLength(2); + ['sum', 'last_value'].forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'operation' }])); + }); + }); + + it('should list all valid named arguments for a fullReference', async () => { + const results = await suggest({ + expression: 'moving_average(count(),)', + zeroIndexedOffset: 23, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: ',', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toEqual(['window']); + }); + + it('should not list named arguments when they are already in use', async () => { + const results = await suggest({ + expression: 'moving_average(count(), window=5, )', + zeroIndexedOffset: 34, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: ',', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toEqual([]); + }); + + it('should list all valid positional arguments for a tinymath function used by name', async () => { + const results = await suggest({ + expression: 'divide(count(), )', + zeroIndexedOffset: 16, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: ',', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); + ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'math' }])); + }); + Object.keys(tinymathFunctions).forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'math' }])); + }); + }); + + it('should list all valid positional arguments for a tinymath function used with alias', async () => { + const results = await suggest({ + expression: 'count() / ', + zeroIndexedOffset: 10, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: ',', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); + ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'math' }])); + }); + Object.keys(tinymathFunctions).forEach((key) => { + expect(results.list).toEqual(expect.arrayContaining([{ label: key, type: 'math' }])); + }); + }); + + it('should not autocomplete any fields for the count function', async () => { + const results = await suggest({ + expression: 'count()', + zeroIndexedOffset: 6, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: '(', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toHaveLength(0); + }); + + it('should autocomplete and validate the right type of field', async () => { + const results = await suggest({ + expression: 'sum()', + zeroIndexedOffset: 4, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: '(', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toEqual(['bytes', 'memory']); + }); + + it('should autocomplete only operations that provide numeric output', async () => { + const results = await suggest({ + expression: 'last_value()', + zeroIndexedOffset: 11, + context: { + triggerKind: monaco.languages.CompletionTriggerKind.TriggerCharacter, + triggerCharacter: '(', + }, + indexPattern: createMockedIndexPattern(), + operationDefinitionMap, + data: dataPluginMock.createStartContract(), + }); + expect(results.list).toEqual(['bytes', 'memory']); + }); + }); + + describe('monacoPositionToOffset', () => { + it('should work with multi-line strings accounting for newline characters', () => { + const input = `012 +456 +89')`; + expect(input[monacoPositionToOffset(input, new monaco.Position(1, 1))]).toEqual('0'); + expect(input[monacoPositionToOffset(input, new monaco.Position(3, 2))]).toEqual('9'); + }); + }); + + describe('getInfoAtZeroIndexedPosition', () => { + it('should return the location for a function inside multiple levels of math', () => { + const expression = `count() + 5 + average(LENS_MATH_MARKER)`; + const ast = parse(expression); + expect(getInfoAtZeroIndexedPosition(ast, 22)).toEqual({ + ast: expect.objectContaining({ value: 'LENS_MATH_MARKER' }), + parent: expect.objectContaining({ name: 'average' }), + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts new file mode 100644 index 00000000000000..df747e532b38a0 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -0,0 +1,594 @@ +/* + * Copyright 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 { uniq, startsWith } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { monaco } from '@kbn/monaco'; +import { + parse, + TinymathLocation, + TinymathAST, + TinymathFunction, + TinymathNamedArgument, +} from '@kbn/tinymath'; +import type { + DataPublicPluginStart, + QuerySuggestion, +} from '../../../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../types'; +import { memoizedGetAvailableOperationsByMetadata } from '../../../operations'; +import { tinymathFunctions, groupArgsByType } from '../util'; +import type { GenericOperationDefinition } from '../..'; +import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help'; +import { hasFunctionFieldArgument } from '../validation'; + +export enum SUGGESTION_TYPE { + FIELD = 'field', + NAMED_ARGUMENT = 'named_argument', + FUNCTIONS = 'functions', + KQL = 'kql', +} + +export type LensMathSuggestion = + | string + | { + label: string; + type: 'operation' | 'math'; + } + | QuerySuggestion; + +export interface LensMathSuggestions { + list: LensMathSuggestion[]; + type: SUGGESTION_TYPE; +} + +function inLocation(cursorPosition: number, location: TinymathLocation) { + return cursorPosition >= location.min && cursorPosition < location.max; +} + +const MARKER = 'LENS_MATH_MARKER'; + +export function getInfoAtZeroIndexedPosition( + ast: TinymathAST, + zeroIndexedPosition: number, + parent?: TinymathFunction +): undefined | { ast: TinymathAST; parent?: TinymathFunction } { + if (typeof ast === 'number') { + return; + } + // +, -, *, and / do not have location any more + if (ast.location && !inLocation(zeroIndexedPosition, ast.location)) { + return; + } + if (ast.type === 'function') { + const [match] = ast.args + .map((arg) => getInfoAtZeroIndexedPosition(arg, zeroIndexedPosition, ast)) + .filter((a) => a); + if (match) { + return match; + } else if (ast.location) { + return { ast }; + } else { + // None of the arguments match, but we don't know the position so it's not a match + return; + } + } + return { + ast, + parent, + }; +} + +export function offsetToRowColumn(expression: string, offset: number): monaco.Position { + const lines = expression.split(/\n/); + let remainingChars = offset; + let lineNumber = 1; + for (const line of lines) { + if (line.length >= remainingChars) { + return new monaco.Position(lineNumber, remainingChars); + } + remainingChars -= line.length + 1; + lineNumber++; + } + + throw new Error('Algorithm failure'); +} + +export function monacoPositionToOffset(expression: string, position: monaco.Position): number { + const lines = expression.split(/\n/); + return lines + .slice(0, position.lineNumber) + .reduce( + (prev, current, index) => + prev + (index === position.lineNumber - 1 ? position.column - 1 : current.length + 1), + 0 + ); +} + +export async function suggest({ + expression, + zeroIndexedOffset, + context, + indexPattern, + operationDefinitionMap, + data, +}: { + expression: string; + zeroIndexedOffset: number; + context: monaco.languages.CompletionContext; + indexPattern: IndexPattern; + operationDefinitionMap: Record; + data: DataPublicPluginStart; +}): Promise<{ list: LensMathSuggestion[]; type: SUGGESTION_TYPE }> { + const text = + expression.substr(0, zeroIndexedOffset) + MARKER + expression.substr(zeroIndexedOffset); + try { + const ast = parse(text); + + const tokenInfo = getInfoAtZeroIndexedPosition(ast, zeroIndexedOffset); + const tokenAst = tokenInfo?.ast; + + const isNamedArgument = + tokenInfo?.parent && + typeof tokenAst !== 'number' && + tokenAst && + 'type' in tokenAst && + tokenAst.type === 'namedArgument'; + if (tokenInfo?.parent && (context.triggerCharacter === '=' || isNamedArgument)) { + return await getNamedArgumentSuggestions({ + ast: tokenAst as TinymathNamedArgument, + data, + indexPattern, + }); + } else if (tokenInfo?.parent) { + return getArgumentSuggestions( + tokenInfo.parent, + tokenInfo.parent.args.findIndex((a) => a === tokenAst), + indexPattern, + operationDefinitionMap + ); + } + if ( + typeof tokenAst === 'object' && + Boolean(tokenAst.type === 'variable' || tokenAst.type === 'function') + ) { + const nameWithMarker = tokenAst.type === 'function' ? tokenAst.name : tokenAst.value; + return getFunctionSuggestions( + nameWithMarker.split(MARKER)[0], + indexPattern, + operationDefinitionMap + ); + } + } catch (e) { + // Fail silently + } + return { list: [], type: SUGGESTION_TYPE.FIELD }; +} + +export function getPossibleFunctions( + indexPattern: IndexPattern, + operationDefinitionMap?: Record +) { + const available = memoizedGetAvailableOperationsByMetadata(indexPattern, operationDefinitionMap); + const possibleOperationNames: string[] = []; + available.forEach((a) => { + if (a.operationMetaData.dataType === 'number' && !a.operationMetaData.isBucketed) { + possibleOperationNames.push( + ...a.operations.filter((o) => o.type !== 'managedReference').map((o) => o.operationType) + ); + } + }); + + return [...uniq(possibleOperationNames), ...Object.keys(tinymathFunctions)]; +} + +function getFunctionSuggestions( + prefix: string, + indexPattern: IndexPattern, + operationDefinitionMap: Record +) { + return { + list: uniq( + getPossibleFunctions(indexPattern, operationDefinitionMap).filter((func) => + startsWith(func, prefix) + ) + ).map((func) => ({ label: func, type: 'operation' as const })), + type: SUGGESTION_TYPE.FUNCTIONS, + }; +} + +function getArgumentSuggestions( + ast: TinymathFunction, + position: number, + indexPattern: IndexPattern, + operationDefinitionMap: Record +) { + const { name } = ast; + const operation = operationDefinitionMap[name]; + if (!operation && !tinymathFunctions[name]) { + return { list: [], type: SUGGESTION_TYPE.FIELD }; + } + + const tinymathFunction = tinymathFunctions[name]; + if (tinymathFunction) { + if (tinymathFunction.positionalArguments[position]) { + return { + list: uniq(getPossibleFunctions(indexPattern, operationDefinitionMap)).map((f) => ({ + type: 'math' as const, + label: f, + })), + type: SUGGESTION_TYPE.FUNCTIONS, + }; + } + return { list: [], type: SUGGESTION_TYPE.FIELD }; + } + + if (position > 0 || !hasFunctionFieldArgument(operation.type)) { + const { namedArguments } = groupArgsByType(ast.args); + const list = []; + if (operation.filterable) { + if (!namedArguments.find((arg) => arg.name === 'kql')) { + list.push('kql'); + } + if (!namedArguments.find((arg) => arg.name === 'lucene')) { + list.push('lucene'); + } + } + if ('operationParams' in operation) { + // Exclude any previously used named args + list.push( + ...operation + .operationParams!.filter( + (param) => + // Keep the param if it's the first use + !namedArguments.find((arg) => arg.name === param.name) + ) + .map((p) => p.name) + ); + } + return { list, type: SUGGESTION_TYPE.NAMED_ARGUMENT }; + } + + if (operation.input === 'field' && position === 0) { + const available = memoizedGetAvailableOperationsByMetadata( + indexPattern, + operationDefinitionMap + ); + // TODO: This only allow numeric functions, will reject last_value(string) for example. + const validOperation = available.find( + ({ operationMetaData }) => + operationMetaData.dataType === 'number' && !operationMetaData.isBucketed + ); + if (validOperation) { + const fields = validOperation.operations + .filter((op) => op.operationType === operation.type) + .map((op) => ('field' in op ? op.field : undefined)) + .filter((field) => field); + return { list: fields as string[], type: SUGGESTION_TYPE.FIELD }; + } else { + return { list: [], type: SUGGESTION_TYPE.FIELD }; + } + } + + if (operation.input === 'fullReference') { + const available = memoizedGetAvailableOperationsByMetadata( + indexPattern, + operationDefinitionMap + ); + const possibleOperationNames: string[] = []; + available.forEach((a) => { + if ( + operation.requiredReferences.some((requirement) => + requirement.validateMetadata(a.operationMetaData) + ) + ) { + possibleOperationNames.push( + ...a.operations + .filter((o) => + operation.requiredReferences.some((requirement) => requirement.input.includes(o.type)) + ) + .map((o) => o.operationType) + ); + } + }); + return { + list: uniq(possibleOperationNames).map((n) => ({ label: n, type: 'operation' as const })), + type: SUGGESTION_TYPE.FUNCTIONS, + }; + } + + return { list: [], type: SUGGESTION_TYPE.FIELD }; +} + +export async function getNamedArgumentSuggestions({ + ast, + data, + indexPattern, +}: { + ast: TinymathNamedArgument; + indexPattern: IndexPattern; + data: DataPublicPluginStart; +}) { + if (ast.name !== 'kql' && ast.name !== 'lucene') { + return { list: [], type: SUGGESTION_TYPE.KQL }; + } + if (!data.autocomplete.hasQuerySuggestions(ast.name === 'kql' ? 'kuery' : 'lucene')) { + return { list: [], type: SUGGESTION_TYPE.KQL }; + } + + const query = ast.value.split(MARKER)[0]; + const position = ast.value.indexOf(MARKER) + 1; + + const suggestions = await data.autocomplete.getQuerySuggestions({ + language: ast.name === 'kql' ? 'kuery' : 'lucene', + query, + selectionStart: position, + selectionEnd: position, + indexPatterns: [indexPattern], + boolFilter: [], + }); + return { + list: suggestions ?? [], + type: SUGGESTION_TYPE.KQL, + }; +} + +const TRIGGER_SUGGESTION_COMMAND = { + title: 'Trigger Suggestion Dialog', + id: 'editor.action.triggerSuggest', +}; + +export function getSuggestion( + suggestion: LensMathSuggestion, + type: SUGGESTION_TYPE, + operationDefinitionMap: Record, + triggerChar: string | undefined +): monaco.languages.CompletionItem { + let kind: monaco.languages.CompletionItemKind = monaco.languages.CompletionItemKind.Method; + let label: string = + typeof suggestion === 'string' + ? suggestion + : 'label' in suggestion + ? suggestion.label + : suggestion.text; + let insertText: string | undefined; + let insertTextRules: monaco.languages.CompletionItem['insertTextRules']; + let detail: string = ''; + let command: monaco.languages.CompletionItem['command']; + let sortText: string = ''; + const filterText: string = label; + + switch (type) { + case SUGGESTION_TYPE.FIELD: + kind = monaco.languages.CompletionItemKind.Value; + break; + case SUGGESTION_TYPE.FUNCTIONS: + insertText = `${label}($0)`; + insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; + if (typeof suggestion !== 'string') { + if ('text' in suggestion) break; + label = getFunctionSignatureLabel(suggestion.label, operationDefinitionMap); + const tinymathFunction = tinymathFunctions[suggestion.label]; + if (tinymathFunction) { + detail = 'TinyMath'; + kind = monaco.languages.CompletionItemKind.Method; + } else { + kind = monaco.languages.CompletionItemKind.Constant; + detail = 'Elasticsearch'; + // Always put ES functions first + sortText = `0${label}`; + command = TRIGGER_SUGGESTION_COMMAND; + } + } + break; + case SUGGESTION_TYPE.NAMED_ARGUMENT: + kind = monaco.languages.CompletionItemKind.Keyword; + if (label === 'kql' || label === 'lucene') { + command = TRIGGER_SUGGESTION_COMMAND; + insertText = `${label}='$0'`; + insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; + sortText = `zzz${label}`; + } + label = `${label}=`; + detail = ''; + break; + case SUGGESTION_TYPE.KQL: + if (triggerChar === ':') { + insertText = `${triggerChar} ${label}`; + } else { + // concatenate KQL suggestion for faster query composition + command = TRIGGER_SUGGESTION_COMMAND; + } + if (label.includes(`'`)) { + insertText = (insertText || label).replaceAll(`'`, "\\'"); + } + break; + } + + return { + detail, + kind, + label, + insertText: insertText ?? label, + insertTextRules, + command, + additionalTextEdits: [], + // @ts-expect-error Monaco says this type is required, but provides a default value + range: undefined, + sortText, + filterText, + }; +} + +function getOperationTypeHelp( + name: string, + operationDefinitionMap: Record +) { + const { description: descriptionInMarkdown, examples } = getHelpTextContent( + name, + operationDefinitionMap + ); + const examplesInMarkdown = examples.length + ? `\n\n**${i18n.translate('xpack.lens.formulaExampleMarkdown', { + defaultMessage: 'Examples', + })}** + + ${examples.map((example) => `\`${example}\``).join('\n\n')}` + : ''; + return { + value: `${descriptionInMarkdown}${examplesInMarkdown}`, + }; +} + +function getSignaturesForFunction( + name: string, + operationDefinitionMap: Record +) { + if (tinymathFunctions[name]) { + const stringify = getFunctionSignatureLabel(name, operationDefinitionMap); + const documentation = tinymathFunctions[name].help.replace(/\n/g, '\n\n'); + return [ + { + label: stringify, + documentation: { value: documentation }, + parameters: tinymathFunctions[name].positionalArguments.map((arg) => ({ + label: arg.name, + documentation: arg.optional + ? i18n.translate('xpack.lens.formula.optionalArgument', { + defaultMessage: 'Optional. Default value is {defaultValue}', + values: { + defaultValue: arg.defaultValue, + }, + }) + : '', + })), + }, + ]; + } + if (operationDefinitionMap[name]) { + const def = operationDefinitionMap[name]; + + const firstParam: monaco.languages.ParameterInformation | null = hasFunctionFieldArgument(name) + ? { + label: def.input === 'field' ? 'field' : def.input === 'fullReference' ? 'function' : '', + } + : null; + + const functionLabel = getFunctionSignatureLabel(name, operationDefinitionMap, firstParam); + const documentation = getOperationTypeHelp(name, operationDefinitionMap); + if ('operationParams' in def && def.operationParams) { + return [ + { + label: functionLabel, + parameters: [ + ...(firstParam ? [firstParam] : []), + ...def.operationParams.map((arg) => ({ + label: `${arg.name}=${arg.type}`, + documentation: arg.required + ? i18n.translate('xpack.lens.formula.requiredArgument', { + defaultMessage: 'Required', + }) + : '', + })), + ], + documentation, + }, + ]; + } + return [ + { + label: functionLabel, + parameters: firstParam ? [firstParam] : [], + documentation, + }, + ]; + } + return []; +} + +export function getSignatureHelp( + expression: string, + position: number, + operationDefinitionMap: Record +): monaco.languages.SignatureHelpResult { + const text = expression.substr(0, position) + MARKER + expression.substr(position); + try { + const ast = parse(text); + + const tokenInfo = getInfoAtZeroIndexedPosition(ast, position); + + let signatures: ReturnType = []; + let index = 0; + if (tokenInfo?.parent) { + const name = tokenInfo.parent.name; + // reference equality is fine here because of the way the getInfo function works + index = tokenInfo.parent.args.findIndex((arg) => arg === tokenInfo.ast); + signatures = getSignaturesForFunction(name, operationDefinitionMap); + } else if (typeof tokenInfo?.ast === 'object' && tokenInfo.ast.type === 'function') { + const name = tokenInfo.ast.name; + signatures = getSignaturesForFunction(name, operationDefinitionMap); + } + if (signatures.length) { + return { + value: { + // remove the documentation + signatures: signatures.map(({ documentation, ...signature }) => ({ + ...signature, + // extract only the first section (usually few lines) + documentation: { value: documentation.value.split('\n\n')[0] }, + })), + activeParameter: index, + activeSignature: 0, + }, + dispose: () => {}, + }; + } + } catch (e) { + // do nothing + } + return { value: { signatures: [], activeParameter: 0, activeSignature: 0 }, dispose: () => {} }; +} + +export function getHover( + expression: string, + position: number, + operationDefinitionMap: Record +): monaco.languages.Hover { + try { + const ast = parse(expression); + + const tokenInfo = getInfoAtZeroIndexedPosition(ast, position); + + if (!tokenInfo || typeof tokenInfo.ast === 'number' || !('name' in tokenInfo.ast)) { + return { contents: [] }; + } + + const name = tokenInfo.ast.name; + const signatures = getSignaturesForFunction(name, operationDefinitionMap); + if (signatures.length) { + const { label } = signatures[0]; + + return { + contents: [{ value: label }], + }; + } + } catch (e) { + // do nothing + } + return { contents: [] }; +} + +export function getTokenInfo(expression: string, position: number) { + const text = expression.substr(0, position) + MARKER + expression.substr(position); + try { + const ast = parse(text); + + return getInfoAtZeroIndexedPosition(ast, position); + } catch (e) { + return; + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_tokenization.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_tokenization.tsx new file mode 100644 index 00000000000000..17394560f8031c --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_tokenization.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { monaco } from '@kbn/monaco'; + +export const LANGUAGE_ID = 'lens_math'; +monaco.languages.register({ id: LANGUAGE_ID }); + +export const languageConfiguration: monaco.languages.LanguageConfiguration = { + wordPattern: /[^()'"\s]+/g, + brackets: [['(', ')']], + autoClosingPairs: [ + { open: '(', close: ')' }, + { open: `'`, close: `'` }, + { open: '"', close: '"' }, + ], + surroundingPairs: [ + { open: '(', close: ')' }, + { open: `'`, close: `'` }, + { open: '"', close: '"' }, + ], +}; + +export const lexerRules = { + defaultToken: 'invalid', + tokenPostfix: '', + ignoreCase: true, + brackets: [{ open: '(', close: ')', token: 'delimiter.parenthesis' }], + escapes: /\\(?:[\\"'])/, + tokenizer: { + root: [ + [/\s+/, 'whitespace'], + [/-?(\d*\.)?\d+([eE][+\-]?\d+)?/, 'number'], + [/[a-zA-Z0-9][a-zA-Z0-9_\-\.]*/, 'keyword'], + [/[,=:]/, 'delimiter'], + // strings double quoted + [/"([^"\\]|\\.)*$/, 'string.invalid'], // string without termination + [/"/, 'string', '@string_dq'], + // strings single quoted + [/'([^'\\]|\\.)*$/, 'string.invalid'], // string without termination + [/'/, 'string', '@string_sq'], + [/\+|\-|\*|\//, 'keyword.operator'], + [/[\(]/, 'delimiter'], + [/[\)]/, 'delimiter'], + ], + string_dq: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'], + ], + string_sq: [ + [/[^\\']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/'/, 'string', '@pop'], + ], + }, +} as monaco.languages.IMonarchLanguage; + +monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, lexerRules); +monaco.languages.setLanguageConfiguration(LANGUAGE_ID, languageConfiguration); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index 4a511e14d59e00..e1c722fd9cb38e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -14,8 +14,10 @@ import { tinymathFunctions } from './util'; jest.mock('../../layer_helpers', () => { return { - getColumnOrder: ({ columns }: { columns: Record }) => - Object.keys(columns), + getColumnOrder: jest.fn(({ columns }: { columns: Record }) => + Object.keys(columns) + ), + getManagedColumnsFrom: jest.fn().mockReturnValue([]), }; }); @@ -142,7 +144,7 @@ describe('formula', () => { indexPattern, }) ).toEqual({ - label: 'Formula', + label: 'average(bytes)', dataType: 'number', operationType: 'formula', isBucketed: false, @@ -170,7 +172,7 @@ describe('formula', () => { indexPattern, }) ).toEqual({ - label: 'Formula', + label: 'average(bytes)', dataType: 'number', operationType: 'formula', isBucketed: false, @@ -204,7 +206,7 @@ describe('formula', () => { indexPattern, }) ).toEqual({ - label: 'Formula', + label: `average(bytes, kql='category.keyword: "Men\\'s Clothing" or category.keyword: "Men\\'s Shoes"')`, dataType: 'number', operationType: 'formula', isBucketed: false, @@ -233,7 +235,7 @@ describe('formula', () => { indexPattern, }) ).toEqual({ - label: 'Formula', + label: `count(lucene='*')`, dataType: 'number', operationType: 'formula', isBucketed: false, @@ -291,7 +293,7 @@ describe('formula', () => { operationDefinitionMap ) ).toEqual({ - label: 'Formula', + label: 'moving_average(average(bytes), window=3)', dataType: 'number', operationType: 'formula', isBucketed: false, @@ -375,6 +377,7 @@ describe('formula', () => { ...layer.columns, col1: { ...currentColumn, + label: formula, params: { ...currentColumn.params, formula, @@ -415,6 +418,7 @@ describe('formula', () => { ...layer.columns, col1: { ...currentColumn, + label: 'average(bytes)', references: ['col1X1'], params: { ...currentColumn.params, @@ -565,7 +569,7 @@ describe('formula', () => { ).toEqual({ col1X0: { min: 15, max: 29 }, col1X2: { min: 0, max: 41 }, - col1X3: { min: 43, max: 50 }, + col1X3: { min: 42, max: 50 }, }); }); }); @@ -787,6 +791,34 @@ invalid: " } }); + it('returns an error if formula or math operations are used', () => { + const formulaFormulas = ['formula()', 'formula(bytes)', 'formula(formula())']; + + for (const formula of formulaFormulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Operation formula not found']); + } + + const mathFormulas = ['math()', 'math(bytes)', 'math(math())']; + + for (const formula of mathFormulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Operation math not found']); + } + }); + it('returns an error if field operation in formula have the wrong first argument', () => { const formulas = [ 'average(7)', @@ -897,6 +929,150 @@ invalid: " ).toEqual(undefined); }); + it('returns no error for a query edge case', () => { + const formulas = [ + `count(kql='')`, + `count(lucene='')`, + `moving_average(count(kql=''), window=7)`, + `count(kql='bytes >= 4000')`, + `count(kql='bytes <= 4000')`, + `count(kql='bytes = 4000')`, + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + } + }); + + it('returns an error for a query not wrapped in single quotes', () => { + const formulas = [ + `count(kql="")`, + `count(kql='")`, + `count(kql="')`, + `count(kql="category.keyword: *")`, + `count(kql='category.keyword: *")`, + `count(kql="category.keyword: *')`, + `count(kql='category.keyword: *)`, + `count(kql=category.keyword: *')`, + `count(kql=category.keyword: *)`, + `count(kql="category.keyword: "Men's Clothing" or category.keyword: "Men's Shoes"")`, + `count(lucene="category.keyword: *")`, + `count(lucene=category.keyword: *)`, + `count(lucene=category.keyword: *) + average(bytes)`, + `count(lucene='category.keyword: *') + count(kql=category.keyword: *)`, + `count(lucene='category.keyword: *") + count(kql='category.keyword: *")`, + `count(lucene='category.keyword: *') + count(kql=category.keyword: *, kql='category.keyword: *')`, + `count(lucene='category.keyword: *') + count(kql="category.keyword: *")`, + `moving_average(count(kql=category.keyword: *), window=7, kql=category.keywork: *)`, + `moving_average( + cumulative_sum( + 7 * clamp(sum(bytes), 0, last_value(memory) + max(memory)) + ), window=10, kql=category.keywork: * + )`, + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(expect.arrayContaining([expect.stringMatching(`Single quotes are required`)])); + } + }); + + it('it returns parse fail error rather than query message if the formula is only a query condition (false positive cases for query checks)', () => { + const formulas = [ + `kql="category.keyword: *"`, + `kql=category.keyword: *`, + `kql='category.keyword: *'`, + `(kql="category.keyword: *")`, + `(kql=category.keyword: *)`, + `(lucene="category.keyword: *")`, + `(lucene=category.keyword: *)`, + `(lucene='category.keyword: *') + (kql=category.keyword: *)`, + `(lucene='category.keyword: *') + (kql=category.keyword: *, kql='category.keyword: *')`, + `(lucene='category.keyword: *') + (kql="category.keyword: *")`, + `((kql=category.keyword: *), window=7, kql=category.keywork: *)`, + `(, window=10, kql=category.keywork: *)`, + `( + , window=10, kql=category.keywork: * + )`, + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The Formula ${formula} cannot be parsed`]); + } + }); + + it('returns no error for a query wrapped in single quotes but with some whitespaces', () => { + const formulas = [ + `count(kql ='category.keyword: *')`, + `count(kql = 'category.keyword: *')`, + `count(kql = 'category.keyword: *')`, + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + } + }); + + it('returns an error for multiple queries submitted for the same function', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`count(kql='category.keyword: *', lucene='category.keyword: *')`), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Use only one of kql= or lucene=, not both']); + }); + + it("returns a clear error when there's a missing field for a function", () => { + for (const fn of ['average', 'terms', 'max', 'sum']) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`${fn}()`), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The first argument for ${fn} should be a field name. Found no field`]); + } + }); + + it("returns a clear error when there's a missing function for a fullReference operation", () => { + for (const fn of ['cumulative_sum', 'derivative']) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`${fn}()`), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The first argument for ${fn} should be a operation name. Found no operation`]); + } + }); + it('returns no error if a math operation is passed to fullReference operations', () => { const formulas = [ 'derivative(7+1)', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx index de7ecb4bc75da3..3ed50906908762 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -10,8 +10,11 @@ import { OperationDefinition } from '../index'; import { ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPattern } from '../../../types'; import { runASTValidation, tryToParse } from './validation'; +import { MemoizedFormulaEditor } from './editor'; import { regenerateLayerFromAst } from './parse'; import { generateFormula } from './generate'; +import { filterByVisibleOperation } from './util'; +import { getManagedColumnsFrom } from '../../layer_helpers'; const defaultLabel = i18n.translate('xpack.lens.indexPattern.formulaLabel', { defaultMessage: 'Formula', @@ -38,7 +41,7 @@ export const formulaOperation: OperationDefinition< > = { type: 'formula', displayName: defaultLabel, - getDefaultLabel: (column, indexPattern) => defaultLabel, + getDefaultLabel: (column, indexPattern) => column.params.formula ?? defaultLabel, input: 'managedReference', hidden: true, getDisabledStatus(indexPattern: IndexPattern) { @@ -49,13 +52,32 @@ export const formulaOperation: OperationDefinition< if (!column.params.formula || !operationDefinitionMap) { return; } - const { root, error } = tryToParse(column.params.formula); + + const visibleOperationsMap = filterByVisibleOperation(operationDefinitionMap); + const { root, error } = tryToParse(column.params.formula, visibleOperationsMap); if (error || !root) { return [error!.message]; } - const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap); - return errors.length ? errors.map(({ message }) => message) : undefined; + const errors = runASTValidation(root, layer, indexPattern, visibleOperationsMap); + + if (errors.length) { + return errors.map(({ message }) => message); + } + + const managedColumns = getManagedColumnsFrom(columnId, layer.columns); + const innerErrors = managedColumns + .flatMap(([id, col]) => { + const def = visibleOperationsMap[col.operationType]; + if (def?.getErrorMessage) { + const messages = def.getErrorMessage(layer, id, indexPattern, visibleOperationsMap); + return messages ? { message: messages.join(', ') } : []; + } + return []; + }) + .filter((marker) => marker); + + return innerErrors.length ? innerErrors.map(({ message }) => message) : undefined; }, getPossibleOperation() { return { @@ -72,8 +94,8 @@ export const formulaOperation: OperationDefinition< const label = !params?.isFormulaBroken ? useDisplayLabel ? currentColumn.label - : params?.formula - : ''; + : params?.formula ?? defaultLabel + : defaultLabel; return [ { @@ -81,21 +103,23 @@ export const formulaOperation: OperationDefinition< function: 'mapColumn', arguments: { id: [columnId], - name: [label || ''], + name: [label || defaultLabel], exp: [ { type: 'expression', - chain: [ - { - type: 'function', - function: 'math', - arguments: { - expression: [ - currentColumn.references.length ? `"${currentColumn.references[0]}"` : ``, - ], - }, - }, - ], + chain: currentColumn.references.length + ? [ + { + type: 'function', + function: 'math', + arguments: { + expression: [ + currentColumn.references.length ? `"${currentColumn.references[0]}"` : ``, + ], + }, + }, + ] + : [], }, ], }, @@ -119,7 +143,7 @@ export const formulaOperation: OperationDefinition< prevFormat = { format: previousColumn.params.format }; } return { - label: 'Formula', + label: previousFormula || defaultLabel, dataType: 'number', operationType: 'formula', isBucketed: false, @@ -152,4 +176,6 @@ export const formulaOperation: OperationDefinition< ); return newLayer; }, + + paramEditor: MemoizedFormulaEditor, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_examples.md b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_examples.md new file mode 100644 index 00000000000000..ae244109ed53ee --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math_examples.md @@ -0,0 +1,28 @@ +Basic numeric functions that we already support in Lens: + +count() +count(normalize_unit='1s') +sum(field name) +avg(field name) +moving_average(sum(field name), window=5) +moving_average(sum(field name), window=5, normalize_unit='1s') +counter_rate(field name, normalize_unit='1s') +differences(count()) +differences(sum(bytes), normalize_unit='1s') +last_value(bytes, sort=timestamp) +percentile(bytes, percent=95) + +Adding features beyond what we already support. New features are: + +* Filtering +* Math across series +* Time offset + +count() * 100 +(count() / count(offset=-7d)) + min(field name) +sum(field name, filter='field.keyword: "KQL autocomplete inside math" AND field.value > 100') + +What about custom formatting using string manipulation? Probably not... + +(avg(bytes) / 1000) + 'kb' + \ No newline at end of file diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index 3bfc6fcbfc011e..517cf5f1bbf45c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { isObject } from 'lodash'; import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath'; import { OperationDefinition, GenericOperationDefinition, IndexPatternColumn } from '../index'; @@ -12,7 +13,12 @@ import { IndexPattern, IndexPatternLayer } from '../../../types'; import { mathOperation } from './math'; import { documentField } from '../../../document_field'; import { runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation'; -import { findVariables, getOperationParams, groupArgsByType } from './util'; +import { + filterByVisibleOperation, + findVariables, + getOperationParams, + groupArgsByType, +} from './util'; import { FormulaIndexPatternColumn } from './formula'; import { getColumnOrder } from '../../layer_helpers'; @@ -27,7 +33,7 @@ function parseAndExtract( indexPattern: IndexPattern, operationDefinitionMap: Record ) { - const { root, error } = tryToParse(text); + const { root, error } = tryToParse(text, operationDefinitionMap); if (error || !root) { return { extracted: [], isValid: false }; } @@ -61,9 +67,9 @@ function extractColumns( const nodeOperation = operations[node.name]; if (!nodeOperation) { // it's a regular math node - const consumedArgs = node.args.map(parseNode).filter(Boolean) as Array< - number | TinymathVariable - >; + const consumedArgs = node.args + .map(parseNode) + .filter((n) => typeof n !== 'undefined' && n !== null) as Array; return { ...node, args: consumedArgs, @@ -168,7 +174,7 @@ export function regenerateLayerFromAst( layer, columnId, indexPattern, - operationDefinitionMap + filterByVisibleOperation(operationDefinitionMap) ); const columns = { ...layer.columns }; @@ -188,6 +194,12 @@ export function regenerateLayerFromAst( columns[columnId] = { ...currentColumn, + label: !currentColumn.customLabel + ? text ?? + i18n.translate('xpack.lens.indexPattern.formulaLabel', { + defaultMessage: 'Formula', + }) + : currentColumn.label, params: { ...currentColumn.params, formula: text, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts index 5d9a8647eb7ab0..dd95ebdec5b8ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -13,7 +13,7 @@ import type { TinymathNamedArgument, TinymathVariable, } from 'packages/kbn-tinymath'; -import type { OperationDefinition, IndexPatternColumn } from '../index'; +import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index'; import type { GroupedNodes } from './types'; export function groupArgsByType(args: TinymathAST[]) { @@ -66,6 +66,16 @@ export function getOperationParams( }, {}); } +function getTypeI18n(type: string) { + if (type === 'number') { + return i18n.translate('xpack.lens.formula.number', { defaultMessage: 'number' }); + } + if (type === 'string') { + return i18n.translate('xpack.lens.formula.string', { defaultMessage: 'string' }); + } + return ''; +} + // Todo: i18n everything here export const tinymathFunctions: Record< string, @@ -73,145 +83,254 @@ export const tinymathFunctions: Record< positionalArguments: Array<{ name: string; optional?: boolean; + defaultValue?: string | number; + type?: string; }>; - // help: React.ReactElement; // Help is in Markdown format help: string; } > = { add: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, - { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + { + name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }), + type: getTypeI18n('number'), + }, + { + name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }), + type: getTypeI18n('number'), + }, ], help: ` +Adds up two numbers. Also works with + symbol -Example: ${'`count() + sum(bytes)`'} -Example: ${'`add(count(), 5)`'} + +Example: Calculate the sum of two fields + +${'`sum(price) + sum(tax)`'} + +Example: Offset count by a static value + +${'`add(count(), 5)`'} `, }, subtract: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, - { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + { + name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }), + type: getTypeI18n('number'), + }, + { + name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }), + type: getTypeI18n('number'), + }, ], help: ` +Subtracts the first number from the second number. Also works with ${'`-`'} symbol -Example: ${'`subtract(sum(bytes), avg(bytes))`'} + +Example: Calculate the range of a field +${'`subtract(max(bytes), min(bytes))`'} `, }, multiply: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, - { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + { + name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }), + type: getTypeI18n('number'), + }, + { + name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }), + type: getTypeI18n('number'), + }, ], help: ` -Also works with ${'`*`'} symbol -Example: ${'`multiply(sum(bytes), 2)`'} +Multiplies two numbers. +Also works with ${'`*`'} symbol. + +Example: Calculate price after current tax rate +${'`sum(bytes) * last_value(tax_rate)`'} + +Example: Calculate price after constant tax rate +${'`multiply(sum(price), 1.2)`'} `, }, divide: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, - { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + { + name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }), + type: getTypeI18n('number'), + }, + { + name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }), + type: getTypeI18n('number'), + }, ], help: ` +Divides the first number by the second number. Also works with ${'`/`'} symbol -Example: ${'`ceil(sum(bytes))`'} + +Example: Calculate profit margin +${'`sum(profit) / sum(revenue)`'} + +Example: ${'`divide(sum(bytes), 2)`'} `, }, abs: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` -Absolute value -Example: ${'`abs(sum(bytes))`'} +Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same. + +Example: Calculate average distance to sea level ${'`abs(average(altitude))`'} `, }, cbrt: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` -Cube root of value -Example: ${'`cbrt(sum(bytes))`'} +Cube root of value. + +Example: Calculate side length from volume +${'`cbrt(last_value(volume))`'} `, }, ceil: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], + // signature: 'ceil(value: number)', help: ` -Ceiling of value, rounds up -Example: ${'`ceil(sum(bytes))`'} +Ceiling of value, rounds up. + +Example: Round up price to the next dollar +${'`ceil(sum(price))`'} `, }, clamp: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, - { name: i18n.translate('xpack.lens.formula.min', { defaultMessage: 'min' }) }, - { name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, + { + name: i18n.translate('xpack.lens.formula.min', { defaultMessage: 'min' }), + type: getTypeI18n('number'), + }, + { + name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }), + type: getTypeI18n('number'), + }, ], + // signature: 'clamp(value: number, minimum: number, maximum: number)', help: ` -Limits the value from a minimum to maximum -Example: ${'`ceil(sum(bytes))`'} - `, +Limits the value from a minimum to maximum. + +Example: Make sure to catch outliers +\`\`\` +clamp( + average(bytes), + percentile(bytes, percentile=5), + percentile(bytes, percentile=95) +) +\`\`\` +`, }, cube: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` -Limits the value from a minimum to maximum -Example: ${'`ceil(sum(bytes))`'} +Calculates the cube of a number. + +Example: Calculate volume from side length +${'`cube(last_value(length))`'} `, }, exp: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` -Raises e to the nth power. -Example: ${'`exp(sum(bytes))`'} +Raises *e* to the nth power. + +Example: Calculate the natural exponential function + +${'`exp(last_value(duration))`'} `, }, fix: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` For positive values, takes the floor. For negative values, takes the ceiling. -Example: ${'`fix(sum(bytes))`'} + +Example: Rounding towards zero +${'`fix(sum(profit))`'} `, }, floor: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` Round down to nearest integer value -Example: ${'`floor(sum(bytes))`'} + +Example: Round down a price +${'`floor(sum(price))`'} `, }, log: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, { name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), optional: true, + defaultValue: 'e', + type: getTypeI18n('number'), }, ], help: ` -Logarithm with optional base. The natural base e is used as default. -Example: ${'`log(sum(bytes))`'} -Example: ${'`log(sum(bytes), 2)`'} +Logarithm with optional base. The natural base *e* is used as default. + +Example: Calculate number of bits required to store values +\`\`\` +log(sum(bytes)) +log(sum(bytes), 2) +\`\`\` `, }, // TODO: check if this is valid for Tinymath // log10: { // positionalArguments: [ - // { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + // { name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), type: getTypeI18n('number') }, // ], // help: ` // Base 10 logarithm. @@ -220,59 +339,89 @@ Example: ${'`log(sum(bytes), 2)`'} // }, mod: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, { name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), - optional: true, + type: getTypeI18n('number'), }, ], help: ` Remainder after dividing the function by a number -Example: ${'`mod(sum(bytes), 2)`'} + +Example: Calculate last three digits of a value +${'`mod(sum(price), 1000)`'} `, }, pow: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, { name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + type: getTypeI18n('number'), }, ], help: ` Raises the value to a certain power. The second argument is required -Example: ${'`pow(sum(bytes), 3)`'} + +Example: Calculate volume based on side length +${'`pow(last_value(length), 3)`'} `, }, round: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, { name: i18n.translate('xpack.lens.formula.decimals', { defaultMessage: 'decimals' }), optional: true, + defaultValue: 0, + type: getTypeI18n('number'), }, ], help: ` Rounds to a specific number of decimal places, default of 0 -Example: ${'`round(sum(bytes))`'} -Example: ${'`round(sum(bytes), 2)`'} + +Examples: Round to the cent +\`\`\` +round(sum(bytes)) +round(sum(bytes), 2) +\`\`\` `, }, sqrt: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` Square root of a positive value only -Example: ${'`sqrt(sum(bytes))`'} + +Example: Calculate side length based on area +${'`sqrt(last_value(area))`'} `, }, square: { positionalArguments: [ - { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), + type: getTypeI18n('number'), + }, ], help: ` Raise the value to the 2nd power -Example: ${'`square(sum(bytes))`'} + +Example: Calculate area based on side length +${'`square(last_value(length))`'} `, }, }; @@ -315,3 +464,11 @@ export function findVariables(node: TinymathAST | string): TinymathVariable[] { } return node.args.flatMap(findVariables); } + +export function filterByVisibleOperation( + operationDefinitionMap: Record +) { + return Object.fromEntries( + Object.entries(operationDefinitionMap).filter(([, operation]) => !operation.hidden) + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts index 5145c7959f1bb5..992b8ee2422e90 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isObject } from 'lodash'; +import { isObject, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import { parse, TinymathLocation } from '@kbn/tinymath'; import type { TinymathAST, TinymathFunction, TinymathNamedArgument } from '@kbn/tinymath'; @@ -58,6 +58,10 @@ interface ValidationErrors { message: string; type: { operation: string; count: number; params: string }; }; + tooManyQueries: { + message: string; + type: {}; + }; } type ErrorTypes = keyof ValidationErrors; type ErrorValues = ValidationErrors[K]['type']; @@ -90,15 +94,76 @@ export function hasInvalidOperations( return { // avoid duplicates names: Array.from(new Set(nodes.map(({ name }) => name))), - locations: nodes.map(({ location }) => location), + locations: nodes.map(({ location }) => location).filter((a) => a) as TinymathLocation[], }; } +export const getRawQueryValidationError = (text: string, operations: Record) => { + // try to extract the query context here + const singleLine = text.split('\n').join(''); + const allArgs = singleLine.split(',').filter((args) => /(kql|lucene)/.test(args)); + // check for the presence of a valid ES operation + const containsOneValidOperation = Object.keys(operations).some((operation) => + singleLine.includes(operation) + ); + // no args or no valid operation, no more work to do here + if (allArgs.length === 0 || !containsOneValidOperation) { + return; + } + // at this point each entry in allArgs may contain one or more + // in the worst case it would be a math chain of count operation + // For instance: count(kql=...) + count(lucene=...) - count(kql=...) + // therefore before partition them, split them by "count" keywork and filter only string with a length + const flattenArgs = allArgs.flatMap((arg) => + arg.split('count').filter((subArg) => /(kql|lucene)/.test(subArg)) + ); + const [kqlQueries, luceneQueries] = partition(flattenArgs, (arg) => /kql/.test(arg)); + const errors = []; + for (const kqlQuery of kqlQueries) { + const result = validateQueryQuotes(kqlQuery, 'kql'); + if (result) { + errors.push(result); + } + } + for (const luceneQuery of luceneQueries) { + const result = validateQueryQuotes(luceneQuery, 'lucene'); + if (result) { + errors.push(result); + } + } + return errors.length ? errors : undefined; +}; + +const validateQueryQuotes = (rawQuery: string, language: 'kql' | 'lucene') => { + // check if the raw argument has the minimal requirements + // use the rest operator here to handle cases where comparison operations are used in the query + const [, ...rawValue] = rawQuery.split('='); + const fullRawValue = (rawValue || ['']).join(''); + const cleanedRawValue = fullRawValue.trim(); + // it must start with a single quote, and quotes must have a closure + if ( + cleanedRawValue.length && + (cleanedRawValue[0] !== "'" || !/'\s*([^']+?)\s*'/.test(fullRawValue)) && + // there's a special case when it's valid as two single quote strings + cleanedRawValue !== "''" + ) { + return i18n.translate('xpack.lens.indexPattern.formulaOperationQueryError', { + defaultMessage: `Single quotes are required for {language}='' at {rawQuery}`, + values: { language, rawQuery }, + }); + } +}; + export const getQueryValidationError = ( - query: string, - language: 'kql' | 'lucene', + { value: query, name: language, text }: TinymathNamedArgument, indexPattern: IndexPattern ): string | undefined => { + // check if the raw argument has the minimal requirements + const result = validateQueryQuotes(text, language as 'kql' | 'lucene'); + // forward the error here is ok? + if (result) { + return result; + } try { if (language === 'kql') { esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query), indexPattern); @@ -113,7 +178,7 @@ export const getQueryValidationError = ( function getMessageFromId({ messageId, - values: { ...values }, + values, locations, }: { messageId: K; @@ -203,6 +268,11 @@ function getMessageFromId({ values: { operation: out.operation, count: out.count, params: out.params }, }); break; + case 'tooManyQueries': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationDoubleQueryError', { + defaultMessage: 'Use only one of kql= or lucene=, not both', + }); + break; // case 'mathRequiresFunction': // message = i18n.translate('xpack.lens.indexPattern.formulaMathRequiresFunctionLabel', { // defaultMessage; 'The function {name} requires an Elasticsearch function', @@ -218,12 +288,22 @@ function getMessageFromId({ } export function tryToParse( - formula: string + formula: string, + operations: Record ): { root: TinymathAST; error: null } | { root: null; error: ErrorWrapper } { let root; try { root = parse(formula); } catch (e) { + // A tradeoff is required here, unless we want to reimplement a full parser + // Internally the function has the following logic: + // * if the formula contains no existing ES operation, assume it's a plain parse failure + // * if the formula contains at least one existing operation, check for query problems + const maybeQueryProblems = getRawQueryValidationError(formula, operations); + if (maybeQueryProblems) { + // need to emulate an error shape here + return { root: null, error: { message: maybeQueryProblems[0], locations: [] } }; + } return { root: null, error: getMessageFromId({ @@ -319,7 +399,10 @@ function getQueryValidationErrors( const errors: ErrorWrapper[] = []; (namedArguments ?? []).forEach((arg) => { if (arg.name === 'kql' || arg.name === 'lucene') { - const message = getQueryValidationError(arg.value, arg.name, indexPattern); + const message = getQueryValidationError( + arg as TinymathNamedArgument & { name: 'kql' | 'lucene' }, + indexPattern + ); if (message) { errors.push({ message, @@ -331,6 +414,12 @@ function getQueryValidationErrors( return errors; } +function checkSingleQuery(namedArguments: TinymathNamedArgument[] | undefined) { + return namedArguments + ? namedArguments.filter((arg) => arg.name === 'kql' || arg.name === 'lucene').length > 1 + : undefined; +} + function validateNameArguments( node: TinymathFunction, nodeOperation: @@ -349,7 +438,7 @@ function validateNameArguments( operation: node.name, params: missingParams.map(({ name }) => name).join(', '), }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } @@ -362,7 +451,7 @@ function validateNameArguments( operation: node.name, params: wrongTypeParams.map(({ name }) => name).join(', '), }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } @@ -375,7 +464,7 @@ function validateNameArguments( operation: node.name, params: duplicateParams.join(', '), }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } @@ -383,6 +472,16 @@ function validateNameArguments( if (queryValidationErrors.length) { errors.push(...queryValidationErrors); } + const hasTooManyQueries = checkSingleQuery(namedArguments); + if (hasTooManyQueries) { + errors.push( + getMessageFromId({ + messageId: 'tooManyQueries', + values: {}, + locations: node.location ? [node.location] : [], + }) + ); + } return errors; } @@ -426,7 +525,7 @@ function runFullASTValidation( type: 'field', argument: `math operation`, }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } else { @@ -436,9 +535,13 @@ function runFullASTValidation( values: { operation: node.name, type: 'field', - argument: getValueOrName(firstArg), + argument: + getValueOrName(firstArg) || + i18n.translate('xpack.lens.indexPattern.formulaNoFieldForOperation', { + defaultMessage: 'no field', + }), }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } @@ -452,7 +555,7 @@ function runFullASTValidation( values: { operation: node.name, }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } @@ -464,7 +567,7 @@ function runFullASTValidation( values: { operation: node.name, }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } else { @@ -493,9 +596,13 @@ function runFullASTValidation( values: { operation: node.name, type: 'operation', - argument: getValueOrName(firstArg), + argument: + getValueOrName(firstArg) || + i18n.translate('xpack.lens.indexPattern.formulaNoOperation', { + defaultMessage: 'no operation', + }), }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } @@ -506,7 +613,7 @@ function runFullASTValidation( values: { operation: node.name, }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } else { @@ -606,7 +713,11 @@ export function validateParams( } export function shouldHaveFieldArgument(node: TinymathFunction) { - return !['count'].includes(node.name); + return hasFunctionFieldArgument(node.name); +} + +export function hasFunctionFieldArgument(type: string) { + return !['count'].includes(type); } export function isFirstArgumentValidType(arg: TinymathAST, type: TinymathNodeTypes['type']) { @@ -628,7 +739,7 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set name).join(', '), }, - locations: [node.location], + locations: node.location ? [node.location] : [], }) ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 164415c1a1f6f3..a7bf415817797d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -153,6 +153,9 @@ export interface ParamEditorProps { updateLayer: ( setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) ) => void; + toggleFullscreen: () => void; + setIsCloseable: (isCloseable: boolean) => void; + isFullscreen: boolean; columnId: string; indexPattern: IndexPattern; uiSettings: IUiSettingsClient; @@ -279,6 +282,11 @@ interface BaseOperationDefinitionProps { * Operations can be used as middleware for other operations, hence not shown in the panel UI */ hidden?: boolean; + documentation?: { + signature: string; + description: string; + section: 'elasticsearch' | 'calculation'; + }; } interface BaseBuildColumnArgs { @@ -290,6 +298,7 @@ interface OperationParam { name: string; type: string; required?: boolean; + defaultValue?: string | number; } interface FieldlessOperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 15ce3bdcd0b0f5..2ad91a7ba91a1e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -30,6 +30,9 @@ const defaultProps = { hasRestrictions: false, } as IndexPattern, operationDefinitionMap: {}, + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), }; describe('last_value', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index bde80accfbc676..bfc5ce39bc9390 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -277,4 +277,20 @@ export const lastValueOperation: OperationDefinition ); }, + documentation: { + section: 'elasticsearch', + signature: i18n.translate('xpack.lens.indexPattern.lastValue.signature', { + defaultMessage: 'field: string', + }), + description: i18n.translate('xpack.lens.indexPattern.lastValue.documentation', { + defaultMessage: ` +Returns the value of a field from the last document, ordered by the default time field of the index pattern. + +This function is usefull the retrieve the latest state of an entity. + +Example: Get the current status of server A: +\`last_value(server.status, kql=\'server.name="A"\')\` + `, + }), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 54a3ff0eb8bdbf..58fe91b23f2c7d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -42,6 +42,7 @@ const supportedTypes = ['number', 'histogram']; function buildMetricOperation>({ type, displayName, + description, ofName, priority, optionalTimeScaling, @@ -51,6 +52,7 @@ function buildMetricOperation>({ ofName: (name: string) => string; priority?: number; optionalTimeScaling?: boolean; + description?: string; }) { const labelLookup = (name: string, column?: BaseIndexPatternColumn) => { const label = ofName(name); @@ -67,6 +69,7 @@ function buildMetricOperation>({ type, priority, displayName, + description, input: 'field', timeScalingMode: optionalTimeScaling ? 'optional' : undefined, getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { @@ -131,6 +134,26 @@ function buildMetricOperation>({ getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), filterable: true, + documentation: { + section: 'elasticsearch', + signature: i18n.translate('xpack.lens.indexPattern.metric.signature', { + defaultMessage: 'field: string', + }), + description: i18n.translate('xpack.lens.indexPattern.metric.documentation', { + defaultMessage: ` +Returns the {metric} of a field. This function only works for number fields. + +Example: Get the {metric} of price: +\`{metric}(price)\` + +Example: Get the {metric} of price for orders from the UK: +\`{metric}(price, kql='location:UK')\` + `, + values: { + metric: type, + }, + }), + }, shiftable: true, } as OperationDefinition; } @@ -151,6 +174,10 @@ export const minOperation = buildMetricOperation({ defaultMessage: 'Minimum of {name}', values: { name }, }), + description: i18n.translate('xpack.lens.indexPattern.min.description', { + defaultMessage: + 'A single-value metrics aggregation that returns the minimum value among the numeric values extracted from the aggregated documents.', + }), }); export const maxOperation = buildMetricOperation({ @@ -163,6 +190,10 @@ export const maxOperation = buildMetricOperation({ defaultMessage: 'Maximum of {name}', values: { name }, }), + description: i18n.translate('xpack.lens.indexPattern.max.description', { + defaultMessage: + 'A single-value metrics aggregation that returns the maximum value among the numeric values extracted from the aggregated documents.', + }), }); export const averageOperation = buildMetricOperation({ @@ -176,6 +207,10 @@ export const averageOperation = buildMetricOperation({ defaultMessage: 'Average of {name}', values: { name }, }), + description: i18n.translate('xpack.lens.indexPattern.avg.description', { + defaultMessage: + 'A single-value metric aggregation that computes the average of numeric values that are extracted from the aggregated documents', + }), }); export const sumOperation = buildMetricOperation({ @@ -190,6 +225,10 @@ export const sumOperation = buildMetricOperation({ values: { name }, }), optionalTimeScaling: true, + description: i18n.translate('xpack.lens.indexPattern.sum.description', { + defaultMessage: + 'A single-value metrics aggregation that sums up numeric values that are extracted from the aggregated documents.', + }), }); export const medianOperation = buildMetricOperation({ @@ -203,4 +242,8 @@ export const medianOperation = buildMetricOperation({ defaultMessage: 'Median of {name}', values: { name }, }), + description: i18n.translate('xpack.lens.indexPattern.median.description', { + defaultMessage: + 'A single-value metrics aggregation that computes the median value that are extracted from the aggregated documents.', + }), }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 2b7104112d63eb..0a3462ef20f3f7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -32,6 +32,9 @@ const defaultProps = { hasRestrictions: false, } as IndexPattern, operationDefinitionMap: {}, + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), }; describe('percentile', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index aa8f951d46b4f2..39b876050c2eae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -59,7 +59,9 @@ export const percentileOperation: OperationDefinition { @@ -213,4 +215,18 @@ export const percentileOperation: OperationDefinition ); }, + documentation: { + section: 'elasticsearch', + signature: i18n.translate('xpack.lens.indexPattern.percentile.signature', { + defaultMessage: 'field: string, [percentile]: number', + }), + description: i18n.translate('xpack.lens.indexPattern.percentile.documentation', { + defaultMessage: ` +Returns the specified percentile of the values of a field. This is the value n percent of the values occuring in documents are smaller. + +Example: Get the number of bytes larger than 95 % of values: +\`percentile(bytes, percentile=95)\` + `, + }), + }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 295f988c6e3906..ca4e6d5df0a3c4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -102,6 +102,9 @@ const defaultOptions = { ]), }, operationDefinitionMap: {}, + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), }; describe('ranges', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index b272d1703377ce..32b7dfee828fce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -35,6 +35,9 @@ const defaultProps = { http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), operationDefinitionMap: {}, + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), }; describe('terms', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 38bc84ae9af357..ba3bee415f3f49 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -24,7 +24,7 @@ import type { IndexPattern, IndexPatternLayer } from '../types'; import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; -import { createMockedFullReference } from './mocks'; +import { createMockedFullReference, createMockedManagedReference } from './mocks'; jest.mock('../operations'); jest.mock('../../id_generator'); @@ -91,10 +91,13 @@ describe('state_helpers', () => { // @ts-expect-error we are inserting an invalid type operationDefinitionMap.testReference = createMockedFullReference(); + // @ts-expect-error we are inserting an invalid type + operationDefinitionMap.managedReference = createMockedManagedReference(); }); afterEach(() => { delete operationDefinitionMap.testReference; + delete operationDefinitionMap.managedReference; }); describe('copyColumn', () => { @@ -102,19 +105,19 @@ describe('state_helpers', () => { const source = { dataType: 'number' as const, isBucketed: false, - label: 'Formula', + label: 'moving_average(sum(bytes), window=5)', operationType: 'formula' as const, params: { formula: 'moving_average(sum(bytes), window=5)', isFormulaBroken: false, }, - references: ['formulaX3'], + references: ['formulaX1'], }; const math = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'math', + label: 'formulaX2', operationType: 'math' as const, params: { tinymathAst: 'formulaX2' }, references: ['formulaX2'], @@ -135,7 +138,7 @@ describe('state_helpers', () => { label: 'formulaX2', operationType: 'moving_average' as const, params: { window: 5 }, - references: ['formulaX1'], + references: ['formulaX0'], }; expect( copyColumn({ @@ -387,6 +390,42 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); + it('should not change order of metrics and references on inserting new buckets', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Cumulative sum of count of records', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'cumulative_sum', + references: ['col2'], + }, + col2: { + label: 'Count of records', + dataType: 'document', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', + }, + }, + }; + expect( + insertNewColumn({ + layer, + indexPattern, + columnId: 'col3', + op: 'filters', + visualizationGroups: [], + }) + ).toEqual(expect.objectContaining({ columnOrder: ['col3', 'col1', 'col2'] })); + }); + it('should insert both incomplete states if the aggregation does not support the field', () => { expect( insertNewColumn({ @@ -2655,6 +2694,36 @@ describe('state_helpers', () => { expect(errors).toHaveLength(1); }); + it('should only collect the top level errors from managed references', () => { + const notCalledMock = jest.fn(); + const mock = jest.fn().mockReturnValue(['error 1']); + operationDefinitionMap.testReference.getErrorMessage = notCalledMock; + operationDefinitionMap.managedReference.getErrorMessage = mock; + const errors = getErrorMessages( + { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed + { operationType: 'managedReference', references: ['col2'] }, + col2: { + // @ts-expect-error not statically analyzed + operationType: 'testReference', + references: [], + }, + }, + }, + indexPattern, + {}, + '1', + {} + ); + expect(notCalledMock).not.toHaveBeenCalled(); + expect(mock).toHaveBeenCalledTimes(1); + expect(errors).toHaveLength(1); + }); + it('should ignore incompleteColumns when checking for errors', () => { const savedRef = jest.fn().mockReturnValue(['error 1']); const incompleteRef = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 56fbb8edef5b43..b650a2818b2d48 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -169,6 +169,10 @@ export function insertNewColumn({ if (field) { throw new Error(`Can't create operation ${op} with the provided field ${field.name}`); } + if (operationDefinition.input === 'managedReference') { + // TODO: need to create on the fly the new columns for Formula, + // like we do for fullReferences to show a seamless transition + } const possibleOperation = operationDefinition.getPossibleOperation(); const isBucketed = Boolean(possibleOperation?.isBucketed); const addOperationFn = isBucketed ? addBucket : addMetric; @@ -358,9 +362,9 @@ export function replaceColumn({ tempLayer = resetIncomplete(tempLayer, columnId); if (previousDefinition.input === 'managedReference') { - // Every transition away from a managedReference resets it, we don't have a way to keep the state + // If the transition is incomplete, leave the managed state until it's finished. tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); - return insertNewColumn({ + const hypotheticalLayer = insertNewColumn({ layer: tempLayer, columnId, indexPattern, @@ -368,6 +372,14 @@ export function replaceColumn({ field, visualizationGroups, }); + if (hypotheticalLayer.incompleteColumns && hypotheticalLayer.incompleteColumns[columnId]) { + return { + ...layer, + incompleteColumns: hypotheticalLayer.incompleteColumns, + }; + } else { + return hypotheticalLayer; + } } if (operationDefinition.input === 'fullReference') { @@ -859,7 +871,10 @@ function addBucket( visualizationGroups: VisualizationDimensionGroupConfig[], targetGroup?: string ): IndexPatternLayer { - const [buckets, metrics, references] = getExistingColumnGroups(layer); + const [buckets, metrics] = partition( + layer.columnOrder, + (colId) => layer.columns[colId].isBucketed + ); const oldDateHistogramIndex = layer.columnOrder.findIndex( (columnId) => layer.columns[columnId].operationType === 'date_histogram' @@ -873,12 +888,11 @@ function addBucket( addedColumnId, ...buckets.slice(oldDateHistogramIndex, buckets.length), ...metrics, - ...references, ]; } else { // Insert the new bucket after existing buckets. Users will see the same data // they already had, with an extra level of detail. - updatedColumnOrder = [...buckets, addedColumnId, ...metrics, ...references]; + updatedColumnOrder = [...buckets, addedColumnId, ...metrics]; } updatedColumnOrder = reorderByGroups( visualizationGroups, @@ -1169,8 +1183,20 @@ export function getErrorMessages( } > | undefined { - const errors = Object.entries(layer.columns) + const columns = Object.entries(layer.columns); + const visibleManagedReferences = columns.filter( + ([columnId, column]) => + !isReferenced(layer, columnId) && + operationDefinitionMap[column.operationType].input === 'managedReference' + ); + const skippedColumns = visibleManagedReferences.flatMap(([columnId]) => + getManagedColumnsFrom(columnId, layer.columns).map(([id]) => id) + ); + const errors = columns .flatMap(([columnId, column]) => { + if (skippedColumns.includes(columnId)) { + return; + } const def = operationDefinitionMap[column.operationType]; if (def.getErrorMessage) { return def.getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap); @@ -1218,6 +1244,25 @@ export function isReferenced(layer: IndexPatternLayer, columnId: string): boolea return allReferences.includes(columnId); } +export function getReferencedColumnIds(layer: IndexPatternLayer, columnId: string): string[] { + const referencedIds: string[] = []; + function collect(id: string) { + const column = layer.columns[id]; + if (column && 'references' in column) { + const columnReferences = column.references; + // only record references which have created columns yet + const existingReferences = columnReferences.filter((reference) => + Boolean(layer.columns[reference]) + ); + referencedIds.push(...existingReferences); + existingReferences.forEach(collect); + } + } + collect(columnId); + + return referencedIds; +} + export function isOperationAllowedAsReference({ operationType, validation, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts index 4a2e065269063a..2d7e70179fb3f7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -40,3 +40,28 @@ export const createMockedFullReference = () => { getErrorMessage: jest.fn(), }; }; + +export const createMockedManagedReference = () => { + return { + input: 'managedReference', + displayName: 'Managed reference test', + type: 'managedReference' as OperationType, + selectionStyle: 'full', + buildColumn: jest.fn((args) => { + return { + label: 'Test reference', + isBucketed: false, + dataType: 'number', + + operationType: 'testReference', + references: args.referenceIds, + }; + }), + filterable: true, + isTransferable: jest.fn(), + toExpression: jest.fn().mockReturnValue([]), + getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: jest.fn().mockReturnValue('Default label'), + getErrorMessage: jest.fn(), + }; +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 98dc767c44c7dd..f24c39f810b214 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -88,6 +88,8 @@ export interface IndexPatternPrivateState { isFirstExistenceFetch: boolean; existenceFetchFailed?: boolean; existenceFetchTimeout?: boolean; + + isDimensionClosePrevented?: boolean; } export interface IndexPatternRef { diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 473c170aef2948..07935bb2f241b8 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -166,6 +166,9 @@ export function mockDataPlugin(sessionIdSubject = new Subject()) { nowProvider: { get: jest.fn(), }, + fieldFormats: { + deserialize: jest.fn(), + }, } as unknown) as DataPublicPluginStart; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 33946166186722..b421d57dae6e1f 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -198,6 +198,11 @@ export interface Datasource { } ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + /** + * The datasource is allowed to cancel a close event on the dimension editor, + * mainly used for formulas + */ + canCloseDimensionEditor?: (state: T) => boolean; getCustomWorkspaceRenderer?: ( state: T, dragging: DraggingIdentifier @@ -300,11 +305,15 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro // Not a StateSetter because we have this unique use case of determining valid columns setState: ( newState: Parameters>[0], - publishToVisualization?: { shouldReplaceDimension?: boolean; shouldRemoveDimension?: boolean } + publishToVisualization?: { + isDimensionComplete?: boolean; + } ) => void; core: Pick; dateRange: DateRange; dimensionGroups: VisualizationDimensionGroupConfig[]; + toggleFullscreen: () => void; + isFullscreen: boolean; }; export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; diff --git a/x-pack/plugins/lens/server/usage/schema.ts b/x-pack/plugins/lens/server/usage/schema.ts index ab3945a0162a68..c3608176717c56 100644 --- a/x-pack/plugins/lens/server/usage/schema.ts +++ b/x-pack/plugins/lens/server/usage/schema.ts @@ -14,6 +14,12 @@ const eventsSchema: MakeSchemaFrom = { type: 'long', _meta: { description: 'Number of times the user opened one of the in-product help popovers.' }, }, + toggle_fullscreen_formula: { + type: 'long', + _meta: { + description: 'Number of times the user toggled fullscreen mode on formula.', + }, + }, indexpattern_field_info_click: { type: 'long' }, loaded: { type: 'long' }, app_filters_updated: { type: 'long' }, @@ -162,6 +168,10 @@ const eventsSchema: MakeSchemaFrom = { type: 'long', _meta: { description: 'Number of times the moving average function was selected' }, }, + indexpattern_dimension_operation_formula: { + type: 'long', + _meta: { description: 'Number of times the formula function was selected' }, + }, }; const suggestionEventsSchema: MakeSchemaFrom = { @@ -183,6 +193,12 @@ const savedSchema: MakeSchemaFrom = { lnsDatatable: { type: 'long' }, lnsPie: { type: 'long' }, lnsMetric: { type: 'long' }, + formula: { + type: 'long', + _meta: { + description: 'Number of saved lens visualizations which are using at least one formula', + }, + }, }; export const lensUsageSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts index 3b9bb99caf5b81..f0c48fb1152e81 100644 --- a/x-pack/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts @@ -43,6 +43,31 @@ export async function getVisualizationCounts( size: 100, }, }, + usesFormula: { + filter: { + match: { + operation_type: 'formula', + }, + }, + }, + }, + }, + }, + runtime_mappings: { + operation_type: { + type: 'keyword', + script: { + lang: 'painless', + source: `try { + if(doc['lens.state'].size() == 0) return; + HashMap layers = params['_source'].get('lens').get('state').get('datasourceStates').get('indexpattern').get('layers'); + for(layerId in layers.keySet()) { + HashMap columns = layers.get(layerId).get('columns'); + for(columnId in columns.keySet()) { + emit(columns.get(columnId).get('operationType')) + } + } + } catch(Exception e) {}`, }, }, }, @@ -56,16 +81,19 @@ export async function getVisualizationCounts( // eslint-disable-next-line @typescript-eslint/no-explicit-any function bucketsToObject(arg: any) { const obj: Record = {}; - arg.buckets.forEach((bucket: { key: string; doc_count: number }) => { + arg.byType.buckets.forEach((bucket: { key: string; doc_count: number }) => { obj[bucket.key] = bucket.doc_count + (obj[bucket.key] ?? 0); }); + if (arg.usesFormula.doc_count > 0) { + obj.formula = arg.usesFormula.doc_count; + } return obj; } return { - saved_overall: bucketsToObject(buckets.overall.byType), - saved_30_days: bucketsToObject(buckets.last30.byType), - saved_90_days: bucketsToObject(buckets.last90.byType), + saved_overall: bucketsToObject(buckets.overall), + saved_30_days: bucketsToObject(buckets.last30), + saved_90_days: bucketsToObject(buckets.last90), saved_overall_total: buckets.overall.doc_count, saved_30_days_total: buckets.last30.doc_count, saved_90_days_total: buckets.last90.doc_count, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 7c96dce3fac7f5..8e52450d393b03 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2162,6 +2162,12 @@ "description": "Number of times the user opened one of the in-product help popovers." } }, + "toggle_fullscreen_formula": { + "type": "long", + "_meta": { + "description": "Number of times the user toggled fullscreen mode on formula." + } + }, "indexpattern_field_info_click": { "type": "long" }, @@ -2371,6 +2377,12 @@ "_meta": { "description": "Number of times the moving average function was selected" } + }, + "indexpattern_dimension_operation_formula": { + "type": "long", + "_meta": { + "description": "Number of times the formula function was selected" + } } } }, @@ -2385,6 +2397,12 @@ "description": "Number of times the user opened one of the in-product help popovers." } }, + "toggle_fullscreen_formula": { + "type": "long", + "_meta": { + "description": "Number of times the user toggled fullscreen mode on formula." + } + }, "indexpattern_field_info_click": { "type": "long" }, @@ -2594,6 +2612,12 @@ "_meta": { "description": "Number of times the moving average function was selected" } + }, + "indexpattern_dimension_operation_formula": { + "type": "long", + "_meta": { + "description": "Number of times the formula function was selected" + } } } }, @@ -2666,6 +2690,12 @@ }, "lnsMetric": { "type": "long" + }, + "formula": { + "type": "long", + "_meta": { + "description": "Number of saved lens visualizations which are using at least one formula" + } } } }, @@ -2709,6 +2739,12 @@ }, "lnsMetric": { "type": "long" + }, + "formula": { + "type": "long", + "_meta": { + "description": "Number of saved lens visualizations which are using at least one formula" + } } } }, @@ -2752,6 +2788,12 @@ }, "lnsMetric": { "type": "long" + }, + "formula": { + "type": "long", + "_meta": { + "description": "Number of saved lens visualizations which are using at least one formula" + } } } } diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts new file mode 100644 index 00000000000000..e9e5051c006f02 --- /dev/null +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const find = getService('find'); + const listingTable = getService('listingTable'); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + describe('lens formula', () => { + it('should transition from count to formula', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'average', + field: 'bytes', + keepOpen: true, + }); + + await PageObjects.lens.switchToFormula(); + await PageObjects.header.waitUntilLoadingHasFinished(); + // .echLegendItem__title is the only viable way of getting the xy chart's + // legend item(s), so we're using a class selector here. + // 4th item is the other bucket + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); + }); + + it('should update and delete a formula', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `count(kql=`, + keepOpen: true, + }); + + const input = await find.activeElement(); + await input.type('*'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('14,005'); + }); + + it('should insert single quotes and escape when needed to create valid KQL', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `count(kql=`, + keepOpen: true, + }); + + let input = await find.activeElement(); + await input.type(' '); + await input.pressKeys(browser.keys.ARROW_LEFT); + await input.type(`Men's Clothing`); + + await PageObjects.common.sleep(100); + + let element = await find.byCssSelector('.monaco-editor'); + expect(await element.getVisibleText()).to.equal(`count(kql='Men\\'s Clothing ')`); + + await PageObjects.lens.typeFormula('count(kql='); + input = await find.activeElement(); + await input.type(`Men\'s Clothing`); + + element = await find.byCssSelector('.monaco-editor'); + expect(await element.getVisibleText()).to.equal(`count(kql='Men\\'s Clothing')`); + }); + + it('should persist a broken formula on close', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + // Close immediately + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `asdf`, + }); + + expect(await PageObjects.lens.getErrorCount()).to.eql(1); + }); + + it('should keep the formula when entering expanded mode', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + // Close immediately + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `count()`, + keepOpen: true, + }); + + await PageObjects.lens.toggleFullscreen(); + + const element = await find.byCssSelector('.monaco-editor'); + expect(await element.getVisibleText()).to.equal('count()'); + }); + + it('should allow an empty formula combined with a valid formula', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `count()`, + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + }); + + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getErrorCount()).to.eql(0); + }); + + it('should duplicate a moving average formula and be a valid table', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_rows > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `moving_average(sum(bytes), window=5`, + keepOpen: true, + }); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.dragDimensionToDimension( + 'lnsDatatable_metrics > lns-dimensionTrigger', + 'lnsDatatable_metrics > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDatatableCellText(1, 1)).to.eql('222420'); + expect(await PageObjects.lens.getDatatableCellText(1, 2)).to.eql('222420'); + }); + + it('should keep the formula if the user does not fully transition to a quick function', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'formula', + formula: `count()`, + keepOpen: true, + }); + + await PageObjects.lens.switchToQuickFunctions(); + await testSubjects.click(`lns-indexPatternDimension-min incompatible`); + await PageObjects.common.sleep(1000); + await PageObjects.lens.closeDimensionEditor(); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_metrics', 0)).to.eql( + 'count()' + ); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 1efceace8b1676..99b75bdabe6c4a 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -41,6 +41,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./geo_field')); loadTestFile(require.resolve('./lens_reporting')); loadTestFile(require.resolve('./lens_tagging')); + loadTestFile(require.resolve('./formula')); // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 2a4d56bbea791a..5d775f154c9430 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -172,10 +172,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('indexPattern-dimension-formatDecimals'); + await PageObjects.lens.closeDimensionEditor(); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( 'Test of label' ); - await PageObjects.lens.closeDimensionEditor(); }); it('should be able to add very long labels and still be able to remove a dimension', async () => { @@ -587,6 +588,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); + it('should not leave an incomplete column in the visualization config with field-based operation', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'min', + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + undefined + ); + }); + + it('should not leave an incomplete column in the visualization config with reference-based operations', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'moving_average', + field: 'Records', + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Moving average of Count of records' + ); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'median', + isPreviousIncompatible: true, + keepOpen: true, + }); + + expect(await PageObjects.lens.isDimensionEditorOpen()).to.eql(true); + + await PageObjects.lens.closeDimensionEditor(); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + undefined + ); + }); + it('should transition from unique count to last value', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index c0111afad28932..44aebed17925d3 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -107,6 +107,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont isPreviousIncompatible?: boolean; keepOpen?: boolean; palette?: string; + formula?: string; }, layerIndex = 0 ) { @@ -114,10 +115,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click(`lns-layerPanel-${layerIndex} > ${opts.dimension}`); await testSubjects.exists(`lns-indexPatternDimension-${opts.operation}`); }); - const operationSelector = opts.isPreviousIncompatible - ? `lns-indexPatternDimension-${opts.operation} incompatible` - : `lns-indexPatternDimension-${opts.operation}`; - await testSubjects.click(operationSelector); + + if (opts.operation === 'formula') { + await this.switchToFormula(); + } else { + const operationSelector = opts.isPreviousIncompatible + ? `lns-indexPatternDimension-${opts.operation} incompatible` + : `lns-indexPatternDimension-${opts.operation}`; + await testSubjects.click(operationSelector); + } if (opts.field) { const target = await testSubjects.find('indexPattern-dimension-field'); @@ -125,6 +131,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await comboBox.setElement(target, opts.field); } + if (opts.formula) { + await this.typeFormula(opts.formula); + } + if (opts.palette) { await this.setPalette(opts.palette); } @@ -357,7 +367,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await retry.try(async () => { await testSubjects.click('lns-palettePicker'); const currentPalette = await ( - await find.byCssSelector('[aria-selected=true]') + await find.byCssSelector('[role=option][aria-selected=true]') ).getAttribute('id'); expect(currentPalette).to.equal(palette); }); @@ -379,6 +389,18 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async isDimensionEditorOpen() { + return await testSubjects.exists('lns-indexPattern-dimensionContainerBack'); + }, + + // closes the dimension editor flyout + async closeDimensionEditor() { + await retry.try(async () => { + await testSubjects.click('lns-indexPattern-dimensionContainerBack'); + await testSubjects.missingOrFail('lns-indexPattern-dimensionContainerBack'); + }); + }, + async enableTimeShift() { await testSubjects.click('indexPattern-advanced-popover'); await retry.try(async () => { @@ -398,14 +420,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('errorFixAction'); }, - // closes the dimension editor flyout - async closeDimensionEditor() { - await retry.try(async () => { - await testSubjects.click('lns-indexPattern-dimensionContainerBack'); - await testSubjects.missingOrFail('lns-indexPattern-dimensionContainerBack'); - }); - }, - async isTopLevelAggregation() { return await testSubjects.isEuiSwitchChecked('indexPattern-nesting-switch'); }, @@ -549,7 +563,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); } const errors = await testSubjects.findAll('configuration-failure-error'); - return errors?.length ?? 0; + const expressionErrors = await testSubjects.findAll('expression-failure'); + return (errors?.length ?? 0) + (expressionErrors?.length ?? 0); }, async searchOnChartSwitch(subVisualizationId: string, searchTerm?: string) { @@ -1025,5 +1040,27 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont ); await PageObjects.header.waitUntilLoadingHasFinished(); }, + + async switchToQuickFunctions() { + await testSubjects.click('lens-dimensionTabs-quickFunctions'); + }, + + async switchToFormula() { + await testSubjects.click('lens-dimensionTabs-formula'); + }, + + async toggleFullscreen() { + await testSubjects.click('lnsFormula-fullscreen'); + }, + + async typeFormula(formula: string) { + // Formula takes time to open + await PageObjects.common.sleep(500); + await find.byCssSelector('.monaco-editor'); + await find.clickByCssSelectorWhenNotDisabled('.monaco-editor'); + const input = await find.activeElement(); + await input.clearValueWithKeyboard(); + await input.type(formula); + }, }); } From fe3c761b4056819abab5a943071709f9cd10871a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 10 Jun 2021 16:30:37 +0200 Subject: [PATCH 15/99] stabilize opening of Lens from listing page (#101569) --- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- x-pack/test/functional/page_objects/lens_page.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 61ed2934a40011..a439a3b5788fb8 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -351,7 +351,7 @@ export function App({ return ( <> -
+
{ + await testSubjects.click(`visListingTitleLink-${title}`); + await this.isLensPageOrFail(); + }); }, /** From ef8eb321cf737584c66a7e6cad9cf333e9fba587 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 10 Jun 2021 16:36:14 +0200 Subject: [PATCH 16/99] [ML] Add usage collection for pages in the ML app (#101886) * [ML] setup usageCollection provider * [ML] track page usage --- x-pack/plugins/ml/kibana.json | 3 ++- x-pack/plugins/ml/public/application/app.tsx | 19 ++++++++++++------- .../application/routing/ml_page_wrapper.tsx | 12 ++++++++++++ .../ml/public/application/routing/router.tsx | 5 ++++- x-pack/plugins/ml/public/plugin.ts | 3 +++ 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index f34172765e1ddf..e3bcf307e6f009 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -39,7 +39,8 @@ "dashboard", "savedObjects", "home", - "maps" + "maps", + "usageCollection" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index f16a7c561ac5d6..8be513f372e56c 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -87,17 +87,22 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { }; const I18nContext = coreStart.i18n.Context; + const ApplicationUsageTrackingProvider = + deps.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; + return ( /** RedirectAppLinks intercepts all tags to use navigateToUrl * avoiding full page reload **/ - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx b/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx new file mode 100644 index 00000000000000..e97ab0fb830a59 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx @@ -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. + */ +import React, { FC } from 'react'; +import { TrackApplicationView } from '../../../../../../src/plugins/usage_collection/public'; + +export const MlPageWrapper: FC<{ path: string }> = ({ path, children }) => { + return {children}; +}; diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 27de3781bc7abe..c2129ef18df3a0 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -18,6 +18,7 @@ import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; import * as routes from './routes'; +import { MlPageWrapper } from './ml_page_wrapper'; // custom RouteProps making location non-optional interface MlRouteProps extends RouteProps { @@ -97,7 +98,9 @@ const MlRoutes: FC<{ window.setTimeout(() => { pageDeps.setBreadcrumbs(route.breadcrumbs); }); - return route.render(props, pageDeps); + return ( + {route.render(props, pageDeps)} + ); }} /> ); diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 1191f3b253fd7d..e3a4a8348ebc10 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -52,6 +52,7 @@ import { import { DataVisualizerPluginStart } from '../../data_visualizer/public'; import { PluginSetupContract as AlertingSetup } from '../../alerting/public'; import { registerManagementSection } from './application/management'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -78,6 +79,7 @@ export interface MlSetupDependencies { share: SharePluginSetup; triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup; alerting?: AlertingSetup; + usageCollection?: UsageCollectionSetup; } export type MlCoreSetup = CoreSetup; @@ -121,6 +123,7 @@ export class MlPlugin implements Plugin { kibanaVersion, triggersActionsUi: pluginsStart.triggersActionsUi, dataVisualizer: pluginsStart.dataVisualizer, + usageCollection: pluginsSetup.usageCollection, }, params ); From 305f12708df7837d5fca5e71760c0e98870ec802 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 10 Jun 2021 11:03:17 -0400 Subject: [PATCH 17/99] [ML] Anomaly Detection: Visualize delayed - data Part 1 (#101236) * wip: add datafeed modal for chart * Add arrows to navigate through time in the chart * ensure runtime_mapping and indices_options in search query * move chart data fetching behind single server endpoint * remove success check as it is not returned in result * load necessary modal data in modal * remove extra legend and add text to action icons * remove unused endpoint and types * handle job not found and fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/common/types/results.ts | 22 ++ .../annotations_table/annotations_table.js | 88 ++++- .../components/datafeed_modal/constants.ts | 41 ++ .../datafeed_modal/datafeed_modal.tsx | 362 ++++++++++++++++++ .../datafeed_modal/edit_query_delay.tsx | 142 +++++++ .../datafeed_modal/get_interval_options.ts | 118 ++++++ .../components/datafeed_modal/index.ts | 8 + .../components/jobs_list/jobs_list.js | 2 +- .../jobs/jobs_list/components/utils.d.ts | 3 + .../services/ml_api_service/index.ts | 2 +- .../services/ml_api_service/results.ts | 13 + .../models/results_service/results_service.ts | 113 +++++- x-pack/plugins/ml/server/routes/apidoc.json | 1 + .../ml/server/routes/results_service.ts | 34 ++ .../routes/schemas/results_service_schema.ts | 9 + 15 files changed, 934 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/edit_query_delay.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/index.ts diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts index 83de62d51671ea..fa40cefcaed48d 100644 --- a/x-pack/plugins/ml/common/types/results.ts +++ b/x-pack/plugins/ml/common/types/results.ts @@ -5,6 +5,28 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; + export interface GetStoppedPartitionResult { jobs: string[] | Record; } +export interface GetDatafeedResultsChartDataResult { + bucketResults: number[][]; + datafeedResults: number[][]; +} + +export interface DatafeedResultsChartDataParams { + jobId: string; + start: number; + end: number; +} + +export const defaultSearchQuery: estypes.QueryDslQueryContainer = { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, +}; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 472bde00e649ae..afed7e79ff757f 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -18,14 +18,13 @@ import React, { Component, Fragment, useContext } from 'react'; import memoizeOne from 'memoize-one'; import { EuiBadge, - EuiButtonIcon, + EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLink, EuiLoadingSpinner, - EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -52,6 +51,7 @@ import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ import { PLUGIN_ID } from '../../../../../common/constants/app'; import { timeFormatter } from '../../../../../common/util/date_utils'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; +import { DatafeedModal } from '../../../jobs/jobs_list/components/datafeed_modal'; const CURRENT_SERIES = 'current_series'; /** @@ -79,6 +79,8 @@ class AnnotationsTableUI extends Component { this.props.jobs[0] !== undefined ? this.props.jobs[0].job_id : undefined, + datafeedModalVisible: false, + datafeedEnd: null, }; this.sorting = { sort: { field: 'timestamp', direction: 'asc' }, @@ -463,28 +465,62 @@ class AnnotationsTableUI extends Component { // find the original annotation because the table might not show everything const annotationId = annotation._id; const originalAnnotation = annotations.find((d) => d._id === annotationId); - const editAnnotationsTooltipText = ( + const editAnnotationsText = ( ); - const editAnnotationsTooltipAriaLabelText = i18n.translate( + const editAnnotationsAriaLabelText = i18n.translate( 'xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel', { defaultMessage: 'Edit annotation' } ); return ( - - annotationUpdatesService.setValue(originalAnnotation ?? annotation)} - iconType="pencil" - aria-label={editAnnotationsTooltipAriaLabelText} - /> - + annotationUpdatesService.setValue(originalAnnotation ?? annotation)} + > + {editAnnotationsText} + ); }, }); + if (this.state.jobId && this.props.jobs[0].analysis_config.bucket_span) { + // add datafeed modal action + actions.push({ + render: (annotation) => { + const viewDataFeedText = ( + + ); + const viewDataFeedTooltipAriaLabelText = i18n.translate( + 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel', + { defaultMessage: 'View datafeed' } + ); + return ( + + this.setState({ + datafeedModalVisible: true, + datafeedEnd: annotation.end_timestamp, + }) + } + > + {viewDataFeedText} + + ); + }, + }); + } + if (isSingleMetricViewerLinkVisible) { actions.push({ render: (annotation) => { @@ -510,14 +546,15 @@ class AnnotationsTableUI extends Component { ); return ( - - this.openSingleMetricView(annotation)} - disabled={!isDrillDownAvailable} - iconType="visLine" - aria-label={openInSingleMetricViewerAriaLabelText} - /> - + this.openSingleMetricView(annotation)} + > + {openInSingleMetricViewerTooltipText} + ); }, }); @@ -690,6 +727,19 @@ class AnnotationsTableUI extends Component { search={search} rowProps={getRowProps} /> + {this.state.jobId && this.state.datafeedModalVisible && this.state.datafeedEnd ? ( + { + this.setState({ + datafeedModalVisible: false, + }); + }} + end={this.state.datafeedEnd} + timefield={this.props.jobs[0].data_description.time_field} + jobId={this.state.jobId} + bucketSpan={this.props.jobs[0].analysis_config.bucket_span} + /> + ) : null} ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts new file mode 100644 index 00000000000000..71f3795518bc95 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChartSizeArray } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; + +export const CHART_DIRECTION = { + FORWARD: 'forward', + BACK: 'back', +} as const; +export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION]; + +// [width, height] +export const CHART_SIZE: ChartSizeArray = ['100%', 300]; + +export const TAB_IDS = { + CHART: 'chart', + MESSAGES: 'messages', +} as const; +export type TabIdsType = typeof TAB_IDS[keyof typeof TAB_IDS]; + +export const tabs = [ + { + id: TAB_IDS.CHART, + name: i18n.translate('xpack.ml.jobsList.datafeedModal.chartTabName', { + defaultMessage: 'Chart', + }), + disabled: false, + }, + { + id: TAB_IDS.MESSAGES, + name: i18n.translate('xpack.ml.jobsList.datafeedModal.messagesTabName', { + defaultMessage: 'Messages', + }), + disabled: false, + }, +]; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx new file mode 100644 index 00000000000000..cf547a49cac4c3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx @@ -0,0 +1,362 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { + EuiButtonEmpty, + EuiDatePicker, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingChart, + EuiModal, + EuiModalHeader, + EuiModalBody, + EuiSelect, + EuiSpacer, + EuiTabs, + EuiTab, + EuiToolTip, +} from '@elastic/eui'; +import { + Axis, + Chart, + CurveType, + LineSeries, + Position, + ScaleType, + Settings, + timeFormatter, +} from '@elastic/charts'; + +import { DATAFEED_STATE } from '../../../../../../common/constants/states'; +import { CombinedJobWithStats } from '../../../../../../common/types/anomaly_detection_jobs'; +import { useToastNotificationService } from '../../../../services/toast_notification_service'; +import { useMlApiContext } from '../../../../contexts/kibana'; +import { useCurrentEuiTheme } from '../../../../components/color_range_legend'; +import { JobMessagesPane } from '../job_details/job_messages_pane'; +import { EditQueryDelay } from './edit_query_delay'; +import { getIntervalOptions } from './get_interval_options'; +import { + CHART_DIRECTION, + ChartDirectionType, + CHART_SIZE, + tabs, + TAB_IDS, + TabIdsType, +} from './constants'; +import { loadFullJob } from '../utils'; + +const dateFormatter = timeFormatter('MM-DD HH:mm'); + +interface DatafeedModalProps { + jobId: string; + end: number; + onClose: (deletionApproved?: boolean) => void; +} + +export const DatafeedModal: FC = ({ jobId, end, onClose }) => { + const [data, setData] = useState<{ + datafeedConfig: CombinedJobWithStats['datafeed_config'] | undefined; + bucketSpan: string | undefined; + isInitialized: boolean; + }>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false }); + const [endDate, setEndDate] = useState(moment(end)); + const [interval, setInterval] = useState(); + const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.CHART); + const [isLoadingChartData, setIsLoadingChartData] = useState(false); + const [bucketData, setBucketData] = useState([]); + const [sourceData, setSourceData] = useState([]); + + const { + results: { getDatafeedResultChartData }, + } = useMlApiContext(); + const { displayErrorToast } = useToastNotificationService(); + const { euiTheme } = useCurrentEuiTheme(); + + const onSelectedTabChanged = (id: TabIdsType) => { + setSelectedTabId(id); + }; + + const renderTabs = useCallback( + () => + tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + key={index} + > + {tab.name} + + )), + [selectedTabId] + ); + + const handleChange = (date: moment.Moment) => setEndDate(date); + + const handleEndDateChange = (direction: ChartDirectionType) => { + if (interval === undefined) return; + + const newEndDate = endDate.clone(); + const [count, type] = interval.split(' '); + + if (direction === CHART_DIRECTION.FORWARD) { + newEndDate.add(Number(count), type); + } else { + newEndDate.subtract(Number(count), type); + } + setEndDate(newEndDate); + }; + + const getChartData = useCallback(async () => { + if (interval === undefined) return; + + const endTimestamp = moment(endDate).valueOf(); + const [count, type] = interval.split(' '); + const startMoment = endDate.clone().subtract(Number(count), type); + const startTimestamp = moment(startMoment).valueOf(); + + try { + const chartData = await getDatafeedResultChartData(jobId, startTimestamp, endTimestamp); + + setSourceData(chartData.datafeedResults); + setBucketData(chartData.bucketResults); + } catch (error) { + const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', { + defaultMessage: 'Error fetching data', + }); + displayErrorToast(error, title); + } + setIsLoadingChartData(false); + }, [endDate, interval]); + + const getJobData = async () => { + try { + const job: CombinedJobWithStats = await loadFullJob(jobId); + setData({ + datafeedConfig: job.datafeed_config, + bucketSpan: job.analysis_config.bucket_span, + isInitialized: true, + }); + const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span); + const initialInterval = intervalOptions.length + ? intervalOptions[intervalOptions.length - 1] + : undefined; + setInterval(initialInterval?.value || '72 hours'); + } catch (error) { + displayErrorToast(error); + } + }; + + useEffect(function loadJobWithDatafeed() { + getJobData(); + }, []); + + useEffect( + function loadChartData() { + if (interval !== undefined) { + setIsLoadingChartData(true); + getChartData(); + } + }, + [endDate, interval] + ); + + const { datafeedConfig, bucketSpan, isInitialized } = data; + + const intervalOptions = useMemo(() => { + if (bucketSpan === undefined) return []; + return getIntervalOptions(bucketSpan); + }, [bucketSpan]); + + return ( + + + + + + + + + + + + + + {renderTabs()} + + {isLoadingChartData || isInitialized === false ? : null} + {!isLoadingChartData && + isInitialized && + selectedTabId === TAB_IDS.CHART && + datafeedConfig !== undefined && + bucketSpan && ( + + + + + setInterval(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.jobsList.datafeedModal.intervalSelection', + { + defaultMessage: 'Datafeed modal chart interval selection', + } + )} + /> + + + + + + + + + + + } + > + { + handleEndDateChange(CHART_DIRECTION.BACK); + }} + iconType="arrowLeft" + /> + + + + + + + + + + + + + + } + > + { + handleEndDateChange(CHART_DIRECTION.FORWARD); + }} + iconType="arrowRight" + /> + + + + + + )} + {!isLoadingChartData && selectedTabId === TAB_IDS.MESSAGES ? ( + + ) : null} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/edit_query_delay.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/edit_query_delay.tsx new file mode 100644 index 00000000000000..5a25781a505d15 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/edit_query_delay.tsx @@ -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 React, { FC, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiToolTip, +} from '@elastic/eui'; + +import { useMlApiContext } from '../../../../contexts/kibana'; +import { useToastNotificationService } from '../../../../services/toast_notification_service'; +import { Datafeed } from '../../../../../../common/types/anomaly_detection_jobs'; + +const tooltipContent = i18n.translate( + 'xpack.ml.jobsList.datafeedModal.editQueryDelay.tooltipContent', + { + defaultMessage: 'Cannot update query delay when datafeed is running.', + } +); + +export const EditQueryDelay: FC<{ + datafeedId: Datafeed['datafeed_id']; + queryDelay: Datafeed['query_delay']; + isEnabled: boolean; +}> = ({ datafeedId, queryDelay, isEnabled }) => { + const [newQueryDelay, setNewQueryDelay] = useState(); + const [isEditing, setIsEditing] = useState(false); + const { updateDatafeed } = useMlApiContext(); + const { displaySuccessToast, displayErrorToast } = useToastNotificationService(); + + const updateQueryDelay = useCallback(async () => { + try { + await updateDatafeed({ + datafeedId, + datafeedConfig: { query_delay: newQueryDelay }, + }); + displaySuccessToast( + i18n.translate( + 'xpack.ml.jobsList.datafeedModal.editQueryDelay.changesSavedNotificationMessage', + { + defaultMessage: 'Changes to query delay for {datafeedId} saved', + values: { + datafeedId, + }, + } + ) + ); + } catch (error) { + displayErrorToast( + error, + i18n.translate( + 'xpack.ml.jobsList.datafeedModal.editQueryDelay.changesNotSavedNotificationMessage', + { + defaultMessage: 'Could not save changes to query delay for {datafeedId}', + values: { + datafeedId, + }, + } + ) + ); + } + setIsEditing(false); + }, [datafeedId, newQueryDelay]); + + const editButton = ( + { + setIsEditing(true); + }} + iconType="pencil" + > + + + ); + + const editButtonWithTooltip = {editButton}; + + return ( + <> + {isEditing === false ? (isEnabled === false ? editButtonWithTooltip : editButton) : null} + {isEditing === true ? ( + + } + > + + + { + setNewQueryDelay(e.target.value); + }} + /> + + + + + + + + + + setIsEditing(false)}> + + + + + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts new file mode 100644 index 00000000000000..cc9431549c79c6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const getIntervalOptions = (bucketSpan: string) => { + const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!; + const unit = unitMatch[0]; + const count = Number(bucketSpan.replace(/[^0-9]/g, '')); + + const intervalOptions = []; + + if (['s', 'ms', 'micros', 'nanos'].includes(unit)) { + intervalOptions.push( + { + value: '1 hour', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', { + defaultMessage: '{count} hour', + values: { count: 1 }, + }), + }, + { + value: '2 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', { + defaultMessage: '{count} hours', + values: { count: 2 }, + }), + } + ); + } + + if ((unit === 'm' && count <= 4) || unit === 'h') { + intervalOptions.push( + { + value: '3 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', { + defaultMessage: '{count} hours', + values: { count: 3 }, + }), + }, + { + value: '8 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', { + defaultMessage: '{count} hours', + values: { count: 8 }, + }), + }, + { + value: '12 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', { + defaultMessage: '{count} hours', + values: { count: 12 }, + }), + }, + { + value: '24 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', { + defaultMessage: '{count} hours', + values: { count: 24 }, + }), + } + ); + } + + if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') { + intervalOptions.push( + { + value: '48 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', { + defaultMessage: '{count} hours', + values: { count: 48 }, + }), + }, + { + value: '72 hours', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', { + defaultMessage: '{count} hours', + values: { count: 72 }, + }), + } + ); + } + + if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') { + intervalOptions.push( + { + value: '5 days', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', { + defaultMessage: '{count} days', + values: { count: 5 }, + }), + }, + { + value: '7 days', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', { + defaultMessage: '{count} days', + values: { count: 7 }, + }), + } + ); + } + + if (unit === 'h' || unit === 'd') { + intervalOptions.push({ + value: '14 days', + text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', { + defaultMessage: '{count} days', + values: { count: 14 }, + }), + }); + } + + return intervalOptions; +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/index.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/index.ts new file mode 100644 index 00000000000000..68c50c6663e1d9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DatafeedModal } from './datafeed_modal'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index bd85420397218d..cc6db66dc1cfd4 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -21,8 +21,8 @@ import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; import { EuiBasicTable, EuiButtonIcon, - EuiScreenReaderOnly, EuiIcon, + EuiScreenReaderOnly, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 82cbf8424973c3..76e1e87312a4a8 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -5,4 +5,7 @@ * 2.0. */ +import { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; + export function deleteJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise; +export function loadFullJob(jobId: string): Promise; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index d66805b4f737f3..1bfc597ba0b10f 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -235,7 +235,7 @@ export function mlApiServicesProvider(httpService: HttpService) { datafeedConfig, }: { datafeedId: string; - datafeedConfig: Datafeed; + datafeedConfig: Partial; }) { const body = JSON.stringify(datafeedConfig); return httpService.http({ diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts index 76d6c19c165a18..19ba5aa304bf04 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts @@ -141,4 +141,17 @@ export const resultsApiProvider = (httpService: HttpService) => ({ body, }); }, + + getDatafeedResultChartData(jobId: string, start: number, end: number) { + const body = JSON.stringify({ + jobId, + start, + end, + }); + return httpService.http({ + path: `${basePath()}/results/datafeed_results_chart`, + method: 'POST', + body, + }); + }, }); diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 225a988298b1cc..9413ee00184d20 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { sortBy, slice, get } from 'lodash'; +import { sortBy, slice, get, cloneDeep } from 'lodash'; import moment from 'moment'; import Boom from '@hapi/boom'; +import { IScopedClusterClient } from 'kibana/server'; import { buildAnomalyTableItems } from './build_anomaly_table_items'; import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; import { getPartitionFieldsValuesFactory } from './get_partition_fields_values'; @@ -17,9 +18,15 @@ import { AnomalyRecordDoc, } from '../../../common/types/anomalies'; import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../common/constants/anomalies'; -import { GetStoppedPartitionResult } from '../../../common/types/results'; +import { + GetStoppedPartitionResult, + GetDatafeedResultsChartDataResult, + defaultSearchQuery, + DatafeedResultsChartDataParams, +} from '../../../common/types/results'; import { MlJobsResponse } from '../../../common/types/job_service'; import type { MlClient } from '../../lib/ml_client'; +import { datafeedsProvider } from '../job_service/datafeeds'; // Service for carrying out Elasticsearch queries to obtain data for the // ML Results dashboards. @@ -37,7 +44,7 @@ interface Influencer { fieldValue: any; } -export function resultsServiceProvider(mlClient: MlClient) { +export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClusterClient) { // Obtains data for the anomalies table, aggregating anomalies by day or hour as requested. // Return an Object with properties 'anomalies' and 'interval' (interval used to aggregate anomalies, // one of day, hour or second. Note 'auto' can be provided as the aggregationInterval in the request, @@ -605,6 +612,105 @@ export function resultsServiceProvider(mlClient: MlClient) { return finalResults; } + async function getDatafeedResultsChartData({ + jobId, + start, + end, + }: DatafeedResultsChartDataParams): Promise { + const finalResults: GetDatafeedResultsChartDataResult = { + bucketResults: [], + datafeedResults: [], + }; + + const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient); + const datafeedConfig = await getDatafeedByJobId(jobId); + + const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId }); + if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { + throw Boom.notFound(`Job with the id "${jobId}" not found`); + } + + const jobConfig = jobsResponse.jobs[0]; + const timefield = jobConfig.data_description.time_field; + const bucketSpan = jobConfig.analysis_config.bucket_span; + + if (datafeedConfig === undefined) { + return finalResults; + } + + const rangeFilter = { + range: { + [timefield]: { gte: start, lte: end }, + }, + }; + + let datafeedQueryClone = + datafeedConfig.query !== undefined ? cloneDeep(datafeedConfig.query) : defaultSearchQuery; + + if (datafeedQueryClone.bool !== undefined) { + if (datafeedQueryClone.bool.filter === undefined) { + datafeedQueryClone.bool.filter = []; + } + if (Array.isArray(datafeedQueryClone.bool.filter)) { + datafeedQueryClone.bool.filter.push(rangeFilter); + } else { + // filter is an object so convert to array so we can add the rangeFilter + const filterQuery = cloneDeep(datafeedQueryClone.bool.filter); + datafeedQueryClone.bool.filter = [filterQuery, rangeFilter]; + } + } else { + // Not a bool query so convert to a bool query so we can add the range filter + datafeedQueryClone = { bool: { must: [datafeedQueryClone], filter: [rangeFilter] } }; + } + + const esSearchRequest = { + index: datafeedConfig.indices.join(','), + body: { + query: datafeedQueryClone, + ...(datafeedConfig.runtime_mappings + ? { runtime_mappings: datafeedConfig.runtime_mappings } + : {}), + aggs: { + doc_count_by_bucket_span: { + date_histogram: { + field: timefield, + fixed_interval: bucketSpan, + }, + }, + }, + size: 0, + }, + ...(datafeedConfig?.indices_options ?? {}), + }; + + if (client) { + const { + body: { aggregations }, + } = await client.asCurrentUser.search(esSearchRequest); + + finalResults.datafeedResults = + // @ts-expect-error incorrect search response type + aggregations?.doc_count_by_bucket_span?.buckets.map((result) => [ + result.key, + result.doc_count, + ]) || []; + } + + const bucketResp = await mlClient.getBuckets({ + job_id: jobId, + body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } }, + }); + + const bucketResults = bucketResp?.body?.buckets ?? []; + bucketResults.forEach((dataForTime) => { + const timestamp = Number(dataForTime?.timestamp); + const eventCount = dataForTime?.event_count; + finalResults.bucketResults.push([timestamp, eventCount]); + }); + + return finalResults; + } + return { getAnomaliesTableData, getCategoryDefinition, @@ -614,5 +720,6 @@ export function resultsServiceProvider(mlClient: MlClient) { getPartitionFieldsValues: getPartitionFieldsValuesFactory(mlClient), getCategorizerStats, getCategoryStoppedPartitions, + getDatafeedResultsChartData, }; } diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 16cd3ea8df6295..ba712583f1b61e 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -48,6 +48,7 @@ "ResultsService", "GetAnomaliesTableData", + "GetDatafeedResultsChartData", "GetCategoryDefinition", "GetMaxAnomalyScore", "GetCategoryExamples", diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 1962463a066778..2cb34ce357fea5 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -11,6 +11,7 @@ import { anomaliesTableDataSchema, categoryDefinitionSchema, categoryExamplesSchema, + getDatafeedResultsChartDataSchema, maxAnomalyScoreSchema, partitionFieldValuesSchema, anomalySearchSchema, @@ -352,4 +353,37 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization } }) ); + + /** + * @apiGroup ResultsService + * + * @api {post} /api/ml/results/datafeed_results_chart Get datafeed results chart data + * @apiName GetDatafeedResultsChartData + * @apiDescription Returns datafeed results chart data + * + * @apiSchema (body) getDatafeedResultsChartDataSchema + */ + router.post( + { + path: '/api/ml/results/datafeed_results_chart', + validate: { + body: getDatafeedResultsChartDataSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + try { + const { getDatafeedResultsChartData } = resultsServiceProvider(mlClient, client); + const resp = await getDatafeedResultsChartData(request.body); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index 175300f88166a6..df4a56b06410b2 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -103,3 +103,12 @@ export const getCategorizerStoppedPartitionsSchema = schema.object({ */ fieldToBucket: schema.maybe(schema.string()), }); + +export const getDatafeedResultsChartDataSchema = schema.object({ + /** + * Job id to fetch the bucket results for + */ + jobId: schema.string(), + start: schema.number(), + end: schema.number(), +}); From 85fa7c879110e5861d9704e4f9c4bc3e69821aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 10 Jun 2021 17:37:57 +0200 Subject: [PATCH 18/99] [ML] Adds popover help for multiclass confusion matrix (#101732) Co-authored-by: Lisa Cawley --- .../confusion_matrix_help_popover.tsx | 61 +++++++++++++++++++ .../evaluate_panel.tsx | 14 +---- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 4 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/confusion_matrix_help_popover.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/confusion_matrix_help_popover.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/confusion_matrix_help_popover.tsx new file mode 100644 index 00000000000000..34fad4d8acb45e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/confusion_matrix_help_popover.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + HelpPopover, + HelpPopoverButton, +} from '../../../../../components/help_popover/help_popover'; + +export const MulticlassConfusionMatrixHelpPopover = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + { + setIsPopoverOpen(!isPopoverOpen); + }} + /> + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.ml.dataframe.analytics.confusionMatrixPopoverTitle', { + defaultMessage: 'Normalized confusion matrix', + })} + > +

+ +

+

+ +

+

+ +

+

+ +

+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index f37be6f6f0eb0e..bc1c9dbed1dcc7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -50,6 +50,7 @@ import { import { isTrainingFilter } from './is_training_filter'; import { useRocCurve } from './use_roc_curve'; import { useConfusionMatrix } from './use_confusion_matrix'; +import { MulticlassConfusionMatrixHelpPopover } from './confusion_matrix_help_popover'; export interface EvaluatePanelProps { jobConfig: DataFrameAnalyticsConfig; @@ -288,21 +289,12 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se {errorConfusionMatrix !== null && } {errorConfusionMatrix === null && ( <> - + {getHelpText(dataSubsetTitle)} - + {/* BEGIN TABLE ELEMENTS */} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8066ca178d34f8..573e101a7a63b9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14048,7 +14048,6 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分類混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "予測されたクラス", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText": "データセットをテストするための正規化された混同行列", - "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText": "データセットを学習するための正規化された混同行列", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobStatusLabel": "ジョブ状態", "xpack.ml.dataframe.analytics.classificationExploration.evaluateSectionAvgRecallTooltip": "平均再現率は、実際のクラスメンバーのデータポイントのうち正しくクラスメンバーとして特定された数を示します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 33f5d4aceb51c1..7ae9f30b8a394d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14230,7 +14230,6 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分类混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "预测类", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText": "用于测试数据集的标准化混淆矩阵", - "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText": "用于训练数据集的标准化混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobStatusLabel": "作业状态", "xpack.ml.dataframe.analytics.classificationExploration.evaluateSectionAvgRecallTooltip": "平均召回率显示作为实际类成员的数据点有多少个已被正确标识为类成员。", From 7dfb086343b3518f2d07ec08cbb928313fcbfb69 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 10 Jun 2021 11:50:08 -0400 Subject: [PATCH 19/99] [Upgrade Assistant] Migrate to new page layout (#101691) --- .../client_integration/kibana.test.ts | 6 +- .../components/coming_soon_prompt.tsx | 2 +- .../es_deprecations/es_deprecations.tsx | 92 ++++++------ .../kibana_deprecation_errors.tsx | 65 ++++++--- .../kibana_deprecations.tsx | 103 ++++++-------- .../components/overview/overview.tsx | 132 +++++++++--------- 6 files changed, 198 insertions(+), 202 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts index 867440ae0d911b..b14ec26e5c8af1 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts @@ -182,9 +182,7 @@ describe('Kibana deprecations', () => { component.update(); expect(exists('kibanaRequestError')).toBe(true); - expect(find('kibanaRequestError').text()).toContain( - 'Could not retrieve Kibana deprecations.' - ); + expect(find('kibanaRequestError').text()).toContain('Could not retrieve Kibana deprecations'); }); test('handles deprecation service error', async () => { @@ -217,7 +215,7 @@ describe('Kibana deprecations', () => { // Verify top-level callout renders expect(exists('kibanaPluginError')).toBe(true); expect(find('kibanaPluginError').text()).toContain( - 'Not all Kibana deprecations were retrieved successfully.' + 'Not all Kibana deprecations were retrieved successfully' ); // Open all deprecations diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx index b7869226a84b0c..14627f0b138b00 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx @@ -16,7 +16,7 @@ export const ComingSoonPrompt: React.FunctionComponent = () => { const { ELASTIC_WEBSITE_URL } = docLinks; return ( - + - - - {i18nTexts.docLinkText} - , - ]} - > - - - {i18nTexts.backupDataButton.label} - - - - - - tab.id === tabName)} - /> - - - + <> + + {i18nTexts.docLinkText} + , + ]} + > + + + {i18nTexts.backupDataButton.label} + + + + + + + tab.id === tabName)} + /> + ); } ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx index e6ba83919c31b2..79ada21941b56a 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx @@ -7,44 +7,67 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; interface Props { errorType: 'pluginError' | 'requestError'; } const i18nTexts = { - pluginError: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorMessage', { - defaultMessage: - 'Not all Kibana deprecations were retrieved successfully. This list may be incomplete. Check the Kibana server logs for errors.', - }), - loadingError: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorMessage', - { - defaultMessage: - 'Could not retrieve Kibana deprecations. Check the Kibana server logs for errors.', - } - ), + pluginError: { + title: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle', { + defaultMessage: 'Not all Kibana deprecations were retrieved successfully', + }), + description: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription', + { + defaultMessage: 'Check the Kibana server logs for errors.', + } + ), + }, + loadingError: { + title: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle', { + defaultMessage: 'Could not retrieve Kibana deprecations', + }), + description: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription', + { + defaultMessage: 'Check the Kibana server logs for errors.', + } + ), + }, }; export const KibanaDeprecationErrors: React.FunctionComponent = ({ errorType }) => { if (errorType === 'pluginError') { return ( - + > + {i18nTexts.pluginError.title}

  • CKFL*dZH3@wU$Lra#x-6xMA#u$bA5ZsenTg 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 out-of-the box to notify you of -potential issues in the {stack}. These alerts are preconfigured based on the +<> out-of-the box to notify you +of potential issues in the {stack}. These rules are preconfigured based on the best practices recommended by Elastic. However, you can tailor them to meet your specific needs. -When you open *{stack-monitor-app}*, the preconfigured {kib} alerts are -created automatically. If you collect monitoring data from multiple clusters, -these alerts can search, detect, and notify on various conditions across the -clusters. The alerts are visible alongside your existing {watcher} cluster -alerts. You can view details about the alerts that are active and view health -and performance data for {es}, {ls}, and Beats in real time, as well as -analyze past performance. You can also modify active alerts. +[role="screenshot"] +image::user/monitoring/images/monitoring-kibana-alerts.png["{kib} alerts in {stack-monitor-app}"] + +When you open *{stack-monitor-app}*, the preconfigured rules are created +automatically. They are initially configured to detect and notify on various +conditions across your monitored clusters. You can view notifications for: *Cluster health*, *Resource utilization*, and *Errors and exceptions* for {es} +in real time. + +NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have +been recreated as rules in {kib} {alert-features}. For this reason, the existing +{watcher} email action +`monitoring.cluster_alerts.email_notifications.email_address` no longer works. +The default action for all {stack-monitor-app} rules is to write to {kib} logs +and display a notification in the UI. [role="screenshot"] -image::user/monitoring/images/monitoring-kibana-alerts.png["Kibana alerts in the Stack Monitoring app"] +image::user/monitoring/images/monitoring-kibana-alerting-notification.png["{kib} alerting notifications in {stack-monitor-app}"] -To review and modify all the available alerts, use -<> in *{stack-manage-app}*. + +[role="screenshot"] +image::user/monitoring/images/monitoring-kibana-alerting-setup-mode.png["Modify {kib} alerting rules in {stack-monitor-app}"] [discrete] [[kibana-alerts-cpu-threshold]] -== CPU threshold +== CPU usage threshold -This alert is triggered when a node runs a consistently high CPU load. By -default, the trigger condition is set at 85% or more averaged over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. +This rule checks for {es} nodes that run a consistently high CPU load. By +default, the condition is set at 85% or more averaged over the last 5 minutes. +The rule is grouped across all the nodes of the cluster by running checks on a +schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-disk-usage-threshold]] == Disk usage threshold -This alert is triggered when a node is nearly at disk capacity. By -default, the trigger condition is set at 80% or more averaged over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. +This rule checks for {es} nodes that are nearly at disk capacity. By default, +the condition is set at 80% or more averaged over the last 5 minutes. The rule +is grouped across all the nodes of the cluster by running checks on a schedule +time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-jvm-memory-threshold]] == JVM memory threshold -This alert is triggered when a node runs a consistently high JVM memory usage. By -default, the trigger condition is set at 85% or more averaged over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. +This rule checks for {es} nodes that use a high amount of JVM memory. By +default, the condition is set at 85% or more averaged over the last 5 minutes. +The rule is grouped across all the nodes of the cluster by running checks on a +schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-missing-monitoring-data]] == Missing monitoring data -This alert is triggered when any stack products nodes or instances stop sending -monitoring data. By default, the trigger condition is set to missing for 15 minutes -looking back 1 day. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +This rule checks for {es} nodes that stop sending monitoring data. By default, +the condition is set to missing for 15 minutes looking back 1 day. The rule is +grouped across all the {es} nodes of the cluster by running checks on a schedule +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-thread-pool-rejections]] == Thread pool rejections (search/write) -This alert is triggered when a node experiences thread pool rejections. By -default, the trigger condition is set at 300 or more over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. -Thresholds can be set independently for `search` and `write` type rejections. +This rule checks for {es} nodes that experience thread pool rejections. By +default, the condition is set at 300 or more over the last 5 minutes. The rule +is grouped across all the nodes of the cluster by running checks on a schedule +time of 1 minute with a re-notify interval of 1 day. Thresholds can be set +independently for `search` and `write` type rejections. [discrete] [[kibana-alerts-ccr-read-exceptions]] == CCR read exceptions -This alert is triggered if a read exception has been detected on any of the -replicated clusters. The trigger condition is met if 1 or more read exceptions -are detected in the last hour. The alert is grouped across all replicated clusters -by running checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +This rule checks for read exceptions on any of the replicated {es} clusters. The +condition is met if 1 or more read exceptions are detected in the last hour. The +rule is grouped across all replicated clusters by running checks on a schedule +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-large-shard-size]] == Large shard size -This alert is triggered if a large average shard size (across associated primaries) is found on any of the -specified index patterns. The trigger condition is met if an index's average shard size is -55gb or higher in the last 5 minutes. The alert is grouped across all indices that match -the default pattern of `*` by running checks on a schedule time of 1 minute with a re-notify -interval of 12 hours. +This rule checks for a large average shard size (across associated primaries) on +any of the specified index patterns in an {es} cluster. The condition is met if +an index's average shard size is 55gb or higher in the last 5 minutes. The rule +is grouped across all indices that match the default pattern of `-.*` by running +checks on a schedule time of 1 minute with a re-notify interval of 12 hours. [discrete] [[kibana-alerts-cluster-alerts]] -== Cluster alerts +== Cluster alerting -These alerts summarize the current status of your {stack}. You can drill down into the metrics -to view more information about your cluster and specific nodes, instances, and indices. +These rules check the current status of your {stack}. You can drill down into +the metrics to view more information about your cluster and specific nodes, instances, and indices. -An alert will be triggered if any of the following conditions are met within the last minute: +An action is triggered if any of the following conditions are met within the +last minute: * {es} cluster health status is yellow (missing at least one replica) or red (missing at least one primary). @@ -110,7 +119,7 @@ versions reporting stats to the same monitoring cluster. -- If you do not preserve the data directory when upgrading a {kib} or Logstash node, the instance is assigned a new persistent UUID and shows up -as a new instance +as a new instance. -- * Subscription license expiration. When the expiration date approaches, you will get notifications with a severity level relative to how From f28777f3baff3bef88132a95a4e3ca1011603263 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 10 Jun 2021 19:04:37 -0500 Subject: [PATCH 39/99] [Enterprise Search] Add owner and description properties to kibana.json (#101957) * [Enterprise Search] Add owner and description properties to kibana.json Adds owner and description properties to kibana.json * Reorder to match other plugins Both others have the props at the end of the file: https://github.com/elastic/kibana/blob/master/src/plugins/bfetch/kibana.json Also removes redundant prefix from description * Copy change Co-authored-by: Constance Co-authored-by: Constance --- x-pack/plugins/enterprise_search/kibana.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index daae8cf57f63da..a7b29a1e6b457f 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -7,5 +7,10 @@ "optionalPlugins": ["usageCollection", "security", "home", "spaces", "cloud"], "server": true, "ui": true, - "requiredBundles": ["home"] + "requiredBundles": ["home"], + "owner": { + "name": "Enterprise Search", + "githubTeam": "enterprise-search-frontend" + }, + "description": "Adds dashboards for discovering and managing Enterprise Search products" } From bdaea87aa65ea0a5efe588d6bd643e1e17c9ed4b Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 10 Jun 2021 18:16:15 -0700 Subject: [PATCH 40/99] [kbnArchiver] fix save to non-existent file (#101974) Co-authored-by: spalger --- .../src/kbn_client/kbn_client_import_export.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index 5fd30929fecf68..88953cdbaed7c9 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -48,7 +48,12 @@ export class KbnClientImportExport { path = `${path}.json`; } - const absolutePath = Path.resolve(this.baseDir, path); + return Path.resolve(this.baseDir, path); + } + + private resolveAndValidatePath(path: string) { + const absolutePath = this.resolvePath(path); + if (!existsSync(absolutePath)) { throw new Error( `unable to resolve path [${path}] to import/export, resolved relative to [${this.baseDir}]` @@ -59,7 +64,7 @@ export class KbnClientImportExport { } async load(path: string, options?: { space?: string }) { - const src = this.resolvePath(path); + const src = this.resolveAndValidatePath(path); this.log.debug('resolved import for', path, 'to', src); const objects = await parseArchive(src); @@ -94,7 +99,7 @@ export class KbnClientImportExport { } async unload(path: string, options?: { space?: string }) { - const src = this.resolvePath(path); + const src = this.resolveAndValidatePath(path); this.log.debug('unloading docs from archive at', src); const objects = await parseArchive(src); @@ -143,6 +148,7 @@ export class KbnClientImportExport { }) .join('\n\n'); + await Fs.mkdir(Path.dirname(dest), { recursive: true }); await Fs.writeFile(dest, fileContents, 'utf-8'); this.log.success('Exported', objects.length, 'saved objects to', dest); From 9c37a6798f9f7205fb71324f69b30181fc1f685d Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 10 Jun 2021 20:47:09 -0700 Subject: [PATCH 41/99] [esArchives] Persist migrated Kibana archives (#101950) Signed-off-by: Tyler Smalley --- .../dashboard/current/kibana/data.json | 3471 +++++++++++++++++ .../dashboard/current/kibana/data.json.gz | Bin 20860 -> 0 bytes .../dashboard/current/kibana/mappings.json | 236 +- .../deprecations_service/data.json | 16 +- .../deprecations_service/mappings.json | 320 +- .../fixtures/es_archiver/discover/data.json | 37 +- .../es_archiver/discover/mappings.json | 322 +- .../export_transform/data.json | 114 +- .../export_transform/mappings.json | 491 +-- .../hidden_saved_objects/data.json | 30 +- .../hidden_saved_objects/mappings.json | 513 +-- .../nested_export_transform/data.json | 78 +- .../nested_export_transform/mappings.json | 491 +-- .../fixtures/es_archiver/visualize/data.json | 268 +- .../es_archiver/visualize/mappings.json | 321 +- 15 files changed, 5485 insertions(+), 1223 deletions(-) create mode 100644 test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json delete mode 100644 test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json new file mode 100644 index 00000000000000..139da89e58d128 --- /dev/null +++ b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json @@ -0,0 +1,3471 @@ +{ + "type": "doc", + "value": { + "id": "search:a16d1990-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "animal", + "isDog", + "name", + "sound", + "weightLbs" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:>40\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "weightLbs", + "desc" + ] + ], + "title": "animal weights", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-11T20:55:26.317Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.3.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-11T20:43:55.434Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:61c58ad0-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "dashboard with filter", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "2:panel_2", + "type": "search" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:2ae34a60-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"}]", + "timeRestore": false, + "title": "couple panels", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "145ced90-3dcb-11e8-8660-4d65aa086b3c", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "e2023110-3dcb-11e8-8660-4d65aa086b3c", + "name": "2:panel_2", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:03:29.670Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:76d03330-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "and_descriptions_has_underscores", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "dashboard_with_underscores", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:58:27.555Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:9b780cd0-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "* hi & $%!!@# 漢字 ^--=++[]{};'~`~<>?,./:\";'\\|\\\\ special chars", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:00:07.322Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6c0b16e0-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "dashboard-name-has-dashes", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:58:09.486Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:19523860-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "im empty too", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:03:00.198Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:14616b50-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "im empty", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:02:51.909Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:33bb8ad0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"}]", + "timeRestore": false, + "title": "few panels", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "145ced90-3dcb-11e8-8660-4d65aa086b3c", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "e2023110-3dcb-11e8-8660-4d65aa086b3c", + "name": "2:panel_2", + "type": "visualization" + }, + { + "id": "4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c", + "name": "3:panel_3", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:03:44.509Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:60659030-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 2", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:59.443Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:65227c00-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 3", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:07.392Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6803a2f0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 4", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:12.223Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6b18f940-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 5", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:17.396Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6e12ff60-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 6", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:22.390Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:4f0fd980-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:30.360Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:3de0bda0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "1", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:01.530Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:46c8b580-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "2", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:04:16.472Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:708fe640-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "zz 7", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:26.564Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:7b8d50a0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "Hi i have a lot of words in my dashboard name! It's pretty long i wonder what it'll look like", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:45.002Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:7e42d3b0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "bye", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:49.547Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:846988b0-3dd4-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "last", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + ], + "type": "dashboard", + "updated_at": "2018-04-11T22:05:59.867Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:cbd3bc30-3e5a-11e8-9fc3-39e49624228e", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"weightLbs:<50\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"name.keyword\",\"value\":\"Fee Fee\",\"params\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"name.keyword\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":true,\"useMargins\":true,\"hidePanelTitles\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"}]", + "timeRestore": false, + "title": "bug", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "771b4f10-3e59-11e8-9fc3-39e49624228e", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "befdb6b0-3e59-11e8-9fc3-39e49624228e", + "name": "2:panel_2", + "type": "visualization" + }, + { + "id": "4c0c3f90-3e5a-11e8-9fc3-39e49624228e", + "name": "3:panel_3", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-12T14:07:12.243Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:5bac3a80-3e5b-11e8-9fc3-39e49624228e", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "dashboard with scripted filter, negated filter and query", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:<50\"},\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"key\":\"name.keyword\",\"negate\":true,\"params\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"},\"type\":\"phrase\",\"value\":\"Fee Fee\",\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"name.keyword\":{\"query\":\"Fee Fee\",\"type\":\"phrase\"}}}},{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":\"is dog\",\"disabled\":false,\"field\":\"isDog\",\"key\":\"isDog\",\"negate\":false,\"params\":{\"value\":true},\"type\":\"phrase\",\"value\":\"true\",\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index\"},\"script\":{\"script\":{\"inline\":\"boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { return doc['animal.keyword'].value == 'dog' }, params.value);\",\"lang\":\"painless\",\"params\":{\"value\":true}}}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":true,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"3\"},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"4\"},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "section": 0, + "value": 0 + }, + "timeFrom": "Wed Apr 12 2017 10:06:21 GMT-0400", + "timeRestore": true, + "timeTo": "Thu Apr 12 2018 10:06:21 GMT-0400", + "title": "filters", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index", + "type": "index-pattern" + }, + { + "id": "771b4f10-3e59-11e8-9fc3-39e49624228e", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "4c0c3f90-3e5a-11e8-9fc3-39e49624228e", + "name": "3:panel_3", + "type": "visualization" + }, + { + "id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "name": "4:panel_4", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-12T14:11:13.576Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"activity level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"barking level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"breed\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"breed.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"size.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"trainability\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "dogbreeds" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-04-12T16:24:29.357Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:5e085850-3e6e-11e8-bbb9-e15942d5d48c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-12T16:27:17.973Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "non timebased line chart - dog data", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"non timebased line chart - dog data\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Max trainability\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Max trainability\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true},{\"show\":true,\"mode\":\"normal\",\"type\":\"line\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"3\",\"label\":\"Max barking level\"},\"valueAxis\":\"ValueAxis-1\"},{\"show\":true,\"mode\":\"normal\",\"type\":\"line\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"4\",\"label\":\"Max activity level\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"trainability\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"breed.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"barking level\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"activity level\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:a5d56330-3e6e-11e8-bbb9-e15942d5d48c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "I have two visualizations that are created off a non time based index", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"}]", + "timeRestore": false, + "title": "Non time based", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "5e085850-3e6e-11e8-bbb9-e15942d5d48c", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c", + "name": "2:panel_2", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-12T16:29:18.435Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:d2525040-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "I have one of every visualization type since the last time I was created!", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"4\"},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"5\"},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"6\"},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"7\"},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"8\"},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9\"},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"10\"},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"11\"},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"12\"},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"13\"},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"14\"},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"15\"},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"16\"},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"17\"},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":120,\"w\":24,\"h\":15,\"i\":\"18\"},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":135,\"w\":24,\"h\":15,\"i\":\"19\"},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":135,\"w\":24,\"h\":15,\"i\":\"20\"},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":150,\"w\":24,\"h\":15,\"i\":\"21\"},\"panelIndex\":\"21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_21\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":150,\"w\":24,\"h\":15,\"i\":\"22\"},\"panelIndex\":\"22\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_22\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":165,\"w\":24,\"h\":15,\"i\":\"23\"},\"panelIndex\":\"23\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_23\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"x\":24,\"y\":165,\"w\":24,\"h\":15,\"i\":\"24\"},\"panelIndex\":\"24\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_24\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":180,\"w\":24,\"h\":15,\"i\":\"25\"},\"panelIndex\":\"25\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_25\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"x\":24,\"y\":180,\"w\":24,\"h\":15,\"i\":\"26\"},\"panelIndex\":\"26\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_26\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":195,\"w\":24,\"h\":15,\"i\":\"27\"},\"panelIndex\":\"27\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_27\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":195,\"w\":24,\"h\":15,\"i\":\"28\"},\"panelIndex\":\"28\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_28\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":210,\"w\":24,\"h\":15,\"i\":\"29\"},\"panelIndex\":\"29\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_29\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":210,\"i\":\"30\"},\"panelIndex\":\"30\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_30\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "dashboard with everything", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "e6140540-3dca-11e8-8660-4d65aa086b3c", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "3525b840-3dcb-11e8-8660-4d65aa086b3c", + "name": "2:panel_2", + "type": "visualization" + }, + { + "id": "4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c", + "name": "3:panel_3", + "type": "visualization" + }, + { + "id": "37a541c0-3dcc-11e8-8660-4d65aa086b3c", + "name": "4:panel_4", + "type": "visualization" + }, + { + "id": "ffa2e0c0-3dcb-11e8-8660-4d65aa086b3c", + "name": "5:panel_5", + "type": "visualization" + }, + { + "id": "e2023110-3dcb-11e8-8660-4d65aa086b3c", + "name": "6:panel_6", + "type": "visualization" + }, + { + "id": "145ced90-3dcb-11e8-8660-4d65aa086b3c", + "name": "7:panel_7", + "type": "visualization" + }, + { + "id": "2d1b1620-3dcd-11e8-8660-4d65aa086b3c", + "name": "8:panel_8", + "type": "visualization" + }, + { + "id": "42535e30-3dcd-11e8-8660-4d65aa086b3c", + "name": "9:panel_9", + "type": "visualization" + }, + { + "id": "42535e30-3dcd-11e8-8660-4d65aa086b3c", + "name": "10:panel_10", + "type": "visualization" + }, + { + "id": "4c0f47e0-3dcd-11e8-8660-4d65aa086b3c", + "name": "11:panel_11", + "type": "visualization" + }, + { + "id": "11ae2bd0-3dcc-11e8-8660-4d65aa086b3c", + "name": "12:panel_12", + "type": "visualization" + }, + { + "id": "3fe22200-3dcb-11e8-8660-4d65aa086b3c", + "name": "13:panel_13", + "type": "visualization" + }, + { + "id": "4ca00ba0-3dcc-11e8-8660-4d65aa086b3c", + "name": "14:panel_14", + "type": "visualization" + }, + { + "id": "78803be0-3dcd-11e8-8660-4d65aa086b3c", + "name": "15:panel_15", + "type": "visualization" + }, + { + "id": "b92ae920-3dcc-11e8-8660-4d65aa086b3c", + "name": "16:panel_16", + "type": "visualization" + }, + { + "id": "e4d8b430-3dcc-11e8-8660-4d65aa086b3c", + "name": "17:panel_17", + "type": "visualization" + }, + { + "id": "f81134a0-3dcc-11e8-8660-4d65aa086b3c", + "name": "18:panel_18", + "type": "visualization" + }, + { + "id": "cc43fab0-3dcc-11e8-8660-4d65aa086b3c", + "name": "19:panel_19", + "type": "visualization" + }, + { + "id": "02a2e4e0-3dcd-11e8-8660-4d65aa086b3c", + "name": "20:panel_20", + "type": "visualization" + }, + { + "id": "df815d20-3dcc-11e8-8660-4d65aa086b3c", + "name": "21:panel_21", + "type": "visualization" + }, + { + "id": "c40f4d40-3dcc-11e8-8660-4d65aa086b3c", + "name": "22:panel_22", + "type": "visualization" + }, + { + "id": "7fda8ee0-3dcd-11e8-8660-4d65aa086b3c", + "name": "23:panel_23", + "type": "visualization" + }, + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "24:panel_24", + "type": "search" + }, + { + "id": "be5accf0-3dca-11e8-8660-4d65aa086b3c", + "name": "25:panel_25", + "type": "search" + }, + { + "id": "ca5ada40-3dca-11e8-8660-4d65aa086b3c", + "name": "26:panel_26", + "type": "search" + }, + { + "id": "771b4f10-3e59-11e8-9fc3-39e49624228e", + "name": "27:panel_27", + "type": "visualization" + }, + { + "id": "5e085850-3e6e-11e8-bbb9-e15942d5d48c", + "name": "28:panel_28", + "type": "visualization" + }, + { + "id": "8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c", + "name": "29:panel_29", + "type": "visualization" + }, + { + "id": "befdb6b0-3e59-11e8-9fc3-39e49624228e", + "name": "30:panel_30", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-04-16T16:05:02.915Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:29bd0240-4197-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-16T16:56:53.092Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"CN\",\"params\":{\"query\":\"CN\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"CN\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"bytes >= 10000\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Kuery: pie bytes with kuery and filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Kuery: pie bytes with kuery and filter\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fieldFormatMap": "{\"machine.ram\":{\"id\":\"number\",\"params\":{\"pattern\":\"0,0.[000] b\"}}}", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-04-16T16:57:12.263Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:55d37a30-4197-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "agent", + "bytes", + "clientip" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"clientip : 73.14.212.83\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"range\",\"key\":\"bytes\",\"value\":\"100 to 1,000\",\"params\":{\"gte\":100,\"lt\":1000},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"range\":{\"bytes\":{\"gte\":100,\"lt\":1000}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Bytes and kuery in saved search with filter", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-16T16:58:07.059Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:b60de070-4197-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "Bytes bytes and more bytes", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3\"},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":15,\"w\":17,\"h\":8,\"i\":\"4\"},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":18,\"h\":13,\"i\":\"5\"},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":37,\"w\":24,\"h\":12,\"i\":\"6\"},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":30,\"w\":9,\"h\":7,\"i\":\"7\"},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":28,\"y\":23,\"w\":15,\"h\":13,\"i\":\"8\"},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":43,\"w\":24,\"h\":15,\"i\":\"9\"},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":49,\"w\":18,\"h\":12,\"i\":\"10\"},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":58,\"w\":24,\"h\":15,\"i\":\"11\"},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":61,\"w\":5,\"h\":4,\"i\":\"12\"},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":73,\"w\":17,\"h\":6,\"i\":\"13\"},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":65,\"w\":24,\"h\":15,\"i\":\"14\"},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":79,\"w\":24,\"h\":6,\"i\":\"15\"},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":80,\"w\":24,\"h\":15,\"i\":\"16\"},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":85,\"w\":13,\"h\":11,\"i\":\"17\"},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"x\":24,\"y\":95,\"w\":23,\"h\":11,\"i\":\"18\"},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "All about those bytes", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "7ff2c4c0-4191-11e8-bb13-d53698fb349a", + "name": "1:panel_1", + "type": "visualization" + }, + { + "id": "03d2afd0-4192-11e8-bb13-d53698fb349a", + "name": "2:panel_2", + "type": "visualization" + }, + { + "id": "63983430-4192-11e8-bb13-d53698fb349a", + "name": "3:panel_3", + "type": "visualization" + }, + { + "id": "0ca8c600-4195-11e8-bb13-d53698fb349a", + "name": "4:panel_4", + "type": "visualization" + }, + { + "id": "c10c6b00-4191-11e8-bb13-d53698fb349a", + "name": "5:panel_5", + "type": "visualization" + }, + { + "id": "760a9060-4190-11e8-bb13-d53698fb349a", + "name": "6:panel_6", + "type": "visualization" + }, + { + "id": "1dcdfe30-4192-11e8-bb13-d53698fb349a", + "name": "7:panel_7", + "type": "visualization" + }, + { + "id": "584c0300-4191-11e8-bb13-d53698fb349a", + "name": "8:panel_8", + "type": "visualization" + }, + { + "id": "b3e70d00-4190-11e8-bb13-d53698fb349a", + "name": "9:panel_9", + "type": "visualization" + }, + { + "id": "df72ad40-4194-11e8-bb13-d53698fb349a", + "name": "10:panel_10", + "type": "visualization" + }, + { + "id": "9bebe980-4192-11e8-bb13-d53698fb349a", + "name": "11:panel_11", + "type": "visualization" + }, + { + "id": "9fb4c670-4194-11e8-bb13-d53698fb349a", + "name": "12:panel_12", + "type": "visualization" + }, + { + "id": "35417e50-4194-11e8-bb13-d53698fb349a", + "name": "13:panel_13", + "type": "visualization" + }, + { + "id": "039e4770-4194-11e8-bb13-d53698fb349a", + "name": "14:panel_14", + "type": "visualization" + }, + { + "id": "76c7f020-4194-11e8-bb13-d53698fb349a", + "name": "15:panel_15", + "type": "visualization" + }, + { + "id": "8090dcb0-4195-11e8-bb13-d53698fb349a", + "name": "16:panel_16", + "type": "visualization" + }, + { + "id": "29bd0240-4197-11e8-bb13-d53698fb349a", + "name": "17:panel_17", + "type": "visualization" + }, + { + "id": "55d37a30-4197-11e8-bb13-d53698fb349a", + "name": "18:panel_18", + "type": "search" + } + ], + "type": "dashboard", + "updated_at": "2018-04-16T17:00:48.503Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:78803be0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.127Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: tag cloud", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tag cloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:3fe22200-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.130Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4ca00ba0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.131Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: region map", + "uiStateJSON": "{\"mapZoom\":2,\"mapCenter\":[8.754794702435618,-9.140625000000002]}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: region map\",\"type\":\"region_map\",\"params\":{\"legendPosition\":\"bottomright\",\"addTooltip\":true,\"colorSchema\":\"Yellow to Red\",\"selectedLayer\":{\"attribution\":\"

    Made with NaturalEarth | Elastic Maps Service

    \",\"name\":\"World Countries\",\"weight\":1,\"format\":{\"type\":\"geojson\"},\"url\":\"https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=6.3.0\",\"fields\":[{\"name\":\"iso2\",\"description\":\"Two letter abbreviation\"},{\"name\":\"iso3\",\"description\":\"Three letter abbreviation\"},{\"name\":\"name\",\"description\":\"Country name\"}],\"created_at\":\"2017-07-31T16:00:19.996450\",\"tags\":[],\"id\":5715999101812736,\"layerId\":\"elastic_maps_service.World Countries\"},\"selectedJoinField\":{\"name\":\"iso2\",\"description\":\"Two letter abbreviation\"},\"isDisplayWarning\":true,\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

    © OpenStreetMap contributors | Elastic Maps Service

    \",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

    © OpenStreetMap contributors | Elastic Maps Service

    \",\"subdomains\":[]}},\"mapZoom\":2,\"mapCenter\":[0,0],\"outlineWeight\":1,\"showAllShapes\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:11ae2bd0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.133Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: metric", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: metric\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:145ced90-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.134Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: heatmap", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 15\":\"rgb(247,252,245)\",\"15 - 30\":\"rgb(199,233,192)\",\"30 - 45\":\"rgb(116,196,118)\",\"45 - 60\":\"rgb(35,139,69)\"}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: heatmap\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Greens\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":false,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:e2023110-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:32.135Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: guage", + "uiStateJSON": "{\"vis\":{\"colors\":{\"0 - 50000\":\"#EF843C\",\"75000 - 10000000\":\"#3F6833\"},\"defaultColors\":{\"0 - 5000000\":\"rgb(0,104,55)\",\"50000000 - 74998990099\":\"rgb(165,0,38)\"}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: guage\",\"type\":\"gauge\",\"params\":{\"addLegend\":true,\"addTooltip\":true,\"gauge\":{\"backStyle\":\"Full\",\"colorSchema\":\"Green to Red\",\"colorsRange\":[{\"from\":0,\"to\":5000000},{\"from\":50000000,\"to\":74998990099}],\"extendRange\":true,\"gaugeColorMode\":\"Labels\",\"gaugeStyle\":\"Full\",\"gaugeType\":\"Arc\",\"invertColors\":false,\"labels\":{\"color\":\"black\",\"show\":true},\"orientation\":\"vertical\",\"percentageMode\":false,\"scale\":{\"color\":\"#333\",\"labels\":false,\"show\":true},\"style\":{\"bgColor\":false,\"bgFill\":\"#eee\",\"bgMask\":false,\"bgWidth\":0.9,\"fontSize\":60,\"labelColor\":true,\"mask\":false,\"maskBars\":50,\"subText\":\"\",\"width\":0.9},\"type\":\"meter\",\"alignment\":\"horizontal\"},\"isDisplayWarning\":false,\"type\":\"gauge\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"machine.ram\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:b92ae920-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.110Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: timelion", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: timelion\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(*, metric=avg:bytes, split=ip:5)\",\"interval\":\"auto\"},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:e4d8b430-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.106Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-guage", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-guage\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"gauge\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_color_rules\":[{\"id\":\"e0be22e0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_width\":10,\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4c0f47e0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.111Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: markdown", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: markdown\",\"type\":\"markdown\",\"params\":{\"fontSize\":20,\"openLinksInNewTab\":false,\"markdown\":\"I'm a markdown!\"},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:2d1b1620-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_1_index_pattern", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_2_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.123Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1523481142694\",\"fieldName\":\"bytes\",\"parent\":\"\",\"label\":\"Bytes Input List\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1523481163654\",\"fieldName\":\"bytes\",\"parent\":\"\",\"label\":\"Bytes range\",\"type\":\"range\",\"options\":{\"decimalPlaces\":0,\"step\":1},\"indexPatternRefName\":\"control_1_index_pattern\"},{\"id\":\"1523481176519\",\"fieldName\":\"sound.keyword\",\"parent\":\"\",\"label\":\"Animal sounds\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_2_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "f908c8e0-3e6d-11e8-bbb9-e15942d5d48c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.173Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"size.keyword\",\"value\":\"extra large\",\"params\":{\"query\":\"extra large\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"size.keyword\":{\"query\":\"extra large\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: non timebased line chart - dog data - with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"field\":\"trainability\"},\"schema\":\"metric\",\"type\":\"max\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"breed.keyword\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"field\":\"barking level\"},\"schema\":\"metric\",\"type\":\"max\"},{\"enabled\":true,\"id\":\"4\",\"params\":{\"field\":\"activity level\"},\"schema\":\"metric\",\"type\":\"max\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Max trainability\"},\"drawLinesBetweenPoints\":true,\"mode\":\"normal\",\"show\":\"true\",\"showCircles\":true,\"type\":\"line\",\"valueAxis\":\"ValueAxis-1\"},{\"data\":{\"id\":\"3\",\"label\":\"Max barking level\"},\"drawLinesBetweenPoints\":true,\"mode\":\"normal\",\"show\":true,\"showCircles\":true,\"type\":\"line\",\"valueAxis\":\"ValueAxis-1\"},{\"data\":{\"id\":\"4\",\"label\":\"Max activity level\"},\"drawLinesBetweenPoints\":true,\"mode\":\"normal\",\"show\":true,\"showCircles\":true,\"type\":\"line\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"line\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Max trainability\"},\"type\":\"value\"}],\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"Rendering Test: non timebased line chart - dog data - with filter\",\"type\":\"line\"}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:42535e30-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:31.124Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: input control parent", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: input control parent\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1523481216736\",\"fieldName\":\"animal.keyword\",\"parent\":\"\",\"label\":\"Animal type\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1523481176519\",\"fieldName\":\"sound.keyword\",\"parent\":\"1523481216736\",\"label\":\"Animal sounds\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:7fda8ee0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.344Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: vega", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: vega\",\"type\":\"vega\",\"params\":{\"spec\":\"{\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v2.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: _all\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 0\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:02a2e4e0-3dcd-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.351Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-table", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-table\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"table\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_color_rules\":[{\"id\":\"e0be22e0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_width\":10,\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"markdown\":\"\\nHi Avg last bytes: {{ average_of_bytes.last.raw }}\",\"pivot_id\":\"bytes\",\"pivot_label\":\"Hello\",\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:f81134a0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.355Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-markdown", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-markdown\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"markdown\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_color_rules\":[{\"id\":\"e0be22e0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"gauge_width\":10,\"gauge_inner_width\":10,\"gauge_style\":\"half\",\"markdown\":\"\\nHi Avg last bytes: {{ average_of_bytes.last.raw }}\",\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:df815d20-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.349Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-topn", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-topn\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"top_n\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"},{\"id\":\"d18e5970-3dcc-11e8-a2f6-c162ca6cf6ea\",\"color\":\"rgba(160,70,216,1)\",\"split_mode\":\"filter\",\"metrics\":[{\"id\":\"d18e5971-3dcc-11e8-a2f6-c162ca6cf6ea\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"bar_color_rules\":[{\"id\":\"cd25a820-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:cc43fab0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.353Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-metric", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-metric\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"metric\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum_of_squares\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"c50bd5b0-3dcc-11e8-a2f6-c162ca6cf6ea\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:c40f4d40-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:30.347Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Rendering Test: tsvb-ts", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: tsvb-ts\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"use_kibana_indexes\":false},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:ffa2e0c0-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.153Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: goal", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: goal\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":4000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":2,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:37a541c0-3dcc-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.156Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"range\",\"key\":\"bytes\",\"value\":\"0 to 10,000\",\"params\":{\"gte\":0,\"lt\":10000},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"range\":{\"bytes\":{\"gte\":0,\"lt\":10000}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: geo map", + "uiStateJSON": "{\"mapZoom\":4,\"mapCenter\":[35.460669951495305,-85.60546875000001]}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: geo map\",\"type\":\"tile_map\",\"params\":{\"mapType\":\"Scaled Circle Markers\",\"isDesaturated\":true,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

    © OpenStreetMap contributors | Elastic Maps Service

    \",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

    © OpenStreetMap contributors | Elastic Maps Service

    \",\"subdomains\":[]}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"geohash_grid\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.coordinates\",\"autoPrecision\":true,\"isFilteredByCollar\":true,\"useGeocentroid\":true,\"precision\":3}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.162Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: datatable", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: datatable\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"clientip\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:3525b840-3dcb-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.163Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: bar", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: bar\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":3,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:e6140540-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.165Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"CN\",\"params\":{\"query\":\"CN\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"CN\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: area with not filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: area with not filter\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"filters\",\"schema\":\"group\",\"params\":{\"filters\":[{\"input\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"label\":\"\"},{\"input\":{\"query\":\"bytes:>10\",\"language\":\"lucene\"}}]}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:4c0c3f90-3e5a-11e8-9fc3-39e49624228e", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:33.166Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"field\":\"isDog\",\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"isDog\",\"value\":\"true\",\"params\":{\"value\":true},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"script\":{\"script\":{\"inline\":\"boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { return doc['animal.keyword'].value == 'dog' }, params.value);\",\"lang\":\"painless\",\"params\":{\"value\":true}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"weightLbs:>40\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: scripted filter and query", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: scripted filter and query\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"sound.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:34.195Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Rendering Test: animal sounds pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: animal sounds pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"sound.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:771b4f10-3e59-11e8-9fc3-39e49624228e", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:34.200Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + }, + "savedSearchRefName": "search_0", + "title": "Rendering Test: animal weights linked to search", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Rendering Test: animal weights linked to search\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"name.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:76c7f020-4194-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:34.583Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb top n with bytes filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb top n with bytes filter\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"top_n\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"Filter Bytes Test:>100\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"split_color_mode\":\"gradient\"},{\"id\":\"4fd5b150-4194-11e8-a461-7d278185cba4\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"4fd5b151-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"Filter Bytes Test:>3000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:0ca8c600-4195-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "control_0_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.229Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: input control with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: input control with filter\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1523896850250\",\"fieldName\":\"bytes\",\"parent\":\"\",\"label\":\"Byte Options\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"size\":10,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:039e4770-4194-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.220Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb time series with bytes filter split by clientip", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb time series with bytes filter split by clientip\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"use_kibana_indexes\":false},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:760a9060-4190-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.235Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"US\",\"params\":{\"query\":\"US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: max bytes in US - area chart with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: max bytes in US - area chart with filter\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Max bytes\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Max bytes\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:b3e70d00-4190-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:35.236Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: standard deviation heatmap with other bucket", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"-4,000 - 1,000\":\"rgb(247,252,245)\",\"1,000 - 6,000\":\"rgb(199,233,192)\",\"6,000 - 11,000\":\"rgb(116,196,118)\",\"11,000 - 16,000\":\"rgb(35,139,69)\"}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: standard deviation heatmap with other bucket\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Greens\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":false,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"std_dev\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":true,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"_term\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:c10c6b00-4191-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.267Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: max bytes guage percent mode", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 1\":\"rgb(0,104,55)\",\"1 - 15\":\"rgb(255,255,190)\",\"15 - 100\":\"rgb(165,0,38)\"}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: max bytes guage percent mode\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"extendRange\":true,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":500},{\"from\":500,\"to\":7500},{\"from\":7500,\"to\":50000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"Im subtext\",\"fontSize\":60,\"labelColor\":true},\"alignment\":\"horizontal\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"max\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:03d2afd0-4192-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.269Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: Goal unique count", + "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 10000\":\"rgb(0,104,55)\"}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: Goal unique count\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:7ff2c4c0-4191-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.270Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: Data table top hit with significant terms geo.src", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: Data table top hit with significant terms geo.src\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"top_hits\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\",\"aggregate\":\"average\",\"size\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"significant_terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"geo.src\",\"size\":10}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:df72ad40-4194-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.276Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":true,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"bytes\",\"value\":\"0\",\"params\":{\"query\":0,\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"bytes\":{\"query\":0,\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: tag cloud with not 0 bytes filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tag cloud with not 0 bytes filter\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"bytes\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:63983430-4192-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:06:36.275Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"US\",\"params\":{\"query\":\"US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"Filter Bytes Test:>5000\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: geo map with filter and query bytes > 5000 in US geo.src, heatmap setting", + "uiStateJSON": "{\"mapZoom\":7,\"mapCenter\":[42.98857645832184,-75.49804687500001]}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: geo map with filter and query bytes > 5000 in US geo.src, heatmap setting\",\"type\":\"tile_map\",\"params\":{\"mapType\":\"Heatmap\",\"isDesaturated\":true,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

    © OpenStreetMap contributors | Elastic Maps Service

    \",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

    © OpenStreetMap contributors | Elastic Maps Service

    \",\"subdomains\":[]}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"geohash_grid\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.coordinates\",\"autoPrecision\":true,\"isFilteredByCollar\":true,\"useGeocentroid\":true,\"precision\":4}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:be5accf0-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "agent", + "bytes", + "clientip" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Rendering Test: saved search", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-17T15:09:39.805Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:ca5ada40-3dca-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "agent", + "bytes", + "clientip" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[{\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"bytes\",\"value\":\"1,607\",\"params\":{\"query\":1607,\"type\":\"phrase\"},\"disabled\":false,\"alias\":null,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"bytes\":{\"query\":1607,\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Filter Bytes Test: search with filter", + "version": 1 + }, + "type": "search", + "updated_at": "2018-04-17T15:09:55.976Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:9bebe980-4192-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T15:59:42.648Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: timelion split 5 on bytes", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: timelion split 5 on bytes\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(*, split=bytes:5)\",\"interval\":\"auto\"},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:1dcdfe30-4192-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T15:59:56.976Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"bytes:>100\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: min bytes metric with query", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: min bytes metric with query\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"min\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:35417e50-4194-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T16:06:03.785Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb metric with custom interval and bytes filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb metric with custom interval and bytes filter\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"metric\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":1,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1d\",\"value_template\":\"{{value}} custom template\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:9fb4c670-4194-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T16:32:59.086Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: tsvb markdown", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: tsvb markdown\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"markdown\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"label\":\"\",\"var_name\":\"\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"markdown\":\"{{bytes_1000.last.formatted}}\",\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:befdb6b0-3e59-11e8-9fc3-39e49624228e", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "a16d1990-3dca-11e8-8660-4d65aa086b3c", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T17:16:27.743Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal.keyword\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal.keyword\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"language\":\"lucene\",\"query\":\"\"}}" + }, + "savedSearchRefName": "search_0", + "title": "Filter Test: animals: linked to search with filter", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Test: animals: linked to search with filter\",\"type\":\"pie\",\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"labels\":{\"last_level\":true,\"show\":false,\"truncate\":100,\"values\":true},\"legendPosition\":\"right\",\"type\":\"pie\",\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"name.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:584c0300-4191-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-04-17T18:36:30.315Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"bytes:>9000\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Filter Bytes Test: split by geo with query", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: split by geo with query\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"bytes\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:7.0.0-alpha1", + "index": ".kibana", + "source": { + "config": { + "buildNum": null, + "dateFormat:tz": "UTC", + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "notifications:lifetime:banner": 3600000, + "notifications:lifetime:error": 3600000, + "notifications:lifetime:info": 3600000, + "notifications:lifetime:warning": 3600000 + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-17T19:25:03.632Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:8090dcb0-4195-11e8-bb13-d53698fb349a", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], + "type": "visualization", + "updated_at": "2018-04-17T19:28:21.967Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{}" + }, + "title": "Filter Bytes Test: vega", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Filter Bytes Test: vega\",\"type\":\"vega\",\"params\":{\"spec\":\"{ \\nconfig: { kibana: { renderer: \\\"svg\\\" }},\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v2.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: _all\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 0\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"aggs\":[]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.2.4", + "index": ".kibana", + "source": { + "config": { + "buildNum": 16627, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "xPackMonitoring:showBanner": false + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-05-09T20:50:57.021Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:edb65990-53ca-11e8-b481-c9426d020fcd", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-05-09T20:52:47.144Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "table created in 6_2", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"table created in 6_2\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"weightLbs\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"animal.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:0644f890-53cb-11e8-b481-c9426d020fcd", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-05-09T20:53:28.345Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"weightLbs:>10\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Weight in lbs pie created in 6.2", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Weight in lbs pie created in 6.2\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"weightLbs\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:1b2f47b0-53cb-11e8-b481-c9426d020fcd", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:>15\"},\"filter\":[{\"meta\":{\"field\":\"isDog\",\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"isDog\",\"value\":\"true\",\"params\":{\"value\":true},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"script\":{\"script\":{\"inline\":\"boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { return doc['animal.keyword'].value == 'dog' }, params.value);\",\"lang\":\"painless\",\"params\":{\"value\":true}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":12,\"x\":24,\"y\":0,\"i\":\"4\"},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":12,\"x\":0,\"y\":0,\"i\":\"5\"},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", + "refreshInterval": { + "display": "Off", + "pause": false, + "value": 0 + }, + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "title": "Animal Weights (created in 6.2)", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "edb65990-53ca-11e8-b481-c9426d020fcd", + "name": "4:panel_4", + "type": "visualization" + }, + { + "id": "0644f890-53cb-11e8-b481-c9426d020fcd", + "name": "5:panel_5", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-05-09T20:54:03.435Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fieldFormatMap": "{\"weightLbs\":{\"id\":\"number\",\"params\":{\"pattern\":\"0,0.0\"}},\"is_dog\":{\"id\":\"boolean\"},\"isDog\":{\"id\":\"boolean\"}}", + "fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"animal\",\"type\":\"string\",\"count\":3,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"animal.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"name\",\"type\":\"string\",\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"name.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sound\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"sound.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"weightLbs\",\"type\":\"number\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"isDog\",\"type\":\"boolean\",\"count\":0,\"scripted\":true,\"script\":\"return doc['animal.keyword'].value == 'dog'\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", + "timeFieldName": "@timestamp", + "title": "animals-*" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-05-09T20:55:44.314Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:6351c590-53cb-11e8-b481-c9426d020fcd", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "animal", + "sound", + "weightLbs" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"weightLbs:>10\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"sound.keyword\",\"value\":\"growl\",\"params\":{\"query\":\"growl\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"sound.keyword\":{\"query\":\"growl\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Search created in 6.2", + "version": 1 + }, + "type": "search", + "updated_at": "2018-05-09T20:56:04.457Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:47b5cf60-9e93-11ea-853e-adc0effaf76d", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "1b1789d0-9e93-11ea-853e-adc0effaf76d", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-05-25T15:16:27.743Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "vis with missing index pattern", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"title\":\"vis with missing index pattern\"}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:502e63a0-9e93-11ea-853e-adc0effaf76d", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"6cfbe6cc-1872-4cb4-9455-a02eeb75127e\"},\"panelIndex\":\"6cfbe6cc-1872-4cb4-9455-a02eeb75127e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cfbe6cc-1872-4cb4-9455-a02eeb75127e\"}]", + "timeRestore": false, + "title": "dashboard with missing index pattern", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "47b5cf60-9e93-11ea-853e-adc0effaf76d", + "name": "6cfbe6cc-1872-4cb4-9455-a02eeb75127e:panel_6cfbe6cc-1872-4cb4-9455-a02eeb75127e", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-05-25T15:16:27.743Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:6eb8a840-a32e-11ea-88c2-d56dd2b14bd7", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\n \"query\": {\n \"language\": \"kuery\",\n \"query\": \"\"\n },\n \"filter\": [\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": true,\n \"type\": \"phrase\",\n \"key\": \"name\",\n \"params\": {\n \"query\": \"moo\"\n },\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"\n },\n \"query\": {\n \"match_phrase\": {\n \"name\": \"moo\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": true,\n \"type\": \"phrase\",\n \"key\": \"baad-field\",\n \"params\": {\n \"query\": \"moo\"\n },\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index\"\n },\n \"query\": {\n \"match_phrase\": {\n \"baad-field\": \"moo\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": false,\n \"type\": \"phrase\",\n \"key\": \"@timestamp\",\n \"params\": {\n \"query\": \"123\"\n },\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[2].meta.index\"\n },\n \"query\": {\n \"match_phrase\": {\n \"@timestamp\": \"123\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": false,\n \"type\": \"exists\",\n \"key\": \"extension\",\n \"value\": \"exists\",\n \"indexRefName\": \"kibanaSavedObjectMeta.searchSourceJSON.filter[3].meta.index\"\n },\n \"exists\": {\n \"field\": \"extension\"\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n },\n {\n \"meta\": {\n \"alias\": null,\n \"negate\": false,\n \"disabled\": false,\n \"type\": \"phrase\",\n \"key\": \"banana\",\n \"params\": {\n \"query\": \"yellow\"\n }\n },\n \"query\": {\n \"match_phrase\": {\n \"banana\": \"yellow\"\n }\n },\n \"$state\": {\n \"store\": \"appState\"\n }\n }\n ]\n}" + }, + "optionsJSON": "{\n \"hidePanelTitles\": false,\n \"useMargins\": true\n}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"94a3dc1d-508a-4d42-a480-65b158925ba0\"},\"panelIndex\":\"94a3dc1d-508a-4d42-a480-65b158925ba0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_94a3dc1d-508a-4d42-a480-65b158925ba0\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "now-10y", + "timeRestore": true, + "timeTo": "now", + "title": "dashboard with bad filters", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "a0f483a0-3dc9-11e8-8660-bad-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[1].meta.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[2].meta.index", + "type": "index-pattern" + }, + { + "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[3].meta.index", + "type": "index-pattern" + }, + { + "id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[4].meta.index", + "type": "index-pattern" + }, + { + "id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159", + "name": "94a3dc1d-508a-4d42-a480-65b158925ba0:panel_94a3dc1d-508a-4d42-a480-65b158925ba0", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-06-04T09:26:04.272Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": null, + "dateFormat:tz": "UTC", + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2020-06-04T09:22:54.572Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:DashboardPanelVersionInUrl:8.0.0", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "ui-metric", + "ui-metric": { + "count": 15 + }, + "updated_at": "2020-06-04T09:28:06.848Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:kibana-user_agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-04T09:28:06.848Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_daily:dashboards:2020-05-31", + "index": ".kibana", + "source": { + "application_usage_daily": { + "appId": "dashboards", + "minutesOnScreen": 13.956333333333333, + "numberOfClicks": 134, + "timestamp": "2020-05-31T00:00:00.000Z" + }, + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "application_usage_daily", + "updated_at": "2021-06-10T22:39:09.215Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_daily:home:2020-05-31", + "index": ".kibana", + "source": { + "application_usage_daily": { + "appId": "home", + "minutesOnScreen": 0.5708666666666666, + "numberOfClicks": 1, + "timestamp": "2020-05-31T00:00:00.000Z" + }, + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "application_usage_daily", + "updated_at": "2021-06-10T22:39:09.215Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_daily:management:2020-05-31", + "index": ".kibana", + "source": { + "application_usage_daily": { + "appId": "management", + "minutesOnScreen": 5.842616666666666, + "numberOfClicks": 107, + "timestamp": "2020-05-31T00:00:00.000Z" + }, + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "application_usage_daily", + "updated_at": "2021-06-10T22:39:09.215Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_daily:management:2020-06-04", + "index": ".kibana", + "source": { + "application_usage_daily": { + "appId": "management", + "minutesOnScreen": 2.5120666666666667, + "numberOfClicks": 38, + "timestamp": "2020-06-04T00:00:00.000Z" + }, + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "application_usage_daily", + "updated_at": "2021-06-10T22:39:09.215Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_daily:dashboards:2020-06-04", + "index": ".kibana", + "source": { + "application_usage_daily": { + "appId": "dashboards", + "minutesOnScreen": 9.065083333333334, + "numberOfClicks": 21, + "timestamp": "2020-06-04T00:00:00.000Z" + }, + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "type": "application_usage_daily", + "updated_at": "2021-06-10T22:39:09.215Z" + }, + "type": "_doc" + } +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json.gz deleted file mode 100644 index ae78761fef0d3415c8ec05ea4bfa9dcca070981a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20860 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 diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json b/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json index 9f5edaad0fe763..b6e225951c5454 100644 --- a/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json +++ b/test/functional/fixtures/es_archiver/dashboard/current/kibana/mappings.json @@ -2,155 +2,139 @@ "type": "index", "value": { "aliases": { - ".kibana": { - } + ".kibana_$KIBANA_PACKAGE_VERSION": {}, + ".kibana": {} }, - "index": ".kibana_1", + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { "_meta": { "migrationMappingPropertyHashes": { - "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", - "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", - "config": "ae24d22d5986d04124cc6568f771066f", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", "references": "7997cf5a56cc02bdc9c93361bde732b0", "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", "telemetry": "36a616f7026dfa617d6655df850fe16d", "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "url": "b675c3be8d76ecf029294d51dc7ec65d", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" } }, "dynamic": "strict", "properties": { - "application_usage_totals": { + "application_usage_daily": { + "dynamic": "false", "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - } - } - }, - "application_usage_transactional": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - }, "timestamp": { "type": "date" } } }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, "buildNum": { "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "notifications:lifetime:banner": { - "type": "long" - }, - "notifications:lifetime:error": { - "type": "long" - }, - "notifications:lifetime:info": { - "type": "long" - }, - "notifications:lifetime:warning": { - "type": "long" - }, - "xPackMonitoring:showBanner": { - "type": "boolean" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -162,33 +146,13 @@ } }, "index-pattern": { + "dynamic": "false", "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, "title": { "type": "text" }, "type": { "type": "keyword" - }, - "typeMeta": { - "type": "keyword" } } }, @@ -202,9 +166,32 @@ } } }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" + }, + "sourceId": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + } + } + }, "migrationVersion": { "dynamic": "true", "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, "dashboard": { "fields": { "keyword": { @@ -249,6 +236,9 @@ "namespaces": { "type": "keyword" }, + "originId": { + "type": "keyword" + }, "query": { "properties": { "description": { @@ -305,22 +295,38 @@ "search": { "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -331,12 +337,13 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } + "dynamic": "false", + "type": "object" }, "telemetry": { "properties": { @@ -408,15 +415,19 @@ } }, "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } + "dynamic": "false", + "type": "object" }, "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, "ui-metric": { "properties": { "count": { @@ -441,6 +452,7 @@ "url": { "fields": { "keyword": { + "ignore_above": 2048, "type": "keyword" } }, @@ -448,6 +460,14 @@ } } }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, "visualization": { "properties": { "description": { @@ -456,23 +476,28 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } @@ -483,7 +508,10 @@ "index": { "auto_expand_replicas": "0-1", "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" } } } diff --git a/test/functional/fixtures/es_archiver/deprecations_service/data.json b/test/functional/fixtures/es_archiver/deprecations_service/data.json index 31ce5af20b46c2..1c36f2e177fc55 100644 --- a/test/functional/fixtures/es_archiver/deprecations_service/data.json +++ b/test/functional/fixtures/es_archiver/deprecations_service/data.json @@ -1,14 +1,18 @@ { "type": "doc", "value": { - "index": ".kibana", "id": "test-deprecations-plugin:ff3733a0-9fty-11e7-ahb3-3dcb94193fab", + "index": ".kibana", "source": { - "type": "test-deprecations-plugin", - "updated_at": "2021-02-11T18:51:23.794Z", + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-deprecations-plugin": { "title": "Test saved object" - } - } + }, + "type": "test-deprecations-plugin", + "updated_at": "2021-02-11T18:51:23.794Z" + }, + "type": "_doc" } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/deprecations_service/mappings.json b/test/functional/fixtures/es_archiver/deprecations_service/mappings.json index 41cddecef0c419..5159b946e082f7 100644 --- a/test/functional/fixtures/es_archiver/deprecations_service/mappings.json +++ b/test/functional/fixtures/es_archiver/deprecations_service/mappings.json @@ -2,132 +2,285 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, + "dynamic": "strict", "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { - "dynamic": "strict", "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, "index-pattern": { - "dynamic": "strict", + "dynamic": "false", "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { + "title": { "type": "text" }, - "intervalName": { + "type": { "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "notExpandable": { + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { "type": "boolean" }, - "sourceFilters": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "timeFieldName": { + "targetType": { "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "type": "object" + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" }, "title": { "type": "text" } } }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, "search": { - "dynamic": "strict", "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -138,16 +291,47 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "dynamic": "strict", + "dynamic": "false", + "type": "object" + }, + "telemetry": { "properties": { - "uuid": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" } } }, + "test-deprecations-plugin": { + "dynamic": "false", + "type": "object" + }, "timelion-sheet": { - "dynamic": "strict", "properties": { "description": { "type": "text" @@ -191,8 +375,24 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, "url": { - "dynamic": "strict", "properties": { "accessCount": { "type": "long" @@ -214,8 +414,15 @@ } } }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, "visualization": { - "dynamic": "strict", "properties": { "description": { "type": "text" @@ -223,59 +430,28 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { - "type": "text" - } - } - }, - "query": { - "properties": { - "title": { - "type": "text" - }, - "description": { - "type": "text" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "type": "keyword", - "index": false - } - } - }, - "filters": { - "type": "object", - "enabled": false - }, - "timefilter": { - "type": "object", - "enabled": false - } - } - }, - "test-deprecations-plugin": { - "properties": { - "title": { + "index": false, "type": "text" } } @@ -284,9 +460,13 @@ }, "settings": { "index": { + "auto_expand_replicas": "0-1", "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json index 0f2edc8c510c3f..14a9f0559c6f3a 100644 --- a/test/functional/fixtures/es_archiver/discover/data.json +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -4,14 +4,21 @@ "id": "index-pattern:logstash-*", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", "index-pattern": { + "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}", "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", "timeFieldName": "@timestamp", - "title": "logstash-*", - "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}" + "title": "logstash-*" }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], "type": "index-pattern" - } + }, + "type": "_doc" } } @@ -21,6 +28,17 @@ "id": "search:ab12e3c0-f231-11e6-9486-733b1ac9221a", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], "search": { "columns": [ "_source" @@ -28,16 +46,19 @@ "description": "A Saved Search Description", "hits": 0, "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\n \"index\": \"logstash-*\",\n \"highlightAll\": true,\n \"filter\": [],\n \"query\": {\n \"query_string\": {\n \"query\": \"*\",\n \"analyze_wildcard\": true\n }\n }\n}" + "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "sort": [ - "@timestamp", - "desc" + [ + "@timestamp", + "desc" + ] ], "title": "A Saved Search", "version": 1 }, "type": "search" - } + }, + "type": "_doc" } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/discover/mappings.json b/test/functional/fixtures/es_archiver/discover/mappings.json index 519af2dd75b9e1..93d724aa556039 100644 --- a/test/functional/fixtures/es_archiver/discover/mappings.json +++ b/test/functional/fixtures/es_archiver/discover/mappings.json @@ -2,126 +2,304 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, + "dynamic": "strict", "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { - "dynamic": "strict", "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, "index-pattern": { - "dynamic": "strict", + "dynamic": "false", "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { + "title": { "type": "text" }, - "intervalName": { + "type": { "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "notExpandable": { + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { "type": "boolean" }, - "sourceFilters": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "timeFieldName": { + "targetType": { "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" }, - "title": { + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" }, - "fieldAttrs": { + "title": { "type": "text" } } }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, "search": { - "dynamic": "strict", "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -132,16 +310,43 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "dynamic": "strict", + "dynamic": "false", + "type": "object" + }, + "telemetry": { "properties": { - "uuid": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" } } }, "timelion-sheet": { - "dynamic": "strict", "properties": { "description": { "type": "text" @@ -185,8 +390,24 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, "url": { - "dynamic": "strict", "properties": { "accessCount": { "type": "long" @@ -208,8 +429,15 @@ } } }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, "visualization": { - "dynamic": "strict", "properties": { "description": { "type": "text" @@ -217,63 +445,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } - }, - "query": { - "properties": { - "title": { - "type": "text" - }, - "description": { - "type": "text" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "type": "keyword", - "index": false - } - } - }, - "filters": { - "type": "object", - "enabled": false - }, - "timefilter": { - "type": "object", - "enabled": false - } - } } } }, "settings": { "index": { - "number_of_replicas": "1", - "number_of_shards": "1" + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json index 7f4043958bc89b..54feabcf54331d 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json @@ -1,149 +1,161 @@ { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-transform:type_1-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-transform": { - "title": "test_1-obj_1", - "enabled": true + "enabled": true, + "title": "test_1-obj_1" }, "type": "test-export-transform", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-transform:type_1-obj_2", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-transform": { - "title": "test_1-obj_2", - "enabled": true + "enabled": true, + "title": "test_1-obj_2" }, "type": "test-export-transform", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-add:type_2-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-add": { "title": "test_2-obj_1" }, "type": "test-export-add", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-add:type_2-obj_2", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-add": { "title": "test_2-obj_2" }, "type": "test-export-add", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-add-dep:type_dep-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + { + "id": "type_2-obj_1", + "type": "test-export-add" + } + ], "test-export-add-dep": { "title": "type_dep-obj_1" }, "type": "test-export-add-dep", - "migrationVersion": {}, - "updated_at": "2018-12-21T00:43:07.096Z", - "references": [ - { - "type": "test-export-add", - "id": "type_2-obj_1" - } - ] - } + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-add-dep:type_dep-obj_2", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + { + "id": "type_2-obj_2", + "type": "test-export-add" + } + ], "test-export-add-dep": { "title": "type_dep-obj_2" }, "type": "test-export-add-dep", - "migrationVersion": {}, - "updated_at": "2018-12-21T00:43:07.096Z", - "references": [ - { - "type": "test-export-add", - "id": "type_2-obj_2" - } - ] - } + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-invalid-transform:type_3-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-invalid-transform": { "title": "test_2-obj_1" }, "type": "test-export-invalid-transform", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-transform-error:type_4-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-transform-error": { "title": "test_2-obj_1" }, "type": "test-export-transform-error", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json index 653e6399548135..b2385a281dd239 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json @@ -2,333 +2,301 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, "dynamic": "strict", "properties": { - "test-export-transform": { - "properties": { - "title": { "type": "text" }, - "enabled": { "type": "boolean" } - } - }, - "test-export-add": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-add-dep": { - "properties": { - "title": { "type": "text" } - } + "apm-telemetry": { + "dynamic": "false", + "type": "object" }, - "test-export-transform-error": { + "application_usage_daily": { + "dynamic": "false", "properties": { - "title": { "type": "text" } + "timestamp": { + "type": "date" + } } }, - "test-export-invalid-transform": { - "properties": { - "title": { "type": "text" } - } + "application_usage_totals": { + "dynamic": "false", + "type": "object" }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } + "application_usage_transactional": { + "dynamic": "false", + "type": "object" }, "canvas-workpad": { "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } + "type": "object" }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, "buildNum": { "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, - "map": { + "graph-workspace": { + "dynamic": "false", + "type": "object" + }, + "index-pattern": { + "dynamic": "false", "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { + "title": { "type": "text" }, - "mapStateJSON": { - "type": "text" + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "title": { - "type": "text" + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" }, - "uiStateJSON": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "version": { - "type": "integer" + "targetType": { + "type": "keyword" } } }, - "graph-workspace": { + "map": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "type": "object" + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { "properties": { "description": { "type": "text" }, - "kibanaSavedObjectMeta": { + "filters": { + "enabled": false, + "type": "object" + }, + "query": { "properties": { - "searchSourceJSON": { - "type": "text" + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" } } }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" + "timefilter": { + "enabled": false, + "type": "object" }, "title": { "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" } } }, - "index-pattern": { + "references": { "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { + "id": { "type": "keyword" }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { + "name": { "type": "keyword" }, - "title": { - "type": "text" - }, "type": { "type": "keyword" - }, - "typeMeta": { - "type": "keyword" } - } + }, + "type": "nested" }, - "kql-telemetry": { + "sample-data-telemetry": { "properties": { - "optInCount": { + "installCount": { "type": "long" }, - "optOutCount": { + "unInstallCount": { "type": "long" } } }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, "search": { "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -339,50 +307,68 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } + "dynamic": "false", + "type": "object" }, "space": { + "dynamic": "false", + "type": "object" + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { "properties": { - "_reserved": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { "type": "boolean" }, - "color": { + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { "type": "keyword" }, - "description": { - "type": "text" + "reportFailureCount": { + "type": "integer" }, - "disabledFeatures": { + "reportFailureVersion": { "type": "keyword" }, - "initials": { + "sendUsageFrom": { "type": "keyword" }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } + "userHasSeenNotice": { + "type": "boolean" } } }, - "spaceId": { - "type": "keyword" + "test-export-add": { + "dynamic": "false", + "type": "object" }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } + "test-export-add-dep": { + "dynamic": "false", + "type": "object" + }, + "test-export-invalid-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform-error": { + "dynamic": "false", + "type": "object" }, "timelion-sheet": { "properties": { @@ -428,6 +414,20 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, "updated_at": { "type": "date" }, @@ -443,13 +443,21 @@ "type": "date" }, "url": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" } - } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" } } }, @@ -461,42 +469,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" } } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json index 6a272dc16e462f..4996c050adbf0d 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json @@ -1,29 +1,37 @@ { "type": "doc", "value": { - "index": ".kibana", "id": "test-hidden-importable-exportable:ff3733a0-9fty-11e7-ahb3-3dcb94193fab", + "index": ".kibana", "source": { - "type": "test-hidden-importable-exportable", - "updated_at": "2021-02-11T18:51:23.794Z", + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-hidden-importable-exportable": { "title": "Hidden Saved object type that is importable/exportable." - } - } + }, + "type": "test-hidden-importable-exportable", + "updated_at": "2021-02-11T18:51:23.794Z" + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", "id": "test-hidden-non-importable-exportable:op3767a1-9rcg-53u7-jkb3-3dnb74193awc", + "index": ".kibana", "source": { - "type": "test-hidden-non-importable-exportable", - "updated_at": "2021-02-11T18:51:23.794Z", + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-hidden-non-importable-exportable": { "title": "Hidden Saved object type that is not importable/exportable." - } - } + }, + "type": "test-hidden-non-importable-exportable", + "updated_at": "2021-02-11T18:51:23.794Z" + }, + "type": "_doc" } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json index a158deb527cc83..d59f3b00d48182 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json @@ -2,333 +2,301 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, "dynamic": "strict", "properties": { - "test-export-transform": { - "properties": { - "title": { "type": "text" }, - "enabled": { "type": "boolean" } - } - }, - "test-export-add": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-add-dep": { - "properties": { - "title": { "type": "text" } - } + "apm-telemetry": { + "dynamic": "false", + "type": "object" }, - "test-export-transform-error": { + "application_usage_daily": { + "dynamic": "false", "properties": { - "title": { "type": "text" } + "timestamp": { + "type": "date" + } } }, - "test-export-invalid-transform": { - "properties": { - "title": { "type": "text" } - } + "application_usage_totals": { + "dynamic": "false", + "type": "object" }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } + "application_usage_transactional": { + "dynamic": "false", + "type": "object" }, "canvas-workpad": { "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } + "type": "object" }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, "buildNum": { "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, - "map": { + "graph-workspace": { + "dynamic": "false", + "type": "object" + }, + "index-pattern": { + "dynamic": "false", "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { + "title": { "type": "text" }, - "mapStateJSON": { - "type": "text" + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "title": { - "type": "text" + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" }, - "uiStateJSON": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "version": { - "type": "integer" + "targetType": { + "type": "keyword" } } }, - "graph-workspace": { + "map": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "type": "object" + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { "properties": { "description": { "type": "text" }, - "kibanaSavedObjectMeta": { + "filters": { + "enabled": false, + "type": "object" + }, + "query": { "properties": { - "searchSourceJSON": { - "type": "text" + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" } } }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" + "timefilter": { + "enabled": false, + "type": "object" }, "title": { "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" } } }, - "index-pattern": { + "references": { "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { + "id": { "type": "keyword" }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { + "name": { "type": "keyword" }, - "title": { - "type": "text" - }, "type": { "type": "keyword" - }, - "typeMeta": { - "type": "keyword" } - } + }, + "type": "nested" }, - "kql-telemetry": { + "sample-data-telemetry": { "properties": { - "optInCount": { + "installCount": { "type": "long" }, - "optOutCount": { + "unInstallCount": { "type": "long" } } }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, "search": { "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -339,50 +307,76 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } + "dynamic": "false", + "type": "object" }, "space": { + "dynamic": "false", + "type": "object" + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { "properties": { - "_reserved": { + "allowChangingOptInStatus": { "type": "boolean" }, - "color": { + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { "type": "keyword" }, - "description": { - "type": "text" + "reportFailureCount": { + "type": "integer" }, - "disabledFeatures": { + "reportFailureVersion": { "type": "keyword" }, - "initials": { + "sendUsageFrom": { "type": "keyword" }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } + "userHasSeenNotice": { + "type": "boolean" } } }, - "spaceId": { - "type": "keyword" + "test-export-add": { + "dynamic": "false", + "type": "object" }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } + "test-export-add-dep": { + "dynamic": "false", + "type": "object" + }, + "test-export-invalid-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform-error": { + "dynamic": "false", + "type": "object" + }, + "test-hidden-importable-exportable": { + "dynamic": "false", + "type": "object" + }, + "test-hidden-non-importable-exportable": { + "dynamic": "false", + "type": "object" }, "timelion-sheet": { "properties": { @@ -428,6 +422,20 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, "updated_at": { "type": "date" }, @@ -443,13 +451,21 @@ "type": "date" }, "url": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" } - } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" } } }, @@ -461,56 +477,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "test-hidden-non-importable-exportable": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-hidden-importable-exportable": { - "properties": { - "title": { + "index": false, "type": "text" } } } } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json index caac89461b9ef0..3c311b0465193a 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json @@ -1,87 +1,91 @@ { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-transform:type_1-obj_1", + "index": ".kibana", "source": { - "test-export-transform": { - "title": "test_1-obj_1", - "enabled": true - }, - "type": "test-export-transform", - "migrationVersion": {}, - "updated_at": "2018-12-21T00:43:07.096Z", + "coreMigrationVersion": "7.14.0", "references": [ { - "type": "test-export-transform", "id": "type_1-obj_2", - "name": "ref-1" + "name": "ref-1", + "type": "test-export-transform" }, { - "type": "test-export-add", "id": "type_2-obj_1", - "name": "ref-2" + "name": "ref-2", + "type": "test-export-add" } - ] - } + ], + "test-export-transform": { + "enabled": true, + "title": "test_1-obj_1" + }, + "type": "test-export-transform", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-transform:type_1-obj_2", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-transform": { - "title": "test_1-obj_2", - "enabled": true + "enabled": true, + "title": "test_1-obj_2" }, "type": "test-export-transform", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-add:type_2-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], "test-export-add": { "title": "test_2-obj_1" }, "type": "test-export-add", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z" - } + }, + "type": "_doc" } } { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", "id": "test-export-add-dep:type_dep-obj_1", + "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + { + "id": "type_2-obj_1", + "type": "test-export-add" + } + ], "test-export-add-dep": { "title": "type_dep-obj_1" }, "type": "test-export-add-dep", - "migrationVersion": {}, - "updated_at": "2018-12-21T00:43:07.096Z", - "references": [ - { - "type": "test-export-add", - "id": "type_2-obj_1" - } - ] - } + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/mappings.json index 653e6399548135..b2385a281dd239 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/mappings.json @@ -2,333 +2,301 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, "dynamic": "strict", "properties": { - "test-export-transform": { - "properties": { - "title": { "type": "text" }, - "enabled": { "type": "boolean" } - } - }, - "test-export-add": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-add-dep": { - "properties": { - "title": { "type": "text" } - } + "apm-telemetry": { + "dynamic": "false", + "type": "object" }, - "test-export-transform-error": { + "application_usage_daily": { + "dynamic": "false", "properties": { - "title": { "type": "text" } + "timestamp": { + "type": "date" + } } }, - "test-export-invalid-transform": { - "properties": { - "title": { "type": "text" } - } + "application_usage_totals": { + "dynamic": "false", + "type": "object" }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } + "application_usage_transactional": { + "dynamic": "false", + "type": "object" }, "canvas-workpad": { "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } + "type": "object" }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, "buildNum": { "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, - "map": { + "graph-workspace": { + "dynamic": "false", + "type": "object" + }, + "index-pattern": { + "dynamic": "false", "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { + "title": { "type": "text" }, - "mapStateJSON": { - "type": "text" + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "title": { - "type": "text" + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" }, - "uiStateJSON": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "version": { - "type": "integer" + "targetType": { + "type": "keyword" } } }, - "graph-workspace": { + "map": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "type": "object" + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { "properties": { "description": { "type": "text" }, - "kibanaSavedObjectMeta": { + "filters": { + "enabled": false, + "type": "object" + }, + "query": { "properties": { - "searchSourceJSON": { - "type": "text" + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" } } }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" + "timefilter": { + "enabled": false, + "type": "object" }, "title": { "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" } } }, - "index-pattern": { + "references": { "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { + "id": { "type": "keyword" }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { + "name": { "type": "keyword" }, - "title": { - "type": "text" - }, "type": { "type": "keyword" - }, - "typeMeta": { - "type": "keyword" } - } + }, + "type": "nested" }, - "kql-telemetry": { + "sample-data-telemetry": { "properties": { - "optInCount": { + "installCount": { "type": "long" }, - "optOutCount": { + "unInstallCount": { "type": "long" } } }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, "search": { "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -339,50 +307,68 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } + "dynamic": "false", + "type": "object" }, "space": { + "dynamic": "false", + "type": "object" + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { "properties": { - "_reserved": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { "type": "boolean" }, - "color": { + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { "type": "keyword" }, - "description": { - "type": "text" + "reportFailureCount": { + "type": "integer" }, - "disabledFeatures": { + "reportFailureVersion": { "type": "keyword" }, - "initials": { + "sendUsageFrom": { "type": "keyword" }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } + "userHasSeenNotice": { + "type": "boolean" } } }, - "spaceId": { - "type": "keyword" + "test-export-add": { + "dynamic": "false", + "type": "object" }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } + "test-export-add-dep": { + "dynamic": "false", + "type": "object" + }, + "test-export-invalid-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform-error": { + "dynamic": "false", + "type": "object" }, "timelion-sheet": { "properties": { @@ -428,6 +414,20 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, "updated_at": { "type": "date" }, @@ -443,13 +443,21 @@ "type": "date" }, "url": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" } - } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" } } }, @@ -461,42 +469,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" } } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index f337bffe80f2cd..d48aa3e98d18a8 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -4,15 +4,22 @@ "id": "index-pattern:logstash-*", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", "index-pattern": { + "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "timeFieldName": "@timestamp", - "title": "logstash-*", - "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}" + "title": "logstash-*" }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], "type": "index-pattern" - } + }, + "type": "_doc" } } @@ -22,13 +29,20 @@ "id": "index-pattern:logstash*", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", "index-pattern": { "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "title": "logstash*" }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], "type": "index-pattern" - } + }, + "type": "_doc" } } @@ -38,14 +52,21 @@ "id": "index-pattern:long-window-logstash-*", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", "index-pattern": { "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "timeFieldName": "@timestamp", "title": "long-window-logstash-*" }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], "type": "index-pattern" - } + }, + "type": "_doc" } } @@ -55,18 +76,30 @@ "id": "visualization:Shared-Item-Visualization-AreaChart", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], "type": "visualization", "visualization": { "description": "AreaChart", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "title": "Shared-Item Visualization AreaChart", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" } - } + }, + "type": "_doc" } } @@ -76,18 +109,30 @@ "id": "visualization:Visualization-AreaChart", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], "type": "visualization", "visualization": { "description": "AreaChart", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "title": "Visualization AreaChart", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" } - } + }, + "type": "_doc" } } @@ -97,34 +142,36 @@ "id": "visualization:68305470-87bc-11e9-a991-3b492a7c3e09", "index": ".kibana", "source": { - "visualization" : { - "title" : "chained input control", - "visState" : "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}", - "uiStateJSON" : "{}", - "description" : "", - "version" : 1, - "kibanaSavedObjectMeta" : { - "searchSourceJSON" : "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - } + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" }, - "type" : "visualization", - "references" : [ + "references": [ { - "name" : "control_0_index_pattern", - "type" : "index-pattern", - "id" : "logstash-*" + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" }, { - "name" : "control_1_index_pattern", - "type" : "index-pattern", - "id" : "logstash-*" + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" } ], - "migrationVersion" : { - "visualization" : "7.3.0" - }, - "updated_at" : "2019-06-05T18:04:48.310Z" - } + "type": "visualization", + "updated_at": "2019-06-05T18:04:48.310Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + }, + "type": "_doc" } } @@ -134,29 +181,31 @@ "id": "visualization:64983230-87bf-11e9-a991-3b492a7c3e09", "index": ".kibana", "source": { - "visualization" : { - "title" : "dynamic options input control", - "visState" : "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}", - "uiStateJSON" : "{}", - "description" : "", - "version" : 1, - "kibanaSavedObjectMeta" : { - "searchSourceJSON" : "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - } + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" }, - "type" : "visualization", - "references" : [ + "references": [ { - "name" : "control_0_index_pattern", - "type" : "index-pattern", - "id" : "logstash-*" + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" } ], - "migrationVersion" : { - "visualization" : "7.3.0" - }, - "updated_at" : "2019-06-05T18:26:10.771Z" - } + "type": "visualization", + "updated_at": "2019-06-05T18:26:10.771Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "dynamic options input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + }, + "type": "_doc" } } @@ -166,34 +215,36 @@ "id": "visualization:5d2de430-87c0-11e9-a991-3b492a7c3e09", "index": ".kibana", "source": { - "visualization" : { - "title" : "chained input control with dynamic options", - "visState" : "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}", - "uiStateJSON" : "{}", - "description" : "", - "version" : 1, - "kibanaSavedObjectMeta" : { - "searchSourceJSON" : "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - } + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" }, - "type" : "visualization", - "references" : [ + "references": [ { - "name" : "control_0_index_pattern", - "type" : "index-pattern", - "id" : "logstash-*" + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" }, { - "name" : "control_1_index_pattern", - "type" : "index-pattern", - "id" : "logstash-*" + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" } ], - "migrationVersion" : { - "visualization" : "7.3.0" - }, - "updated_at" : "2019-06-05T18:33:07.827Z" - } + "type": "visualization", + "updated_at": "2019-06-05T18:33:07.827Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control with dynamic options", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + } + }, + "type": "_doc" } } @@ -203,12 +254,19 @@ "id": "index-pattern:test_index*", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", "index-pattern": { "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", "title": "test_index*" }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], "type": "index-pattern" - } + }, + "type": "_doc" } } @@ -218,18 +276,30 @@ "id": "visualization:AreaChart-no-date-field", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "test_index*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], "type": "visualization", "visualization": { "description": "AreaChart", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"test_index*\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "title": "AreaChart [no date field]", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" - } - } + "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" } } @@ -239,13 +309,20 @@ "id": "index-pattern:log*", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", "index-pattern": { "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "title": "log*" }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], "type": "index-pattern" - } + }, + "type": "_doc" } } @@ -255,18 +332,30 @@ "id": "visualization:AreaChart-no-time-filter", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "log*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], "type": "visualization", "visualization": { "description": "AreaChart", "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"log*\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "title": "AreaChart [no time filter]", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" - } - } + "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" } } @@ -276,6 +365,12 @@ "id": "visualization:VegaMap", "index": ".kibana", "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + ], "type": "visualization", "visualization": { "description": "VegaMap", @@ -287,6 +382,7 @@ "version": 1, "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" } - } + }, + "type": "_doc" } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize/mappings.json b/test/functional/fixtures/es_archiver/visualize/mappings.json index 59ec24853e2270..d032352d9a6886 100644 --- a/test/functional/fixtures/es_archiver/visualize/mappings.json +++ b/test/functional/fixtures/es_archiver/visualize/mappings.json @@ -2,126 +2,304 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, + "dynamic": "strict", "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { - "dynamic": "strict", "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, "index-pattern": { - "dynamic": "strict", + "dynamic": "false", "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { + "title": { "type": "text" }, - "intervalName": { + "type": { "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "notExpandable": { + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { "type": "boolean" }, - "sourceFilters": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "timeFieldName": { + "targetType": { "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" }, - "title": { + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { "type": "text" }, - "fieldAttrs": { + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { "type": "text" } } }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, "search": { - "dynamic": "strict", "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -132,16 +310,43 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { - "dynamic": "strict", + "dynamic": "false", + "type": "object" + }, + "telemetry": { "properties": { - "uuid": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" } } }, "timelion-sheet": { - "dynamic": "strict", "properties": { "description": { "type": "text" @@ -185,8 +390,24 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, "url": { - "dynamic": "strict", "properties": { "accessCount": { "type": "long" @@ -208,8 +429,15 @@ } } }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, "visualization": { - "dynamic": "strict", "properties": { "description": { "type": "text" @@ -217,23 +445,28 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } @@ -242,37 +475,13 @@ }, "settings": { "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "references": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" } } } -} +} \ No newline at end of file From 9bcae4d9cb82088f4bc5850c6d6481a99dd30807 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Fri, 11 Jun 2021 09:53:04 +0200 Subject: [PATCH 42/99] [Expressions] Refactor expression functions to use observables underneath (#100409) --- ...ons-public.expressionfunctiondefinition.md | 2 +- ...ublic.expressionfunctiondefinition.type.md | 2 +- ...ons-server.expressionfunctiondefinition.md | 2 +- ...erver.expressionfunctiondefinition.type.md | 2 +- .../expression_functions/specs/map_column.ts | 101 +++--- .../specs/tests/map_column.test.ts | 316 +++++++++++------- .../common/expression_functions/types.ts | 5 +- src/plugins/expressions/public/public.api.md | 2 +- src/plugins/expressions/server/server.api.md | 2 +- .../functions/common/case.test.js | 91 +++-- .../functions/common/case.ts | 44 +-- .../functions/common/filterrows.test.js | 38 ++- .../functions/common/filterrows.ts | 28 +- .../functions/common/if.test.js | 105 +++--- .../canvas_plugin_src/functions/common/if.ts | 20 +- .../functions/common/ply.test.js | 104 ++++-- .../canvas_plugin_src/functions/common/ply.ts | 122 +++---- .../functions/common/switch.test.js | 38 ++- .../functions/common/switch.ts | 35 +- 19 files changed, 605 insertions(+), 454 deletions(-) diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md index 449cc66cb3335f..34de4f9e13cda4 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md @@ -23,7 +23,7 @@ export interface ExpressionFunctionDefinitionstring | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | List of allowed type names for input value of this function. If this property is set the input of function will be cast to the first possible type in this list. If this property is missing the input will be provided to the function as-is. | | [name](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | -| [type](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md) | TypeToString<UnwrapPromiseOrReturn<Output>> | Name of type of value this function outputs. | +| [type](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md) | TypeString<Output> | UnmappedTypeStrings | Name of type of value this function outputs. | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md index 4831f24a418bc6..01ad35b8a1ba5c 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md @@ -9,5 +9,5 @@ Name of type of value this function outputs. Signature: ```typescript -type?: TypeToString>; +type?: TypeString | UnmappedTypeStrings; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md index 51240f094b181a..35248c01a4e29d 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md @@ -23,7 +23,7 @@ export interface ExpressionFunctionDefinitionstring | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | List of allowed type names for input value of this function. If this property is set the input of function will be cast to the first possible type in this list. If this property is missing the input will be provided to the function as-is. | | [name](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | -| [type](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md) | TypeToString<UnwrapPromiseOrReturn<Output>> | Name of type of value this function outputs. | +| [type](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md) | TypeString<Output> | UnmappedTypeStrings | Name of type of value this function outputs. | ## Methods diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md index a73ded342f053d..2994b9547fd8c0 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md @@ -9,5 +9,5 @@ Name of type of value this function outputs. Signature: ```typescript -type?: TypeToString>; +type?: TypeString | UnmappedTypeStrings; ``` diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts index d6af19d9dbf531..7ea96ee7fdde82 100644 --- a/src/plugins/expressions/common/expression_functions/specs/map_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { Observable, defer, of, zip } from 'rxjs'; +import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; import { Datatable, DatatableColumn, getType } from '../../expression_types'; @@ -15,7 +15,7 @@ import { Datatable, DatatableColumn, getType } from '../../expression_types'; export interface MapColumnArguments { id?: string | null; name: string; - expression?(datatable: Datatable): Observable; + expression(datatable: Datatable): Observable; copyMetaFrom?: string | null; } @@ -23,7 +23,7 @@ export const mapColumn: ExpressionFunctionDefinition< 'mapColumn', Datatable, MapColumnArguments, - Promise + Observable > = { name: 'mapColumn', aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file. @@ -80,57 +80,56 @@ export const mapColumn: ExpressionFunctionDefinition< default: null, }, }, - fn: (input, args) => { - const expression = (...params: Parameters['expression']>) => - args - .expression?.(...params) - .pipe(take(1)) - .toPromise() ?? Promise.resolve(null); + fn(input, args) { + const existingColumnIndex = input.columns.findIndex(({ id, name }) => + args.id ? id === args.id : name === args.name + ); + const id = input.columns[existingColumnIndex]?.id ?? args.id ?? args.name; - const columns = [...input.columns]; - const existingColumnIndex = columns.findIndex(({ id, name }) => { - if (args.id) { - return id === args.id; - } - return name === args.name; - }); - const columnId = - existingColumnIndex === -1 ? args.id ?? args.name : columns[existingColumnIndex].id; - - const rowPromises = input.rows.map((row) => { - return expression({ - type: 'datatable', - columns, - rows: [row], - }).then((val) => ({ - ...row, - [columnId]: val, - })); - }); + return defer(() => { + const rows$ = input.rows.length + ? zip( + ...input.rows.map((row) => + args + .expression({ + type: 'datatable', + columns: [...input.columns], + rows: [row], + }) + .pipe(map((value) => ({ ...row, [id]: value }))) + ) + ) + : of([]); - return Promise.all(rowPromises).then((rows) => { - const type = rows.length ? getType(rows[0][columnId]) : 'null'; - const newColumn: DatatableColumn = { - id: columnId, - name: args.name, - meta: { type, params: { id: type } }, - }; - if (args.copyMetaFrom) { - const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom); - newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta || {}) }; - } + return rows$.pipe( + map((rows) => { + const type = getType(rows[0]?.[id]); + const newColumn: DatatableColumn = { + id, + name: args.name, + meta: { type, params: { id: type } }, + }; + if (args.copyMetaFrom) { + const metaSourceFrom = input.columns.find( + ({ id: columnId }) => columnId === args.copyMetaFrom + ); + newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta ?? {}) }; + } - if (existingColumnIndex === -1) { - columns.push(newColumn); - } else { - columns[existingColumnIndex] = newColumn; - } + const columns = [...input.columns]; + if (existingColumnIndex === -1) { + columns.push(newColumn); + } else { + columns[existingColumnIndex] = newColumn; + } - return { - type: 'datatable', - columns, - rows, - } as Datatable; + return { + columns, + rows, + type: 'datatable', + }; + }) + ); }); }, }; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts index bb4e6303e90b7d..bd934745fed723 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; +import { of, Observable } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { Datatable } from '../../../expression_types'; import { mapColumn, MapColumnArguments } from '../map_column'; import { emptyTable, functionWrapper, testTable } from './utils'; @@ -16,142 +17,227 @@ const pricePlusTwo = (datatable: Datatable) => of(datatable.rows[0].price + 2); describe('mapColumn', () => { const fn = functionWrapper(mapColumn); const runFn = (input: Datatable, args: MapColumnArguments) => - fn(input, args) as Promise; + fn(input, args) as Observable; + let testScheduler: TestScheduler; - it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', async () => { - const arbitraryRowIndex = 2; - const result = await runFn(testTable, { - id: 'pricePlusTwo', - name: 'pricePlusTwo', - expression: pricePlusTwo, - }); - - expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([ - ...testTable.columns, - { - id: 'pricePlusTwo', - name: 'pricePlusTwo', - meta: { type: 'number', params: { id: 'number' } }, - }, - ]); - expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo'); + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); }); - it('allows the id arg to be optional, looking up by name instead', async () => { - const result = await runFn(testTable, { name: 'name label', expression: pricePlusTwo }); - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name label'); - const arbitraryRowIndex = 4; - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(testTable.columns.length); - expect(result.columns[nameColumnIndex]).toHaveProperty('id', 'name'); - expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name label'); - expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202); - expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('name label'); + it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(testTable, { + id: 'pricePlusTwo', + name: 'pricePlusTwo', + expression: pricePlusTwo, + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + ...testTable.columns, + { + id: 'pricePlusTwo', + name: 'pricePlusTwo', + meta: { type: 'number', params: { id: 'number' } }, + }, + ], + rows: expect.arrayContaining([ + expect.objectContaining({ + pricePlusTwo: expect.anything(), + }), + ]), + }), + ]); + }); }); - it('allows a duplicate name when the ids are different', async () => { - const result = await runFn(testTable, { - id: 'new', - name: 'name label', - expression: pricePlusTwo, + it('allows the id arg to be optional, looking up by name instead', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(runFn(testTable, { name: 'name label', expression: pricePlusTwo })).toBe( + '(0|)', + [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'name', + name: 'name label', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + name: 202, + }), + ]), + }), + ] + ); }); - const nameColumnIndex = result.columns.findIndex(({ id }) => id === 'new'); - const arbitraryRowIndex = 4; - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(testTable.columns.length + 1); - expect(result.columns[nameColumnIndex]).toHaveProperty('id', 'new'); - expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name label'); - expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('new', 202); }); - it('adds a column to empty tables', async () => { - const result = await runFn(emptyTable, { name: 'name', expression: pricePlusTwo }); + it('allows a duplicate name when the ids are different', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(testTable, { + id: 'new', + name: 'name label', + expression: pricePlusTwo, + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'new', + name: 'name label', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + new: 202, + }), + ]), + }), + ]); + }); + }); - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); + it('overwrites existing column with the new column if an existing column name is provided', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(runFn(testTable, { name: 'name', expression: pricePlusTwo })).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + name: 202, + }), + ]), + }), + ]); + }); }); - it('should assign specific id, different from name, when id arg is passed for new columns', async () => { - const result = await runFn(emptyTable, { name: 'name', id: 'myid', expression: pricePlusTwo }); + it('adds a column to empty tables', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(runFn(emptyTable, { name: 'name', expression: pricePlusTwo })).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + expect.objectContaining({ + name: 'name', + meta: expect.objectContaining({ type: 'null' }), + }), + ], + }), + ]); + }); + }); - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('id', 'myid'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); + it('should assign specific id, different from name, when id arg is passed for copied column', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(testTable, { name: 'name', id: 'myid', expression: pricePlusTwo }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'myid', + name: 'name', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + }), + ]); + }); }); - it('should copy over the meta information from the specified column', async () => { - const result = await runFn( - { - ...testTable, - columns: [ - ...testTable.columns, - // add a new entry + it('should copy over the meta information from the specified column', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn( { - id: 'myId', - name: 'myName', - meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + ...testTable, + columns: [ + ...testTable.columns, + // add a new entry + { + id: 'myId', + name: 'myName', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + }, + ], + rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })), }, - ], - rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })), - }, - { name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo } - ); - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); - - expect(result.type).toBe('datatable'); - expect(result.columns[nameColumnIndex]).toEqual({ - id: 'name', - name: 'name', - meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + { name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo } + ) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'name', + name: 'name', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + }), + ]), + }), + ]); }); }); - it('should be resilient if the references column for meta information does not exists', async () => { - const result = await runFn(emptyTable, { - name: 'name', - copyMetaFrom: 'time', - expression: pricePlusTwo, + it('should be resilient if the references column for meta information does not exists', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(emptyTable, { + name: 'name', + copyMetaFrom: 'time', + expression: pricePlusTwo, + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + expect.objectContaining({ + id: 'name', + name: 'name', + meta: expect.objectContaining({ type: 'null' }), + }), + ], + }), + ]); }); - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('id', 'name'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); }); - it('should correctly infer the type fromt he first row if the references column for meta information does not exists', async () => { - const result = await runFn( - { ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] }, - { name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo } - ); - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'value'); - expect(result.columns[0]).toHaveProperty('id', 'value'); - expect(result.columns[0].meta).toHaveProperty('type', 'number'); - }); - - describe('expression', () => { - it('maps null values to the new column', async () => { - const result = await runFn(testTable, { name: 'empty' }); - const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty'); - const arbitraryRowIndex = 8; - - expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty'); - expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null); + it('should correctly infer the type fromt he first row if the references column for meta information does not exists', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn( + { ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] }, + { name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo } + ) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + expect.objectContaining({ + id: 'value', + name: 'value', + meta: expect.objectContaining({ type: 'number' }), + }), + ], + }), + ]); }); }); }); diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index b91e16d1804aa6..e1378a27bdfc29 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { ArgumentType } from './arguments'; -import { TypeToString } from '../types/common'; +import { TypeToString, TypeString, UnmappedTypeStrings } from '../types/common'; import { ExecutionContext } from '../execution/types'; import { ExpressionFunctionClog, @@ -47,7 +46,7 @@ export interface ExpressionFunctionDefinition< /** * Name of type of value this function outputs. */ - type?: TypeToString>; + type?: TypeString | UnmappedTypeStrings; /** * List of allowed type names for input value of this function. If this diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 2cc7fc3118d61a..ea11f7e728e45f 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -375,7 +375,7 @@ export interface ExpressionFunctionDefinition>; name: Name; - type?: TypeToString>; + type?: TypeString | UnmappedTypeStrings; } // @public diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 12af0480fac93f..bc0980121b827e 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -347,7 +347,7 @@ export interface ExpressionFunctionDefinition>; name: Name; - type?: TypeToString>; + type?: TypeString | UnmappedTypeStrings; } // @public diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js index fc5d03190d4f8d..adee8a56dea494 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js @@ -6,11 +6,17 @@ */ import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { caseFn } from './case'; describe('case', () => { const fn = functionWrapper(caseFn); + let testScheduler; + + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); + }); describe('spec', () => { it('is a function', () => { @@ -19,29 +25,22 @@ describe('case', () => { }); describe('function', () => { - describe('no args', () => { - it('should return a case object that matches with the result as the context', () => { - const context = null; - const args = {}; - expect(fn(context, args)).resolves.toEqual({ - type: 'case', - matches: true, - result: context, - }); - }); - }); - describe('no if or value', () => { it('should return the result if provided', () => { const context = null; const args = { then: () => of('foo'), }; - expect(fn(context, args)).resolves.toEqual({ - type: 'case', - matches: true, - result: 'foo', - }); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', [ + { + type: 'case', + matches: true, + result: 'foo', + }, + ]) + ); }); }); @@ -49,11 +48,16 @@ describe('case', () => { it('should return as the matches prop', () => { const context = null; const args = { if: false }; - expect(fn(context, args)).resolves.toEqual({ - type: 'case', - matches: args.if, - result: context, - }); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', [ + { + type: 'case', + matches: args.if, + result: context, + }, + ]) + ); }); }); @@ -63,15 +67,23 @@ describe('case', () => { when: () => of('foo'), then: () => of('bar'), }; - expect(fn('foo', args)).resolves.toEqual({ - type: 'case', - matches: true, - result: 'bar', - }); - expect(fn('bar', args)).resolves.toEqual({ - type: 'case', - matches: false, - result: null, + + testScheduler.run(({ expectObservable }) => { + expectObservable(fn('foo', args)).toBe('(0|)', [ + { + type: 'case', + matches: true, + result: 'bar', + }, + ]); + + expectObservable(fn('bar', args)).toBe('(0|)', [ + { + type: 'case', + matches: false, + result: null, + }, + ]); }); }); }); @@ -81,13 +93,18 @@ describe('case', () => { const context = null; const args = { when: () => 'foo', - if: true, + if: false, }; - expect(fn(context, args)).resolves.toEqual({ - type: 'case', - matches: args.if, - result: context, - }); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', [ + { + type: 'case', + matches: args.if, + result: context, + }, + ]) + ); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts index 7fba5b74e9b207..89329d69827160 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { Observable, defer, isObservable, of } from 'rxjs'; +import { map, concatMap } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { when?(): Observable; if?: boolean; - then?(): Observable; + then(): Observable; } interface Case { @@ -22,7 +22,7 @@ interface Case { result: any; } -export function caseFn(): ExpressionFunctionDefinition<'case', any, Arguments, Promise> { +export function caseFn(): ExpressionFunctionDefinition<'case', any, Arguments, Observable> { const { help, args: argHelp } = getFunctionHelp().case; return { @@ -45,24 +45,24 @@ export function caseFn(): ExpressionFunctionDefinition<'case', any, Arguments, P help: argHelp.then!, }, }, - fn: async (input, args) => { - const matches = await doesMatch(input, args); - const result = matches ? await getResult(input, args) : null; - return { type: 'case', matches, result }; + fn(input, { if: condition, then, when }) { + return defer(() => { + const matches = condition ?? when?.().pipe(map((value) => value === input)) ?? true; + + return isObservable(matches) ? matches : of(matches); + }).pipe( + concatMap((matches) => + (matches ? then() : of(null)).pipe( + map( + (result): Case => ({ + matches, + result, + type: 'case', + }) + ) + ) + ) + ); }, }; } - -async function doesMatch(context: any, args: Arguments) { - if (typeof args.if !== 'undefined') { - return args.if; - } - if (typeof args.when !== 'undefined') { - return (await args.when().pipe(take(1)).toPromise()) === context; - } - return true; -} - -async function getResult(context: any, args: Arguments) { - return args.then?.().pipe(take(1)).toPromise() ?? context; -} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js index fdea4faa4ece28..8c328e3d8adf6e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js @@ -6,6 +6,7 @@ */ import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { testTable } from './__fixtures__/test_tables'; import { filterrows } from './filterrows'; @@ -15,31 +16,46 @@ const returnFalse = () => of(false); describe('filterrows', () => { const fn = functionWrapper(filterrows); + let testScheduler; + + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); + }); it('returns a datable', () => { - expect(fn(testTable, { fn: inStock })).resolves.toHaveProperty('type', 'datatable'); + testScheduler.run(({ expectObservable }) => + expectObservable(fn(testTable, { fn: inStock })).toBe('(0|)', [ + expect.objectContaining({ type: 'datatable' }), + ]) + ); }); it('keeps rows that evaluate to true and removes rows that evaluate to false', () => { const inStockRows = testTable.rows.filter((row) => row.in_stock); - expect(fn(testTable, { fn: inStock })).resolves.toEqual( - expect.objectContaining({ - columns: testTable.columns, - rows: inStockRows, - }) + testScheduler.run(({ expectObservable }) => + expectObservable(fn(testTable, { fn: inStock })).toBe('(0|)', [ + expect.objectContaining({ + columns: testTable.columns, + rows: inStockRows, + }), + ]) ); }); it('returns datatable with no rows when no rows meet function condition', () => { - expect(fn(testTable, { fn: returnFalse })).resolves.toEqual( - expect.objectContaining({ - rows: [], - }) + testScheduler.run(({ expectObservable }) => + expectObservable(fn(testTable, { fn: returnFalse })).toBe('(0|)', [ + expect.objectContaining({ + rows: [], + }), + ]) ); }); it('throws when no function is provided', () => { - expect(() => fn(testTable)).toThrow('fn is not a function'); + testScheduler.run(({ expectObservable }) => + expectObservable(fn(testTable)).toBe('#', {}, new TypeError('fn is not a function')) + ); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts index 082506f58e86fa..4923b835d2871e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { Observable, combineLatest, defer } from 'rxjs'; +import { map } from 'rxjs/operators'; import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; @@ -18,7 +18,7 @@ export function filterrows(): ExpressionFunctionDefinition< 'filterrows', Datatable, Arguments, - Promise + Observable > { const { help, args: argHelp } = getFunctionHelp().filterrows; @@ -38,24 +38,12 @@ export function filterrows(): ExpressionFunctionDefinition< }, }, fn(input, { fn }) { - const checks = input.rows.map((row) => - fn({ - ...input, - rows: [row], - }) - .pipe(take(1)) - .toPromise() + return defer(() => + combineLatest(input.rows.map((row) => fn({ ...input, rows: [row] }))) + ).pipe( + map((checks) => input.rows.filter((row, i) => checks[i])), + map((rows) => ({ ...input, rows })) ); - - return Promise.all(checks) - .then((results) => input.rows.filter((row, i) => results[i])) - .then( - (rows) => - ({ - ...input, - rows, - } as Datatable) - ); }, }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js index 8e1106644105e4..cab331807e44c8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js @@ -6,11 +6,17 @@ */ import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { ifFn } from './if'; describe('if', () => { const fn = functionWrapper(ifFn); + let testScheduler; + + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); + }); describe('spec', () => { it('is a function', () => { @@ -21,66 +27,73 @@ describe('if', () => { describe('function', () => { describe('condition passed', () => { it('with then', () => { - expect(fn(null, { condition: true, then: () => of('foo') })).resolves.toBe('foo'); - expect( - fn(null, { condition: true, then: () => of('foo'), else: () => of('bar') }) - ).resolves.toBe('foo'); + testScheduler.run(({ expectObservable }) => { + expectObservable(fn(null, { condition: true, then: () => of('foo') })).toBe('(0|)', [ + 'foo', + ]); + + expectObservable( + fn(null, { condition: true, then: () => of('foo'), else: () => of('bar') }) + ).toBe('(0|)', ['foo']); + }); }); it('without then', () => { - expect(fn(null, { condition: true })).resolves.toBe(null); - expect(fn('some context', { condition: true })).resolves.toBe('some context'); + testScheduler.run(({ expectObservable }) => { + expectObservable(fn(null, { condition: true })).toBe('(0|)', [null]); + expectObservable(fn('some context', { condition: true })).toBe('(0|)', ['some context']); + }); }); }); describe('condition failed', () => { - it('with else', () => - expect( - fn('some context', { - condition: false, - then: () => of('foo'), - else: () => of('bar'), - }) - ).resolves.toBe('bar')); - - it('without else', () => - expect(fn('some context', { condition: false, then: () => of('foo') })).resolves.toBe( - 'some context' - )); - }); - - describe('falsy values', () => { - describe('for then', () => { - it('with null', () => { - expect(fn('some context', { condition: true, then: () => of(null) })).resolves.toBe(null); - }); - - it('with false', () => { - expect(fn('some context', { condition: true, then: () => of(false) })).resolves.toBe( - false - ); - }); - - it('with 0', () => { - expect(fn('some context', { condition: true, then: () => of(0) })).resolves.toBe(0); + it('with else', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + fn('some context', { + condition: false, + then: () => of('foo'), + else: () => of('bar'), + }) + ).toBe('(0|)', ['bar']); }); }); - describe('for else', () => { - it('with null', () => { - expect(fn('some context', { condition: false, else: () => of(null) })).resolves.toBe( - null - ); + it('without else', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + fn('some context', { condition: false, then: () => of('foo') }) + ).toBe('(0|)', ['some context']); }); + }); + }); - it('with false', () => { - expect(fn('some context', { condition: false, else: () => of(false) })).resolves.toBe( - false - ); + describe('falsy values', () => { + // eslint-disable-next-line no-unsanitized/method + it.each` + value + ${null} + ${false} + ${0} + `('for then with $value', ({ value }) => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + fn('some context', { condition: true, then: () => of(value) }) + ).toBe('(0|)', [value]); }); + }); - it('with 0', () => { - expect(fn('some context', { condition: false, else: () => of(0) })).resolves.toBe(0); + // eslint-disable-next-line no-unsanitized/method + it.each` + value + ${null} + ${false} + ${0} + `('for else with $value', ({ value }) => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + fn('some context', { condition: false, else: () => of(value) }) + ).toBe('(0|)', [value]); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts index 6d7665db551e4b..82bfb87c173b0a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts @@ -5,18 +5,22 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { Observable, defer, of } from 'rxjs'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - condition: boolean | null; + condition: boolean; then?(): Observable; else?(): Observable; } -export function ifFn(): ExpressionFunctionDefinition<'if', unknown, Arguments, unknown> { +export function ifFn(): ExpressionFunctionDefinition< + 'if', + unknown, + Arguments, + Observable +> { const { help, args: argHelp } = getFunctionHelp().if; return { @@ -38,12 +42,8 @@ export function ifFn(): ExpressionFunctionDefinition<'if', unknown, Arguments, u help: argHelp.else!, }, }, - fn: async (input, args) => { - if (args.condition) { - return args.then?.().pipe(take(1)).toPromise() ?? input; - } else { - return args.else?.().pipe(take(1)).toPromise() ?? input; - } + fn(input, args) { + return defer(() => (args.condition ? args.then?.() : args.else?.()) ?? of(input)); }, }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js index 74eca79395a103..5bf100eb90f4ca 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js @@ -6,6 +6,7 @@ */ import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; import { testTable } from './__fixtures__/test_tables'; @@ -46,36 +47,56 @@ const rowCount = (datatable) => describe('ply', () => { const fn = functionWrapper(ply); + let testScheduler; - it('maps a function over sub datatables grouped by specified columns and merges results into one datatable', async () => { - const arbitaryRowIndex = 0; - const result = await fn(testTable, { - by: ['name', 'in_stock'], - expression: [averagePrice, rowCount], - }); + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); + }); - expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([ - { id: 'name', name: 'name', meta: { type: 'string' } }, - { id: 'in_stock', name: 'in_stock', meta: { type: 'boolean' } }, - { id: 'average_price', name: 'average_price', meta: { type: 'number' } }, - { id: 'row_count', name: 'row_count', meta: { type: 'number' } }, - ]); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('average_price'); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('row_count'); + it('maps a function over sub datatables grouped by specified columns and merges results into one datatable', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + fn(testTable, { + by: ['name', 'in_stock'], + expression: [averagePrice, rowCount], + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + { id: 'name', name: 'name', meta: { type: 'string' } }, + { id: 'in_stock', name: 'in_stock', meta: { type: 'boolean' } }, + { id: 'average_price', name: 'average_price', meta: { type: 'number' } }, + { id: 'row_count', name: 'row_count', meta: { type: 'number' } }, + ], + rows: expect.arrayContaining([ + expect.objectContaining({ + average_price: expect.anything(), + row_count: expect.anything(), + }), + ]), + }), + ]); + }); }); describe('missing args', () => { it('returns the original datatable if both args are missing', () => { - expect(fn(testTable)).resolves.toEqual(testTable); + testScheduler.run(({ expectObservable }) => { + expectObservable(fn(testTable)).toBe('(0|)', [testTable]); + }); }); describe('by', () => { it('passes the entire context into the expression when no columns are provided', () => { - expect(fn(testTable, { expression: [rowCount] })).resolves.toEqual({ - type: 'datatable', - rows: [{ row_count: testTable.rows.length }], - columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], + testScheduler.run(({ expectObservable }) => { + expectObservable(fn(testTable, { expression: [rowCount] })).toBe('(0|)', [ + { + type: 'datatable', + rows: [{ row_count: testTable.rows.length }], + columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], + }, + ]); }); }); @@ -91,24 +112,37 @@ describe('ply', () => { }); describe('expression', () => { - it('returns the original datatable grouped by the specified columns', async () => { - const arbitaryRowIndex = 6; - const result = await fn(testTable, { by: ['price', 'quantity'] }); - - expect(result.columns[0]).toHaveProperty('name', 'price'); - expect(result.columns[1]).toHaveProperty('name', 'quantity'); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('price'); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('quantity'); + it('returns the original datatable grouped by the specified columns', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(fn(testTable, { by: ['price', 'quantity'] })).toBe('(0|)', [ + expect.objectContaining({ + columns: expect.arrayContaining([ + expect.objectContaining({ name: 'price' }), + expect.objectContaining({ name: 'quantity' }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + price: expect.anything(), + quantity: expect.anything(), + }), + ]), + }), + ]); + }); }); it('throws when row counts do not match across resulting datatables', () => { - expect( - fn(testTable, { by: ['name'], expression: [doublePrice, rowCount] }) - ).rejects.toEqual( - expect.objectContaining({ - message: errors.rowCountMismatch().message, - }) - ); + testScheduler.run(({ expectObservable }) => { + expectObservable( + fn(testTable, { by: ['name'], expression: [doublePrice, rowCount] }) + ).toBe( + '#', + [], + expect.objectContaining({ + message: errors.rowCountMismatch().message, + }) + ); + }); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts index 514d7f73d48e47..322bf7fee980cc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { groupBy, flatten, pick, map } from 'lodash'; +import { combineLatest, defer, of, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { groupBy, flatten, pick, map as _map, uniqWith } from 'lodash'; import { Datatable, DatatableColumn, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { - by: string[]; + by?: string[]; expression: Array<(datatable: Datatable) => Observable>; } -type Output = Datatable | Promise; +type Output = Datatable | Observable; export function ply(): ExpressionFunctionDefinition<'ply', Datatable, Arguments, Output> { const { help, args: argHelp } = getFunctionHelp().ply; @@ -30,7 +30,7 @@ export function ply(): ExpressionFunctionDefinition<'ply', Datatable, Arguments, args: { by: { types: ['string'], - help: argHelp.by, + help: argHelp.by!, multi: true, }, expression: { @@ -41,86 +41,62 @@ export function ply(): ExpressionFunctionDefinition<'ply', Datatable, Arguments, help: argHelp.expression, }, }, - fn: (input, args) => { + fn(input, args) { if (!args) { return input; } - let byColumns: DatatableColumn[]; - let originalDatatables: Datatable[]; - - if (args.by) { - byColumns = args.by.map((by) => { - const column = input.columns.find((col) => col.name === by); + const byColumns = + args.by?.map((by) => { + const column = input.columns.find(({ name }) => name === by); if (!column) { throw errors.columnNotFound(by); } return column; - }); - - const keyedDatatables = groupBy(input.rows, (row) => JSON.stringify(pick(row, args.by))); - - originalDatatables = Object.values(keyedDatatables).map((rows) => ({ - ...input, - rows, - })); - } else { - originalDatatables = [input]; - } - - const datatablePromises = originalDatatables.map((originalDatatable) => { - let expressionResultPromises = []; - - if (args.expression) { - expressionResultPromises = args.expression.map((expression) => - expression(originalDatatable).pipe(take(1)).toPromise() + }) ?? []; + + const originalDatatables = args.by + ? Object.values( + groupBy(input.rows, (row) => JSON.stringify(pick(row, args.by!))) + ).map((rows) => ({ ...input, rows })) + : [input]; + + const datatables$ = originalDatatables.map((originalDatatable) => + combineLatest( + args.expression?.map((expression) => defer(() => expression(originalDatatable))) ?? [ + of(originalDatatable), + ] + ).pipe(map(combineAcross)) + ); + + return (datatables$.length ? combineLatest(datatables$) : of([])).pipe( + map((newDatatables) => { + // Here we're just merging each for the by splits, so it doesn't actually matter if the rows are the same length + const columns = combineColumns([byColumns].concat(_map(newDatatables, 'columns'))); + const rows = flatten( + newDatatables.map((datatable, index) => + datatable.rows.map((row) => ({ + ...pick(originalDatatables[index].rows[0], args.by!), + ...row, + })) + ) ); - } else { - expressionResultPromises.push(Promise.resolve(originalDatatable)); - } - - return Promise.all(expressionResultPromises).then(combineAcross); - }); - - return Promise.all(datatablePromises).then((newDatatables) => { - // Here we're just merging each for the by splits, so it doesn't actually matter if the rows are the same length - const columns = combineColumns([byColumns].concat(map(newDatatables, 'columns'))); - const rows = flatten( - newDatatables.map((dt, i) => { - const byColumnValues = pick(originalDatatables[i].rows[0], args.by); - return dt.rows.map((row) => ({ - ...byColumnValues, - ...row, - })); - }) - ); - - return { - type: 'datatable', - rows, - columns, - } as Datatable; - }); + + return { + type: 'datatable', + rows, + columns, + } as Datatable; + }) + ); }, }; } function combineColumns(arrayOfColumnsArrays: DatatableColumn[][]) { - return arrayOfColumnsArrays.reduce((resultingColumns, columns) => { - if (columns) { - columns.forEach((column) => { - if (resultingColumns.find((resultingColumn) => resultingColumn.name === column.name)) { - return; - } else { - resultingColumns.push(column); - } - }); - } - - return resultingColumns; - }, []); + return uniqWith(arrayOfColumnsArrays.flat(), ({ name: a }, { name: b }) => a === b); } // This handles merging the tables produced by multiple expressions run on a single member of the `by` split. @@ -138,17 +114,17 @@ function combineAcross(datatableArray: Datatable[]) { }); // Merge columns and rows. - const arrayOfRowsArrays = map(datatableArray, 'rows'); + const arrayOfRowsArrays = _map(datatableArray, 'rows'); const rows = []; for (let i = 0; i < targetRowLength; i++) { - const rowsAcross = map(arrayOfRowsArrays, i); + const rowsAcross = _map(arrayOfRowsArrays, i); // The reason for the Object.assign is that rowsAcross is an array // and those rows need to be applied as arguments to Object.assign rows.push(Object.assign({}, ...rowsAcross)); } - const columns = combineColumns(map(datatableArray, 'columns')); + const columns = combineColumns(_map(datatableArray, 'columns')); return { type: 'datatable', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js index 6d9a20dfeb4873..7a6d483d6c72bc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js @@ -6,6 +6,7 @@ */ import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { switchFn } from './switch'; @@ -39,7 +40,13 @@ describe('switch', () => { result: 5, }, ]; - const nonMatchingCases = mockCases.filter((c) => !c.matches); + const nonMatchingCases = mockCases.filter(({ matches }) => !matches); + + let testScheduler; + + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); + }); describe('spec', () => { it('is a function', () => { @@ -51,13 +58,19 @@ describe('switch', () => { describe('with no cases', () => { it('should return the context if no default is provided', () => { const context = 'foo'; - expect(fn(context, {})).resolves.toBe(context); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, {})).toBe('(0|)', [context]) + ); }); it('should return the default if provided', () => { const context = 'foo'; const args = { default: () => of('bar') }; - expect(fn(context, args)).resolves.toBe('bar'); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', ['bar']) + ); }); }); @@ -65,7 +78,10 @@ describe('switch', () => { it('should return the context if no default is provided', () => { const context = 'foo'; const args = { case: nonMatchingCases.map(getter) }; - expect(fn(context, args)).resolves.toBe(context); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', [context]) + ); }); it('should return the default if provided', () => { @@ -74,16 +90,22 @@ describe('switch', () => { case: nonMatchingCases.map(getter), default: () => of('bar'), }; - expect(fn(context, args)).resolves.toBe('bar'); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', ['bar']) + ); }); }); describe('with matching cases', () => { - it('should return the first match', async () => { + it('should return the first match', () => { const context = 'foo'; const args = { case: mockCases.map(getter) }; - const firstMatch = mockCases.find((c) => c.matches); - expect(fn(context, args)).resolves.toBe(firstMatch.result); + const { result } = mockCases.find(({ matches }) => matches); + + testScheduler.run(({ expectObservable }) => + expectObservable(fn(context, args)).toBe('(0|)', [result]) + ); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts index 4258f56ec4cf5f..f4e6c92c91cb60 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts @@ -5,18 +5,23 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { Observable, defer, from, of } from 'rxjs'; +import { concatMap, filter, merge, pluck, take } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Case } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - case: Array<() => Observable>; + case?: Array<() => Observable>; default?(): Observable; } -export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Arguments, unknown> { +export function switchFn(): ExpressionFunctionDefinition< + 'switch', + unknown, + Arguments, + Observable +> { const { help, args: argHelp } = getFunctionHelp().switch; return { @@ -29,7 +34,7 @@ export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Argu resolve: false, multi: true, required: true, - help: argHelp.case, + help: argHelp.case!, }, default: { aliases: ['finally'], @@ -37,18 +42,14 @@ export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Argu help: argHelp.default!, }, }, - fn: async (input, args) => { - const cases = args.case || []; - - for (let i = 0; i < cases.length; i++) { - const { matches, result } = await cases[i]().pipe(take(1)).toPromise(); - - if (matches) { - return result; - } - } - - return args.default?.().pipe(take(1)).toPromise() ?? input; + fn(input, args) { + return from(args.case ?? []).pipe( + concatMap((item) => item()), + filter(({ matches }) => matches), + pluck('result'), + merge(defer(() => args.default?.() ?? of(input))), + take(1) + ); }, }; } From 724ca2d89648c1e7aaab69b45627ed75cd9b6338 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 11 Jun 2021 12:09:24 +0200 Subject: [PATCH 43/99] [Exploratory View] Use human readable formats (#101520) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../configurations/rum/field_formats.ts | 26 +++++++++++-- .../configurations/rum/kpi_trends_config.ts | 2 +- .../rum/performance_dist_config.ts | 2 +- .../synthetics/field_formats.ts | 5 ++- .../synthetics/monitor_duration_config.ts | 2 +- .../synthetics/monitor_pings_config.ts | 2 +- .../shared/exploratory_view/types.ts | 3 +- .../observability_index_patterns.test.ts | 38 +++++++++++++++++-- .../utils/observability_index_patterns.ts | 1 + 9 files changed, 66 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts index f1fc5f310b8ef3..25f258e17307dc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/field_formats.ts @@ -12,6 +12,7 @@ import { LCP_FIELD, TBT_FIELD, TRANSACTION_DURATION, + TRANSACTION_TIME_TO_FIRST_BYTE, } from '../constants/elasticsearch_fieldnames'; export const rumFieldFormats: FieldFormat[] = [ @@ -24,6 +25,7 @@ export const rumFieldFormats: FieldFormat[] = [ outputFormat: 'asSeconds', showSuffix: true, outputPrecision: 1, + useShortSuffix: true, }, }, }, @@ -33,8 +35,9 @@ export const rumFieldFormats: FieldFormat[] = [ id: 'duration', params: { inputFormat: 'milliseconds', - outputFormat: 'asSeconds', + outputFormat: 'humanizePrecise', showSuffix: true, + useShortSuffix: true, }, }, }, @@ -44,8 +47,9 @@ export const rumFieldFormats: FieldFormat[] = [ id: 'duration', params: { inputFormat: 'milliseconds', - outputFormat: 'asSeconds', + outputFormat: 'humanizePrecise', showSuffix: true, + useShortSuffix: true, }, }, }, @@ -55,8 +59,9 @@ export const rumFieldFormats: FieldFormat[] = [ id: 'duration', params: { inputFormat: 'milliseconds', - outputFormat: 'asSeconds', + outputFormat: 'humanizePrecise', showSuffix: true, + useShortSuffix: true, }, }, }, @@ -66,8 +71,21 @@ export const rumFieldFormats: FieldFormat[] = [ id: 'duration', params: { inputFormat: 'milliseconds', - outputFormat: 'asSeconds', + outputFormat: 'humanizePrecise', + showSuffix: true, + useShortSuffix: true, + }, + }, + }, + { + field: TRANSACTION_TIME_TO_FIRST_BYTE, + format: { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', showSuffix: true, + useShortSuffix: true, }, }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts index 5e2d3440526b54..f6c683caaa0391 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts @@ -44,7 +44,7 @@ export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): id: seriesId, defaultSeriesType: 'bar_stacked', reportType: 'kpi-trends', - seriesTypes: ['bar', 'bar_stacked'], + seriesTypes: [], xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts index 0dc582c5683dda..4a1521c834806c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts @@ -44,7 +44,7 @@ export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigP id: seriesId ?? 'unique-key', reportType: 'page-load-dist', defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], + seriesTypes: [], xAxisColumn: { sourceField: 'performance.metric', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts index 8dad1839f0bcd9..5c91e3924cbb7c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts @@ -14,9 +14,10 @@ export const syntheticsFieldFormats: FieldFormat[] = [ id: 'duration', params: { inputFormat: 'microseconds', - outputFormat: 'asMilliseconds', - outputPrecision: 0, + outputFormat: 'humanizePrecise', + outputPrecision: 1, showSuffix: true, + useShortSuffix: true, }, }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts index 698b8f9e951e16..5e8a43ccf2ef46 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts @@ -15,7 +15,7 @@ export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps id: seriesId, reportType: 'uptime-duration', defaultSeriesType: 'line', - seriesTypes: ['line', 'bar_stacked'], + seriesTypes: [], xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts index fc33c37c7bcad1..697a940f666f76 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts @@ -15,7 +15,7 @@ export function getMonitorPingsConfig({ seriesId, indexPattern }: ConfigProps): id: seriesId, reportType: 'uptime-pings', defaultSeriesType: 'bar_stacked', - seriesTypes: ['bar_stacked', 'bar'], + seriesTypes: [], xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index c3dd824e441a8b..87772532f410d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -108,13 +108,14 @@ export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | type FormatType = 'duration' | 'number'; type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; -type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize'; +type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize' | 'humanizePrecise'; export interface FieldFormatParams { inputFormat: InputFormat; outputFormat: OutputFormat; outputPrecision?: number; showSuffix?: boolean; + useShortSuffix?: boolean; } export interface FieldFormat { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts index ade74e7c6744ea..a8c5c1a0a3b79e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts @@ -17,23 +17,53 @@ const fieldFormats = { outputFormat: 'asSeconds', outputPrecision: 1, showSuffix: true, + useShortSuffix: true, }, }, 'transaction.experience.fid': { id: 'duration', - params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + params: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + showSuffix: true, + useShortSuffix: true, + }, }, 'transaction.experience.tbt': { id: 'duration', - params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + params: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + showSuffix: true, + useShortSuffix: true, + }, }, 'transaction.marks.agent.firstContentfulPaint': { id: 'duration', - params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + params: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + showSuffix: true, + useShortSuffix: true, + }, }, 'transaction.marks.agent.largestContentfulPaint': { id: 'duration', - params: { inputFormat: 'milliseconds', outputFormat: 'asSeconds', showSuffix: true }, + params: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + showSuffix: true, + useShortSuffix: true, + }, + }, + 'transaction.marks.agent.timeToFirstByte': { + id: 'duration', + params: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + showSuffix: true, + useShortSuffix: true, + }, }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index c265bad56e8645..858eb52555da60 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -60,6 +60,7 @@ export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldForma const isSame = param1?.inputFormat === param2?.inputFormat && param1?.outputFormat === param2?.outputFormat && + param1?.useShortSuffix === param2?.useShortSuffix && param1?.showSuffix === param2?.showSuffix; if (param2.outputPrecision !== undefined) { From c28b5e922b86c513213b9a259cdf7e346a77db75 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 11 Jun 2021 19:16:34 +0200 Subject: [PATCH 44/99] remove unnecessary hack (#101909) --- .../server/http/lifecycle/on_pre_routing.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/core/server/http/lifecycle/on_pre_routing.ts b/src/core/server/http/lifecycle/on_pre_routing.ts index dbd00df13707b3..be0af8c118627f 100644 --- a/src/core/server/http/lifecycle/on_pre_routing.ts +++ b/src/core/server/http/lifecycle/on_pre_routing.ts @@ -102,24 +102,7 @@ export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) { appState.rewrittenUrl = appState.rewrittenUrl ?? request.url; const { url } = result; - - // TODO: Remove once we upgrade to Node.js 12! - // - // Warning: The following for-loop took 10 days to write, and is a hack - // to force V8 to make a copy of the string in memory. - // - // The reason why we need this is because of what appears to be a bug - // in V8 that caused some URL paths to not be routed correctly once - // `request.setUrl` was called with the path. - // - // The details can be seen in this discussion on Twitter: - // https://twitter.com/wa7son/status/1319992632366518277 - let urlCopy = ''; - for (let i = 0; i < url.length; i++) { - urlCopy += url[i]; - } - - request.setUrl(urlCopy); + request.setUrl(url); // We should update raw request as well since it can be proxied to the old platform request.raw.req.url = url; From 8ffebe8c75fb561935847a1b68f717b69e128a16 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 11 Jun 2021 12:18:24 -0700 Subject: [PATCH 45/99] skip flaky suite (#101449) --- test/functional/apps/discover/_sidebar.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_sidebar.ts b/test/functional/apps/discover/_sidebar.ts index d8701261126c46..8179f4e44e8b81 100644 --- a/test/functional/apps/discover/_sidebar.ts +++ b/test/functional/apps/discover/_sidebar.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const testSubjects = getService('testSubjects'); - describe('discover sidebar', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/101449 + describe.skip('discover sidebar', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); From f9fbfc508b8a6bdedc6294b960bdb38c333ec5a8 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 11 Jun 2021 20:44:20 -0700 Subject: [PATCH 46/99] [esArchive] Persists updates for management/saved_objects/* (#101992) Same as #101950 - these archives are causing issues with a non-oss build https://github.com/elastic/kibana/issues/101654 Last remaining fix for https://github.com/elastic/kibana/pull/101118 Signed-off-by: Tyler Smalley --- .../saved_objects/scroll_count/data.json | 239 +++++++++++ .../saved_objects/scroll_count/data.json.gz | Bin 1295 -> 0 bytes .../saved_objects/scroll_count/mappings.json | 378 ++++++++++++++++-- .../management/saved_objects/search/data.json | 227 +++++++++++ .../saved_objects/search/data.json.gz | Bin 1385 -> 0 bytes .../saved_objects/search/mappings.json | 346 +++++++++++++--- 6 files changed, 1093 insertions(+), 97 deletions(-) create mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json delete mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json.gz create mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json delete mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json.gz diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json new file mode 100644 index 00000000000000..349545be443162 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json @@ -0,0 +1,239 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "saved_objects*" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:7.0.0-alpha1", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "8963ca30-3224-11e8-a572-ffca06da1357", + "telemetry:optIn": false + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-03-28T01:08:39.248Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:960372e0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "8963ca30-3224-11e8-a572-ffca06da1357", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "_source" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "_score", + "desc" + ] + ], + "title": "OneRecord", + "version": 1 + }, + "type": "search", + "updated_at": "2018-03-28T01:08:55.182Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "960372e0-3224-11e8-a572-ffca06da1357", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-03-28T01:09:18.936Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + }, + "savedSearchRefName": "search_0", + "title": "VisualizationFromSavedSearch", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:add810b0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "8963ca30-3224-11e8-a572-ffca06da1357", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-03-28T01:09:35.163Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Amazing Visualization", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Visualization\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Dashboard", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "add810b0-3224-11e8-a572-ffca06da1357", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "a42c0580-3224-11e8-a572-ffca06da1357", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:d70c7ae0-3224-11e8-a572-ffca82da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Amazing Dashboard", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "add810b0-3224-11e8-a572-ffca06da1357", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "a42c0580-3224-11e8-a572-ffca06da1357", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z" + }, + "type": "_doc" + } +} \ No newline at end of file diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json.gz deleted file mode 100644 index 1c327e7e0769b5981367d5bc6e651dbdfa9f0658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1295 zcmV+q1@QVGiwFq+nP^@B17u-zVJ>QOZ*BnXm|IWdI23^2`4v_6ZAGgjy`_0WgIKW| zE}|6=OVP-Q-PAj7+_jw!rOJQbV<&AK=rZXd7FGfYCONi`kI&`v$!v~VEk~`F#0gst z=CO0E1uMidE!n^eywVht2R)(lokl1{W|Ed;Ei|L;B~prrGz>ynYp5(h;(003RKbW{)=muf8 z*YQX0AJzQ2nra`t6IIM?CD95r^5{QX@vJ`;R# z%Hvxd47t^kAJ=WsC;T|($e0UKHz&;&5fKNQvlTwEXDe=OkV&z+MYixAOe*a7AQ5|- z%l|T32b-dY@_VN&w)|-OH$TdcjyA7{Qq0pCO~PTv?YM2kmUHC&oI1s#=C~Pk{9a(; zrd(g+Oq5ZEFS7+mgWh0h;YATf7DS14$d~F|snVEtEm6!_wn(+~ zZYKnBi4JEcySLUzF4Xe{nuykBr6v0{tcZ|>eV!#Pf-%M3-M73^My-K;mhe@Q_d{Ux1o5E5s}{ zO240FX3_t8Vq^nC?wUV!d2Stj$2-D=5m{p1u{a-V;VnkLY_ma zTE5PxakW)t;Qi8Dmgf#VF#I&+m8?Q|Zj$@}CchD9tg`+mHoU-v_aFhrf#*i=LV_;b zW`pj5Bsf{1N9Yn;w|lp2jDUL0@6C6pCX6SXGewtHZ-?<$l91Hy0r%wkxXWxZ# z@I(1c57nTzec@2|b_}_Rth6c=Bk_F#czHtc3nUVn$_h(!ipjM;z(l81^}h~3=ZB&T z_-vC(e=j}%7Jdy~4u!)rx-|JD`j3f%Um!~0e;}f4`<73#e3IppEN?=VzX36INOHv) F001+VcESJv diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json index d2177481130a2c..f44bb7463e9ebc 100644 --- a/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json @@ -2,140 +2,331 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, "dynamic": "strict", "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, "index-pattern": { + "dynamic": "false", "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { + "title": { "type": "text" }, - "intervalName": { + "type": { "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "notExpandable": { + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { "type": "boolean" }, - "sourceFilters": { - "type": "text" + "sourceId": { + "type": "keyword" }, - "timeFieldName": { + "targetType": { "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" }, "title": { "type": "text" } } }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, "search": { "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -146,16 +337,100 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { + "dynamic": "false", + "type": "object" + }, + "telemetry": { "properties": { - "uuid": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" } } }, "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, "updated_at": { "type": "date" }, @@ -171,13 +446,21 @@ "type": "date" }, "url": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" } - } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" } } }, @@ -189,28 +472,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } } } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } } } -} +} \ No newline at end of file diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json new file mode 100644 index 00000000000000..6402a255afd376 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json @@ -0,0 +1,227 @@ +{ + "type": "doc", + "value": { + "id": "timelion-sheet:190f3e90-2ec3-11e8-ba48-69fc4e41e1f6", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "timelion-sheet": { + "description": "", + "hits": 0, + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_interval": "auto", + "timelion_rows": 2, + "timelion_sheet": [ + ".es(*)" + ], + "title": "New TimeLion Sheet", + "version": 1 + }, + "type": "timelion-sheet", + "updated_at": "2018-03-23T17:53:30.872Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "index-pattern": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "saved_objects*" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [ + ], + "type": "index-pattern", + "updated_at": "2018-03-28T01:08:34.290Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:7.0.0-alpha1", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "8963ca30-3224-11e8-a572-ffca06da1357", + "telemetry:optIn": false + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-03-28T01:08:39.248Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "search:960372e0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "8963ca30-3224-11e8-a572-ffca06da1357", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "_source" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "_score", + "desc" + ] + ], + "title": "OneRecord", + "version": 1 + }, + "type": "search", + "updated_at": "2018-03-28T01:08:55.182Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "960372e0-3224-11e8-a572-ffca06da1357", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2018-03-28T01:09:18.936Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" + }, + "savedSearchRefName": "search_0", + "title": "VisualizationFromSavedSearch", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:add810b0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "8963ca30-3224-11e8-a572-ffca06da1357", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2018-03-28T01:09:35.163Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Visualization", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Visualization\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.0.0-alpha1\",\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "Dashboard", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "add810b0-3224-11e8-a572-ffca06da1357", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "a42c0580-3224-11e8-a572-ffca06da1357", + "name": "panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2018-03-28T01:09:50.606Z" + }, + "type": "_doc" + } +} \ No newline at end of file diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json.gz deleted file mode 100644 index 0834567abb66b663079894089ed4edd91f1cf0b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1385 zcmV-v1(y0BiwFP!000026V+JVZ`(Eyf6rfGXfIn48~X5vthb^?hYW2R#6}+$2LUCX zWv;U5QB=~@(Eq+8C0mrECGvtGnI97Qcs$m8BGZD22gy7Lt@`B_*dyDA^hk#?yYb0+4|-wU-`D?Y;|<*LNK7`ym-mNf3G{|YrRCa=-?zQK>&=}>F!BP=9{3aY&szV$ zPJNPIlZig;9PWB^RQ!yJy;cWFiU@hL0v4~-Deh#{s>PFhohtX;wq?QZ4%co$WMx=R zB`i*Me~Xji3UJ=YzUfFYxa+g~mtVvi|tywT)oz%*<= zi5GuvJAv&7-f-YfZ38b&GwpE6$Sqpr;a?ER?46mNC4+>j8?~;s3o9jSSXjZrx?yx- zoi4PmT98S>(pbwPo~IIpHa?f20#pu`B*{RDfCx-=n5d0XmXn12Br5R%8M=`@ z@}CLbhRu!`o(7ITn0jLa!%Z{oQ2u7>C=%5$m^G`Xv^A4>dX;v)U*G*>2Ab4gu{Me} zM38k>=5_<(qRgYC8^Ma-UEr+BNOFnerr8g01%b(;?7c$HXSju=v5wVInk;MUtU_j* zCkZZ7CJ@;r!j!0}OwPF^iD5>n@1OECDm!PsE@6e8M;)dHHPzB^$?- z?M*kg6|9LCDn4e>!6g(0Le;qIoaw7Jstj+xx-H}8jtv+;9r-G&Q+TF9egr4K5YHH8 z{cqgx2rs+_6Hw|qcK9kx;9)l#d(UBl<4eC;>#aD)Dx!4Gc_P`ynCU3}3^AnU?AK;z z_gIkzW>#XJzi?{K$aw~rhyXB&0jqKX zJa<^$+w06QBYQBm%~_*1(atU(9~^P?Z)F>jVs-73tAHE}MnB@)V3{9PZthSGn5rm8 z`0%4D)BEZ_+u^=w44ezge2uHHjc1+h!Q(X9?e+ojRVCGh^vkNlw_r+D<$ciabY&Ht zc8p0&nnAh82jzARs>4kCNKn^i4!O>3W>hF8;`(&bg?DMtAR@76`}lotR1KQp@| diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json index 7a6d752eafb08e..7699a72ff7120b 100644 --- a/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json @@ -2,169 +2,335 @@ "type": "index", "value": { "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, ".kibana": {} }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, "dynamic": "strict", "properties": { + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" } } }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { "type": "text" }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "optionsJSON": { + "index": false, "type": "text" }, "panelsJSON": { + "index": false, "type": "text" }, "refreshInterval": { "properties": { "display": { + "doc_values": false, + "index": false, "type": "keyword" }, "pause": { + "doc_values": false, + "index": false, "type": "boolean" }, "section": { + "doc_values": false, + "index": false, "type": "integer" }, "value": { + "doc_values": false, + "index": false, "type": "integer" } } }, "timeFrom": { + "doc_values": false, + "index": false, "type": "keyword" }, "timeRestore": { + "doc_values": false, + "index": false, "type": "boolean" }, "timeTo": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, - "uiStateJSON": { - "type": "text" - }, "version": { "type": "integer" } } }, "graph-workspace": { + "dynamic": "false", + "type": "object" + }, + "index-pattern": { + "dynamic": "false", "properties": { - "description": { + "title": { "type": "text" }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" }, - "numLinks": { - "type": "integer" + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" }, - "numVertices": { - "type": "integer" + "sourceId": { + "type": "keyword" }, - "title": { + "targetType": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, "type": "text" }, - "version": { - "type": "integer" + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" }, - "wsState": { + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, "type": "text" } } }, - "index-pattern": { + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { "properties": { - "fieldFormatMap": { + "description": { "type": "text" }, - "fields": { - "type": "text" + "filters": { + "enabled": false, + "type": "object" }, - "intervalName": { - "type": "keyword" + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } }, - "notExpandable": { - "type": "boolean" + "timefilter": { + "enabled": false, + "type": "object" }, - "sourceFilters": { + "title": { "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" }, - "timeFieldName": { + "name": { "type": "keyword" }, - "title": { - "type": "text" + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" } } }, "search": { "properties": { "columns": { + "doc_values": false, + "index": false, "type": "keyword" }, "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { + "doc_values": false, + "index": false, "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, "sort": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { @@ -175,10 +341,39 @@ } } }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, "server": { + "dynamic": "false", + "type": "object" + }, + "telemetry": { "properties": { - "uuid": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" } } }, @@ -226,6 +421,20 @@ "type": { "type": "keyword" }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, "updated_at": { "type": "date" }, @@ -241,13 +450,21 @@ "type": "date" }, "url": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" } - } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" } } }, @@ -259,28 +476,43 @@ "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { + "index": false, "type": "text" } } }, - "savedSearchId": { + "savedSearchRefName": { + "doc_values": false, + "index": false, "type": "keyword" }, "title": { "type": "text" }, "uiStateJSON": { + "index": false, "type": "text" }, "version": { "type": "integer" }, "visState": { + "index": false, "type": "text" } } } } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } } } } \ No newline at end of file From 344b6310d74a6896048d85b2bb13e9747ce8e67b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Sat, 12 Jun 2021 06:49:27 -0700 Subject: [PATCH 47/99] skip flaky suite (#102012) --- x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts index 0f5e12f226c0ed..96e6bd4b302bd0 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts @@ -67,7 +67,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { let testJobId = ''; - describe('anomaly detection alert', function () { + // Failing: See https://github.com/elastic/kibana/issues/102012 + describe.skip('anomaly detection alert', function () { this.tags('ciGroup13'); before(async () => { From 012bb4490b5598790bc88fd112ab1c3d006a9a61 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 12 Jun 2021 16:08:21 +0100 Subject: [PATCH 48/99] skip flaky suite (#94043) --- .../vis_type_tagcloud/public/components/tag_cloud.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js index 2fb2be0ace7cdc..eb575457146c5d 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js @@ -150,7 +150,8 @@ describe('tag cloud tests', () => { }); [5, 100, 200, 300, 500].forEach((timeout) => { - describe(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { + // FLAKY: https://github.com/elastic/kibana/issues/94043 + describe.skip(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { beforeEach(async () => { //TagCloud takes at least 600ms to complete (due to d3 animation) //renderComplete should only notify at the last one From 80b109f95c344c8e7f8d484242f4be2feb32ba3c Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 14 Jun 2021 09:33:58 +0100 Subject: [PATCH 49/99] [Discover] Deangularization of search embeddable (#100552) * [Discover] Render empty embeddable * First version of grid embeddable * More search embeddable * Almost stable version * Fixing typescript errors * Fixing filtering and sorting * Add data-shared-item to DiscoverGridEmbeddable * Trigger rerender when title changes * Fixing incorrectly touched files * Remove search_embeddable * Remove lodash * Fixing imports * Minor fixes * Removing unnecessary files * Minor fix * Remove unused import * Applying PR comments * Applying PR comments * Removing search embeddable * Fix missing import * Addressing PR comments * Do not memoize saved search component * Applying Matthias's suggestion Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- api_docs/deprecations.mdx | 103 ++-- .../create_discover_grid_directive.tsx | 1 + .../doc_table/create_doc_table_embeddable.tsx | 85 ++++ .../application/angular/doc_table/index.ts | 1 + .../discover_grid/discover_grid.tsx | 7 + ...eddable.ts => saved_search_embeddable.tsx} | 451 +++++++++--------- .../saved_search_embeddable_component.tsx | 39 ++ .../embeddable/search_embeddable_factory.ts | 18 +- .../embeddable/search_template.html | 21 - .../embeddable/search_template_datagrid.html | 19 - 10 files changed, 418 insertions(+), 327 deletions(-) create mode 100644 src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx rename src/plugins/discover/public/application/embeddable/{search_embeddable.ts => saved_search_embeddable.tsx} (54%) create mode 100644 src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx delete mode 100644 src/plugins/discover/public/application/embeddable/search_template.html delete mode 100644 src/plugins/discover/public/application/embeddable/search_template_datagrid.html diff --git a/api_docs/deprecations.mdx b/api_docs/deprecations.mdx index d9261b943d1708..74dae7faf838a4 100644 --- a/api_docs/deprecations.mdx +++ b/api_docs/deprecations.mdx @@ -111,6 +111,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [clone_panel_action.tsx#L14](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#L14) | - | | | [clone_panel_action.tsx#L98](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#L98) | - | | | [clone_panel_action.tsx#L126](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#L126) | - | +| | [use_dashboard_state_manager.ts#L23](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts#L23) | - | +| | [use_dashboard_state_manager.ts#L35](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts#L35) | - | @@ -127,8 +129,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [search_embeddable.ts#L23](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L23) | - | | | [search_embeddable.ts#L59](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L59) | - | | | [kibana_services.ts#L104](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/kibana_services.ts#L104) | - | -| | [search_embeddable.ts#L23](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L23) | - | -| | [search_embeddable.ts#L59](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L59) | - | | | [kibana_services.ts#L101](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/kibana_services.ts#L101) | - | | | [create_doc_table_react.tsx#L15](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx#L15) | - | | | [create_doc_table_react.tsx#L25](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx#L25) | - | @@ -153,7 +153,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| | | [attribute_service.tsx#L13](https://github.com/elastic/kibana/tree/master/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#L13) | - | -| | [attribute_service.tsx#L165](https://github.com/elastic/kibana/tree/master/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#L165) | - | +| | [attribute_service.tsx#L167](https://github.com/elastic/kibana/tree/master/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#L167) | - | @@ -189,7 +189,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [query_bar.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx#L30) | - | | | [query_bar.tsx#L38](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx#L38) | - | | | [plugin.ts#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/server/plugin.ts#L14) | - | -| | [plugin.ts#L190](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/server/plugin.ts#L190) | - | +| | [plugin.ts#L189](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/server/plugin.ts#L189) | - | | | [plugin.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/target/types/server/plugin.d.ts#L2) | - | | | [plugin.d.ts#L84](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/target/types/server/plugin.d.ts#L84) | - | @@ -228,18 +228,18 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| -| | [plugin.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L12) | 7.16 | -| | [plugin.ts#L38](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L38) | 7.16 | +| | [plugin.ts#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L14) | 7.16 | +| | [plugin.ts#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L42) | 7.16 | | | [types.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L9) | 7.16 | -| | [types.ts#L39](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L39) | 7.16 | +| | [types.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L40) | 7.16 | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L1) | 7.16 | -| | [types.d.ts#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L24) | 7.16 | +| | [types.d.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L25) | 7.16 | | | [types.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L10) | 7.16 | -| | [types.ts#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L42) | 7.16 | -| | [types.ts#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L49) | 7.16 | +| | [types.ts#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L43) | 7.16 | +| | [types.ts#L50](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L50) | 7.16 | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L1) | 7.16 | -| | [types.d.ts#L26](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L26) | 7.16 | -| | [types.d.ts#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L32) | 7.16 | +| | [types.d.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L27) | 7.16 | +| | [types.d.ts#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L33) | 7.16 | @@ -578,6 +578,11 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [custom_metric_form.d.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.d.ts#L8) | - | | | [index.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/public/pages/metrics/inventory_view/components/waffle/metric_control/index.d.ts#L1) | - | | | [index.d.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/public/pages/metrics/inventory_view/components/waffle/metric_control/index.d.ts#L9) | - | +| | [log_entry_categories_analysis.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts#L9) | 7.16 | +| | [log_entry_categories_analysis.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts#L139) | 7.16 | +| | [log_entry_categories_analysis.ts#L405](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts#L405) | 7.16 | +| | [log_entry_categories_analysis.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/server/lib/log_analysis/log_entry_categories_analysis.d.ts#L1) | 7.16 | +| | [log_entry_categories_analysis.d.ts#L58](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/server/lib/log_analysis/log_entry_categories_analysis.d.ts#L58) | 7.16 | @@ -587,8 +592,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | ---------------|-----------|-----------| | | [embeddable.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L14) | - | | | [embeddable.tsx#L85](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L85) | - | -| | [index.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L25) | - | -| | [index.tsx#L102](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L102) | - | | | [field_item.tsx#L47](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L47) | - | | | [field_item.tsx#L172](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L172) | - | | | [datapanel.tsx#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx#L42) | - | @@ -603,10 +606,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L8) | - | | | [types.ts#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L57) | - | | | [field_stats.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L11) | - | -| | [field_stats.ts#L141](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L141) | - | -| | [field_stats.ts#L250](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L250) | - | -| | [field_stats.ts#L290](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L290) | - | -| | [field_stats.ts#L332](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L332) | - | +| | [field_stats.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L139) | - | +| | [field_stats.ts#L248](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L248) | - | +| | [field_stats.ts#L287](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L287) | - | +| | [field_stats.ts#L329](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L329) | - | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L1) | - | | | [types.d.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L22) | - | | | [field_stats.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L3) | - | @@ -618,10 +621,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L8) | - | | | [types.ts#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L57) | - | | | [field_stats.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L11) | - | -| | [field_stats.ts#L141](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L141) | - | -| | [field_stats.ts#L250](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L250) | - | -| | [field_stats.ts#L290](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L290) | - | -| | [field_stats.ts#L332](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L332) | - | +| | [field_stats.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L139) | - | +| | [field_stats.ts#L248](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L248) | - | +| | [field_stats.ts#L287](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L287) | - | +| | [field_stats.ts#L329](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L329) | - | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L1) | - | | | [types.d.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L22) | - | | | [field_stats.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L3) | - | @@ -630,8 +633,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [field_stats.d.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L9) | - | | | [embeddable.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L14) | - | | | [embeddable.tsx#L85](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L85) | - | -| | [index.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L25) | - | -| | [index.tsx#L102](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L102) | - | | | [field_item.tsx#L47](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L47) | - | | | [field_item.tsx#L172](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L172) | - | | | [datapanel.tsx#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx#L42) | - | @@ -646,10 +647,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L8) | - | | | [types.ts#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L57) | - | | | [field_stats.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L11) | - | -| | [field_stats.ts#L141](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L141) | - | -| | [field_stats.ts#L250](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L250) | - | -| | [field_stats.ts#L290](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L290) | - | -| | [field_stats.ts#L332](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L332) | - | +| | [field_stats.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L139) | - | +| | [field_stats.ts#L248](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L248) | - | +| | [field_stats.ts#L287](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L287) | - | +| | [field_stats.ts#L329](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L329) | - | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L1) | - | | | [types.d.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L22) | - | | | [field_stats.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L3) | - | @@ -935,7 +936,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L8) | - | -| | [types.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L44) | - | +| | [types.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L45) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | | | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | @@ -1125,7 +1126,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [get_docvalue_source_fields.test.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L10) | - | | | [get_docvalue_source_fields.test.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L12) | - | | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L8) | - | -| | [types.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L44) | - | +| | [types.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L45) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | | | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | @@ -1360,7 +1361,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [static_globals.ts#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/static_globals.ts#L43) | 7.16 | | | [plugin.ts#L18](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L18) | 7.16 | | | [plugin.ts#L74](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L74) | 7.16 | -| | [plugin.ts#L284](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L284) | 7.16 | +| | [plugin.ts#L279](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L279) | 7.16 | | | [types.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/target/types/server/types.d.ts#L2) | 7.16 | | | [types.d.ts#L47](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/target/types/server/types.d.ts#L47) | 7.16 | | | [plugin.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/target/types/server/plugin.d.ts#L1) | 7.16 | @@ -1449,22 +1450,22 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| -| | [types.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L19) | - | -| | [types.ts#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L104) | - | +| | [types.ts#L18](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L18) | - | +| | [types.ts#L95](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L95) | - | | | [utils.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L10) | - | | | [utils.ts#L53](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L53) | - | | | [utils.ts#L61](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L61) | - | | | [utils.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L69) | - | | | [default_configs.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L19) | - | -| | [default_configs.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L25) | - | -| | [types.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L19) | - | -| | [types.ts#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L104) | - | +| | [default_configs.ts#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L24) | - | +| | [types.ts#L18](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L18) | - | +| | [types.ts#L95](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L95) | - | | | [utils.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L10) | - | | | [utils.ts#L53](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L53) | - | | | [utils.ts#L61](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L61) | - | | | [utils.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L69) | - | | | [default_configs.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L19) | - | -| | [default_configs.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L25) | - | +| | [default_configs.ts#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L24) | - | @@ -1540,9 +1541,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| -| | [types.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L22) | - | -| | [types.ts#L72](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L72) | - | -| | [action.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L20) | - | +| | [types.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L21) | - | +| | [types.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L66) | - | +| | [action.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L19) | - | | | [action.ts#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L100) | - | | | [index.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L8) | - | | | [index.ts#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L86) | - | @@ -1579,9 +1580,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L48](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L48) | - | -| | [middleware.ts#L64](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L64) | - | -| | [middleware.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L69) | - | +| | [middleware.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L44) | - | +| | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | +| | [middleware.ts#L65](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L65) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1636,8 +1637,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L31) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L30) | - | -| | [query.ts#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L13) | - | -| | [query.ts#L52](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L52) | - | | | [model.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L8) | - | | | [model.ts#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L30) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L33) | - | @@ -1754,9 +1753,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.tsx#L242](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx#L242) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx#L12) | - | | | [index.tsx#L35](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx#L35) | - | -| | [types.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L22) | - | -| | [types.ts#L72](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L72) | - | -| | [action.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L20) | - | +| | [types.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L21) | - | +| | [types.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L66) | - | +| | [action.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L19) | - | | | [action.ts#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L100) | - | | | [index.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L8) | - | | | [index.ts#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L86) | - | @@ -1793,9 +1792,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L48](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L48) | - | -| | [middleware.ts#L64](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L64) | - | -| | [middleware.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L69) | - | +| | [middleware.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L44) | - | +| | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | +| | [middleware.ts#L65](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L65) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1850,8 +1849,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L31) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L30) | - | -| | [query.ts#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L13) | - | -| | [query.ts#L52](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L52) | - | | | [model.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L8) | - | | | [model.ts#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L30) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L33) | - | diff --git a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx index 1fc8edcb4d0659..810be94ce24b06 100644 --- a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx @@ -51,5 +51,6 @@ export function createDiscoverGridDirective(reactDirective: any) { ['settings', { watchDepth: 'reference' }], ['showTimeCol', { watchDepth: 'value' }], ['sort', { watchDepth: 'value' }], + ['className', { watchDepth: 'value' }], ]); } diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx new file mode 100644 index 00000000000000..19913ed6de8704 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useRef, useEffect } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { IScope } from 'angular'; +import { getServices } from '../../../kibana_services'; +import { DocTableLegacyProps, injectAngularElement } from './create_doc_table_react'; + +type AngularEmbeddableScope = IScope & { renderProps?: DocTableEmbeddableProps }; + +export interface DocTableEmbeddableProps extends Partial { + refs: HTMLElement; +} + +function getRenderFn(domNode: Element, props: DocTableEmbeddableProps) { + const directive = { + template: ``, + }; + + return async () => { + try { + const injector = await getServices().getEmbeddableInjector(); + return await injectAngularElement(domNode, directive.template, props, injector); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } + }; +} + +export function DiscoverDocTableEmbeddable(props: DocTableEmbeddableProps) { + return ( + + + + ); +} + +function DocTableLegacyInner(renderProps: DocTableEmbeddableProps) { + const scope = useRef(); + + useEffect(() => { + if (renderProps.refs && !scope.current) { + const fn = getRenderFn(renderProps.refs, renderProps); + fn().then((newScope) => { + scope.current = newScope; + }); + } else if (scope?.current) { + scope.current.renderProps = { ...renderProps }; + scope.current.$applyAsync(); + } + }, [renderProps]); + + useEffect(() => { + return () => { + scope.current?.$destroy(); + }; + }, []); + return ; +} diff --git a/src/plugins/discover/public/application/angular/doc_table/index.ts b/src/plugins/discover/public/application/angular/doc_table/index.ts index 2aaf5a8bda7b69..3a8f170f8680d5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.ts +++ b/src/plugins/discover/public/application/angular/doc_table/index.ts @@ -9,3 +9,4 @@ export { createDocTableDirective } from './doc_table'; export { getSort, getSortArray } from './lib/get_sort'; export { getSortForSearchSource } from './lib/get_sort_for_search_source'; +export { getDefaultSort } from './lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 65a6ee80564e9f..f1c56b7a57195b 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -51,6 +51,10 @@ export interface DiscoverGridProps { * Determines which element labels the grid for ARIA */ ariaLabelledBy: string; + /** + * Optional class name to apply + */ + className?: string; /** * Determines which columns are displayed */ @@ -175,6 +179,7 @@ export const DiscoverGrid = ({ isSortEnabled = true, isPaginationEnabled = true, controlColumnIds = ['openDetails', 'select'], + className, }: DiscoverGridProps) => { const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -284,6 +289,7 @@ export const DiscoverGrid = ({ ), [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled] ); + const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const columnsVisibility = useMemo( () => ({ @@ -368,6 +374,7 @@ export const DiscoverGrid = ({ data-title={searchTitle} data-description={searchDescription} data-document-number={displayedRows.length} + className={className} > { settings?: DiscoverGridSettings; description?: string; - sort?: SortOrder[]; sharedItemTitle?: string; inspectorAdapters?: Adapters; - setSortOrder?: (sortPair: SortOrder[]) => void; - setColumns?: (columns: string[]) => void; - removeColumn?: (column: string) => void; - addColumn?: (column: string) => void; - moveColumn?: (column: string, index: number) => void; + filter?: (field: IFieldType, value: string[], operator: string) => void; hits?: ElasticSearchHit[]; - indexPattern?: IndexPattern; totalHitCount?: number; - isLoading?: boolean; - showTimeCol?: boolean; - useNewFieldsApi?: boolean; + onMoveColumn?: (column: string, index: number) => void; } interface SearchEmbeddableConfig { - $rootScope: ng.IRootScopeService; - $compile: ng.ICompileService; savedSearch: SavedSearch; editUrl: string; editPath: string; @@ -77,17 +66,13 @@ interface SearchEmbeddableConfig { services: DiscoverServices; } -export class SearchEmbeddable +export class SavedSearchEmbeddable extends Embeddable implements ISearchEmbeddable { private readonly savedSearch: SavedSearch; - private $rootScope: ng.IRootScopeService; - private $compile: ng.ICompileService; private inspectorAdapters: Adapters; - private searchScope?: SearchScope; private panelTitle: string = ''; - private filtersSearchSource?: ISearchSource; - private searchInstance?: JQLite; + private filtersSearchSource!: ISearchSource; private subscription?: Subscription; public readonly type = SEARCH_EMBEDDABLE_TYPE; private filterManager: FilterManager; @@ -98,11 +83,12 @@ export class SearchEmbeddable private prevFilters?: Filter[]; private prevQuery?: Query; private prevSearchSessionId?: string; + private searchProps?: SearchProps; + + private node?: HTMLElement; constructor( { - $rootScope, - $compile, savedSearch, editUrl, editPath, @@ -130,164 +116,24 @@ export class SearchEmbeddable this.services = services; this.filterManager = filterManager; this.savedSearch = savedSearch; - this.$rootScope = $rootScope; - this.$compile = $compile; this.inspectorAdapters = { requests: new RequestAdapter(), }; - this.initializeSearchScope(); + this.initializeSearchEmbeddableProps(); this.subscription = this.getUpdated$().subscribe(() => { this.panelTitle = this.output.title || ''; - if (this.searchScope) { - this.pushContainerStateParamsToScope(this.searchScope); + if (this.searchProps) { + this.pushContainerStateParamsToProps(this.searchProps); } }); } - public getInspectorAdapters() { - return this.inspectorAdapters; - } - - public getSavedSearch() { - return this.savedSearch; - } - - /** - * - * @param {Element} domNode - */ - public render(domNode: HTMLElement) { - if (!this.searchScope) { - throw new Error('Search scope not defined'); - } - this.searchInstance = this.$compile( - this.services.uiSettings.get('doc_table:legacy') ? searchTemplate : searchTemplateGrid - )(this.searchScope); - const rootNode = angular.element(domNode); - rootNode.append(this.searchInstance); - - this.pushContainerStateParamsToScope(this.searchScope); - } - - public destroy() { - super.destroy(); - this.savedSearch.destroy(); - if (this.searchInstance) { - this.searchInstance.remove(); - } - if (this.searchScope) { - this.searchScope.$destroy(); - delete this.searchScope; - } - if (this.subscription) { - this.subscription.unsubscribe(); - } - - if (this.abortController) this.abortController.abort(); - } - - private initializeSearchScope() { - const searchScope: SearchScope = (this.searchScope = this.$rootScope.$new()); - - searchScope.description = this.savedSearch.description; - searchScope.inspectorAdapters = this.inspectorAdapters; - - const { searchSource } = this.savedSearch; - const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; - - if (!this.savedSearch.sort || !this.savedSearch.sort.length) { - this.savedSearch.sort = getDefaultSort( - indexPattern, - getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') - ); - } - - const timeRangeSearchSource = searchSource.create(); - timeRangeSearchSource.setField('filter', () => { - if (!this.searchScope || !this.input.timeRange) return; - return this.services.timefilter.createFilter(indexPattern, this.input.timeRange); - }); - - this.filtersSearchSource = searchSource.create(); - this.filtersSearchSource.setParent(timeRangeSearchSource); - - searchSource.setParent(this.filtersSearchSource); - - this.pushContainerStateParamsToScope(searchScope); - - searchScope.setSortOrder = (sort) => { - this.updateInput({ sort }); - }; - - searchScope.isLoading = true; - - const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); - searchScope.useNewFieldsApi = useNewFieldsApi; - - searchScope.addColumn = (columnName: string) => { - if (!searchScope.columns) { - return; - } - const columns = columnActions.addColumn(searchScope.columns, columnName, useNewFieldsApi); - this.updateInput({ columns }); - }; - - searchScope.removeColumn = (columnName: string) => { - if (!searchScope.columns) { - return; - } - const columns = columnActions.removeColumn(searchScope.columns, columnName, useNewFieldsApi); - this.updateInput({ columns }); - }; - - searchScope.moveColumn = (columnName, newIndex: number) => { - if (!searchScope.columns) { - return; - } - const columns = columnActions.moveColumn(searchScope.columns, columnName, newIndex); - this.updateInput({ columns }); - }; - - searchScope.setColumns = (columns: string[]) => { - this.updateInput({ columns }); - }; - - if (this.savedSearch.grid) { - searchScope.settings = this.savedSearch.grid; - } - searchScope.showTimeCol = !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); - - searchScope.filter = async (field, value, operator) => { - let filters = esFilters.generateFilters( - this.filterManager, - field, - value, - operator, - indexPattern.id! - ); - filters = filters.map((filter) => ({ - ...filter, - $state: { store: esFilters.FilterStateStore.APP_STATE }, - })); - - await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters, - }); - }; - } - - public reload() { - if (this.searchScope) - this.pushContainerStateParamsToScope(this.searchScope, { forceFetch: true }); - } - private fetch = async () => { const searchSessionId = this.input.searchSessionId; const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); - if (!this.searchScope) return; + if (!this.searchProps) return; const { searchSource } = this.savedSearch; @@ -299,8 +145,8 @@ export class SearchEmbeddable searchSource.setField( 'sort', getSortForSearchSource( - this.searchScope.sort, - this.searchScope.indexPattern, + this.searchProps!.sort, + this.searchProps!.indexPattern, this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ) ); @@ -310,8 +156,8 @@ export class SearchEmbeddable searchSource.setField('fields', [fields]); } else { searchSource.removeField('fields'); - if (this.searchScope.indexPattern) { - const fieldNames = this.searchScope.indexPattern.fields.map((field) => field.name); + if (this.searchProps.indexPattern) { + const fieldNames = this.searchProps.indexPattern.fields.map((field) => field.name); searchSource.setField('fieldsFromSource', fieldNames); } } @@ -319,9 +165,8 @@ export class SearchEmbeddable // Log request to inspector this.inspectorAdapters.requests!.reset(); - this.searchScope.$apply(() => { - this.searchScope!.isLoading = true; - }); + this.searchProps!.isLoading = true; + this.updateOutput({ loading: true, error: undefined }); try { @@ -344,64 +189,222 @@ export class SearchEmbeddable .toPromise(); this.updateOutput({ loading: false, error: undefined }); - // Apply the changes to the angular scope - this.searchScope.$apply(() => { - this.searchScope!.hits = resp.hits.hits; - this.searchScope!.totalHitCount = resp.hits.total as number; - this.searchScope!.isLoading = false; - }); + this.searchProps!.rows = resp.hits.hits; + this.searchProps!.totalHitCount = resp.hits.total as number; + this.searchProps!.isLoading = false; } catch (error) { this.updateOutput({ loading: false, error }); - this.searchScope.$apply(() => { - this.searchScope!.isLoading = false; - }); + + this.searchProps!.isLoading = false; } }; - private pushContainerStateParamsToScope( - searchScope: SearchScope, + private initializeSearchEmbeddableProps() { + const { searchSource } = this.savedSearch; + + const indexPattern = searchSource.getField('index'); + + if (!indexPattern) { + return; + } + + if (!this.savedSearch.sort || !this.savedSearch.sort.length) { + this.savedSearch.sort = getDefaultSort( + indexPattern, + getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') + ); + } + + const props: SearchProps = { + columns: this.savedSearch.columns, + indexPattern, + isLoading: false, + sort: getDefaultSort( + indexPattern, + getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') + ), + rows: [], + searchDescription: this.savedSearch.description, + description: this.savedSearch.description, + inspectorAdapters: this.inspectorAdapters, + searchTitle: this.savedSearch.lastSavedTitle, + services: this.services, + onAddColumn: (columnName: string) => { + if (!props.columns) { + return; + } + const updatedColumns = columnActions.addColumn(props.columns, columnName, true); + this.updateInput({ columns: updatedColumns }); + }, + onRemoveColumn: (columnName: string) => { + if (!props.columns) { + return; + } + const updatedColumns = columnActions.removeColumn(props.columns, columnName, true); + this.updateInput({ columns: updatedColumns }); + }, + onMoveColumn: (columnName: string, newIndex: number) => { + if (!props.columns) { + return; + } + const columns = columnActions.moveColumn(props.columns, columnName, newIndex); + this.updateInput({ columns }); + }, + onSetColumns: (columns: string[]) => { + this.updateInput({ columns }); + }, + onSort: (sort: string[][]) => { + const sortOrderArr: SortOrder[] = []; + sort.forEach((arr) => { + sortOrderArr.push(arr as SortOrder); + }); + this.updateInput({ sort: sortOrderArr }); + }, + sampleSize: 500, + onFilter: async (field, value, operator) => { + let filters = esFilters.generateFilters( + this.filterManager, + // @ts-expect-error + field, + value, + operator, + indexPattern.id! + ); + filters = filters.map((filter) => ({ + ...filter, + $state: { store: esFilters.FilterStateStore.APP_STATE }, + })); + + await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters, + }); + }, + useNewFieldsApi: !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false), + showTimeCol: !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + ariaLabelledBy: 'documentsAriaLabel', + }; + + const timeRangeSearchSource = searchSource.create(); + timeRangeSearchSource.setField('filter', () => { + if (!this.searchProps || !this.input.timeRange) return; + return this.services.timefilter.createFilter(indexPattern, this.input.timeRange); + }); + + this.filtersSearchSource = searchSource.create(); + this.filtersSearchSource.setParent(timeRangeSearchSource); + + searchSource.setParent(this.filtersSearchSource); + + this.pushContainerStateParamsToProps(props); + + props.isLoading = true; + + if (this.savedSearch.grid) { + props.settings = this.savedSearch.grid; + } + } + + private async pushContainerStateParamsToProps( + searchProps: SearchProps, { forceFetch = false }: { forceFetch: boolean } = { forceFetch: false } ) { const isFetchRequired = !esFilters.onlyDisabledFiltersChanged(this.input.filters, this.prevFilters) || - !_.isEqual(this.prevQuery, this.input.query) || - !_.isEqual(this.prevTimeRange, this.input.timeRange) || - !_.isEqual(searchScope.sort, this.input.sort || this.savedSearch.sort) || + !isEqual(this.prevQuery, this.input.query) || + !isEqual(this.prevTimeRange, this.input.timeRange) || + !isEqual(searchProps.sort, this.input.sort || this.savedSearch.sort) || this.prevSearchSessionId !== this.input.searchSessionId; // If there is column or sort data on the panel, that means the original columns or sort settings have // been overridden in a dashboard. - searchScope.columns = handleSourceColumnState( + searchProps.columns = handleSourceColumnState( { columns: this.input.columns || this.savedSearch.columns }, this.services.core.uiSettings ).columns; + const savedSearchSort = this.savedSearch.sort && this.savedSearch.sort.length ? this.savedSearch.sort : getDefaultSort( - this.searchScope?.indexPattern, + this.searchProps?.indexPattern, getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') ); - searchScope.sort = this.input.sort || savedSearchSort; - searchScope.sharedItemTitle = this.panelTitle; - + searchProps.sort = this.input.sort || savedSearchSort; + searchProps.sharedItemTitle = this.panelTitle; if (forceFetch || isFetchRequired) { - this.filtersSearchSource!.setField('filter', this.input.filters); - this.filtersSearchSource!.setField('query', this.input.query); + this.filtersSearchSource.setField('filter', this.input.filters); + this.filtersSearchSource.setField('query', this.input.query); if (this.input.query?.query || this.input.filters?.length) { - this.filtersSearchSource!.setField('highlightAll', true); + this.filtersSearchSource.setField('highlightAll', true); } else { - this.filtersSearchSource!.removeField('highlightAll'); + this.filtersSearchSource.removeField('highlightAll'); } this.prevFilters = this.input.filters; this.prevQuery = this.input.query; this.prevTimeRange = this.input.timeRange; this.prevSearchSessionId = this.input.searchSessionId; - this.fetch(); - } else if (this.searchScope) { - // trigger a digest cycle to make sure non-fetch relevant changes are propagated - this.searchScope.$applyAsync(); + this.searchProps = searchProps; + await this.fetch(); + } else if (this.searchProps && this.node) { + this.searchProps = searchProps; + } + + if (this.node) { + this.renderReactComponent(this.node, this.searchProps!); + } + } + + /** + * + * @param {Element} domNode + */ + public async render(domNode: HTMLElement) { + if (!this.searchProps) { + throw new Error('Search props not defined'); + } + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = domNode; + } + + private renderReactComponent(domNode: HTMLElement, searchProps: SearchProps) { + if (!this.searchProps) { + return; + } + const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY); + const props = { + searchProps, + useLegacyTable, + refs: domNode, + }; + ReactDOM.render(, domNode); + } + + public reload() { + if (this.searchProps) { + this.pushContainerStateParamsToProps(this.searchProps, { forceFetch: true }); } } + + public getSavedSearch(): SavedSearch { + return this.savedSearch; + } + + public getInspectorAdapters() { + return this.inspectorAdapters; + } + + public destroy() { + super.destroy(); + this.savedSearch.destroy(); + if (this.searchProps) { + delete this.searchProps; + } + this.subscription?.unsubscribe(); + + if (this.abortController) this.abortController.abort(); + } } diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx new file mode 100644 index 00000000000000..5b2a2635d04bdf --- /dev/null +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { DiscoverGridEmbeddable } from '../angular/create_discover_grid_directive'; +import { DiscoverDocTableEmbeddable } from '../angular/doc_table/create_doc_table_embeddable'; +import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; +import { SearchProps } from './saved_search_embeddable'; + +interface SavedSearchEmbeddableComponentProps { + searchProps: SearchProps; + useLegacyTable: boolean; + refs: HTMLElement; +} + +const DiscoverDocTableEmbeddableMemoized = React.memo(DiscoverDocTableEmbeddable); +const DiscoverGridEmbeddableMemoized = React.memo(DiscoverGridEmbeddable); + +export function SavedSearchEmbeddableComponent({ + searchProps, + useLegacyTable, + refs, +}: SavedSearchEmbeddableComponentProps) { + if (useLegacyTable) { + const docTableProps = { + ...searchProps, + refs, + }; + return ; + } + const discoverGridProps = searchProps as DiscoverGridProps; + return ; +} diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 77da138d118dd0..360844976284eb 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -18,8 +18,9 @@ import { import { TimeRange } from '../../../../data/public'; -import { SearchInput, SearchOutput, SearchEmbeddable } from './types'; +import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +import { SavedSearchEmbeddable } from './saved_search_embeddable'; interface StartServices { executeTriggerActions: UiActionsStart['executeTriggerActions']; @@ -27,7 +28,7 @@ interface StartServices { } export class SearchEmbeddableFactory - implements EmbeddableFactoryDefinition { + implements EmbeddableFactoryDefinition { public readonly type = SEARCH_EMBEDDABLE_TYPE; private $injector: auto.IInjectorService | null; private getInjector: () => Promise | null; @@ -65,14 +66,11 @@ export class SearchEmbeddableFactory savedObjectId: string, input: Partial & { id: string; timeRange: TimeRange }, parent?: Container - ): Promise => { + ): Promise => { if (!this.$injector) { this.$injector = await this.getInjector(); } - const $injector = this.$injector as auto.IInjectorService; - const $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); const filterManager = getServices().filterManager; const url = await getServices().getSavedSearchUrlById(savedObjectId); @@ -81,12 +79,12 @@ export class SearchEmbeddableFactory const savedObject = await getServices().getSavedSearchById(savedObjectId); const indexPattern = savedObject.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); - const { SearchEmbeddable: SearchEmbeddableClass } = await import('./search_embeddable'); - return new SearchEmbeddableClass( + const { SavedSearchEmbeddable: SavedSearchEmbeddableClass } = await import( + './saved_search_embeddable' + ); + return new SavedSearchEmbeddableClass( { savedSearch: savedObject, - $rootScope, - $compile, editUrl, editPath: url, filterManager, diff --git a/src/plugins/discover/public/application/embeddable/search_template.html b/src/plugins/discover/public/application/embeddable/search_template.html deleted file mode 100644 index 3e37b3645650f2..00000000000000 --- a/src/plugins/discover/public/application/embeddable/search_template.html +++ /dev/null @@ -1,21 +0,0 @@ - - diff --git a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html deleted file mode 100644 index 8ad7938350d9c5..00000000000000 --- a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html +++ /dev/null @@ -1,19 +0,0 @@ - From 06aaa529d439f6c60d2e08cd4af93e59828a0cd6 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 14 Jun 2021 13:04:04 +0300 Subject: [PATCH 50/99] [Dashboard]: Fixes disabled viz filter is applied (#101859) * Fixes filter is applied even if is disabled on a dashboard * Fix 18n problem Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/search/expressions/filters_to_ast.test.ts | 6 ++++++ .../data/common/search/expressions/filters_to_ast.ts | 1 + .../data/common/search/expressions/kibana_filter.ts | 10 +++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts index 108b48f9ea77ea..5d191a94d4c8d1 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts @@ -24,6 +24,9 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[0].functions[0]).toHaveProperty('name', 'kibanaFilter'); expect(actual[0].functions[0].arguments).toMatchInlineSnapshot(` Object { + "disabled": Array [ + false, + ], "negate": Array [ false, ], @@ -35,6 +38,9 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[1].functions[0]).toHaveProperty('name', 'kibanaFilter'); expect(actual[1].functions[0].arguments).toMatchInlineSnapshot(` Object { + "disabled": Array [ + false, + ], "negate": Array [ true, ], diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index a4dd959caecf6f..edcf884b3ed312 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -17,6 +17,7 @@ export const filtersToAst = (filters: Filter[] | Filter) => { buildExpressionFunction('kibanaFilter', { query: JSON.stringify(restOfFilter), negate: filter.meta.negate, + disabled: filter.meta.disabled, }), ]); }); diff --git a/src/plugins/data/common/search/expressions/kibana_filter.ts b/src/plugins/data/common/search/expressions/kibana_filter.ts index 6d6f70fa8d1d6b..c94a3763ee084e 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.ts @@ -13,6 +13,7 @@ import { KibanaFilter } from './kibana_context_type'; interface Arguments { query: string; negate?: boolean; + disabled?: boolean; } export type ExpressionFunctionKibanaFilter = ExpressionFunctionDefinition< @@ -45,6 +46,13 @@ export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { defaultMessage: 'Should the filter be negated', }), }, + disabled: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.functions.kibanaFilter.disabled.help', { + defaultMessage: 'Should the filter be disabled', + }), + }, }, fn(input, args) { @@ -53,7 +61,7 @@ export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { meta: { negate: args.negate || false, alias: '', - disabled: false, + disabled: args.disabled || false, }, ...JSON.parse(args.query), }; From ec2ec6ab406c39067c3b26158c97a29eb871e7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Mon, 14 Jun 2021 12:25:42 +0200 Subject: [PATCH 51/99] [Security solution][Endpoint] Removes zip compression when creating artifacts (#101379) * Removes zlib compression when creating artifacts. Also fixes related unit tests and removes old code * Replaces artifact in new manifest using the ones from fleet client with zlip compression * Fixes create_policy_artifact_manifest pushArtifacts missing new manifest. Also fixes unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/schema/common.ts | 1 + .../server/endpoint/lib/artifacts/lists.ts | 37 ------ .../endpoint/lib/artifacts/manifest.test.ts | 4 +- .../server/endpoint/lib/artifacts/manifest.ts | 22 ++++ .../lib/artifacts/manifest_entry.test.ts | 12 +- .../migrate_artifacts_to_fleet.test.ts | 2 +- .../server/endpoint/lib/artifacts/mocks.ts | 18 +-- .../endpoint/lib/artifacts/task.test.ts | 35 +++--- .../server/endpoint/lib/artifacts/task.ts | 3 +- .../schemas/artifacts/saved_objects.mock.ts | 34 +----- .../artifacts/artifact_client.test.ts | 2 +- .../services/artifacts/artifact_client.ts | 10 +- .../manifest_manager/manifest_manager.mock.ts | 2 +- .../manifest_manager/manifest_manager.test.ts | 107 +++++++++--------- .../manifest_manager/manifest_manager.ts | 39 +++---- .../fleet_integration.test.ts | 20 ++-- .../create_policy_artifact_manifest.ts | 3 +- 17 files changed, 158 insertions(+), 193 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts index ee59ce12f98124..0aff91aafa5998 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts @@ -15,6 +15,7 @@ export type CompressionAlgorithm = t.TypeOf; export const compressionAlgorithmDispatch = t.keyof({ zlib: null, + none: null, }); export type CompressionAlgorithmDispatch = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index d2fad90d9e7d60..f5d3b30bf15faa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -6,7 +6,6 @@ */ import { createHash } from 'crypto'; -import { deflate } from 'zlib'; import type { Entry, EntryNested, @@ -21,9 +20,7 @@ import { } from '@kbn/securitysolution-list-constants'; import { ExceptionListClient } from '../../../../../lists/server'; import { - internalArtifactCompleteSchema, InternalArtifactCompleteSchema, - InternalArtifactSchema, TranslatedEntry, translatedEntry as translatedEntryType, translatedEntryMatchAnyMatcher, @@ -60,28 +57,6 @@ export async function buildArtifact( }; } -export async function maybeCompressArtifact( - uncompressedArtifact: InternalArtifactSchema -): Promise { - const compressedArtifact = { ...uncompressedArtifact }; - if (internalArtifactCompleteSchema.is(uncompressedArtifact)) { - const compressedArtifactBody = await compressExceptionList( - Buffer.from(uncompressedArtifact.body, 'base64') - ); - compressedArtifact.body = compressedArtifactBody.toString('base64'); - compressedArtifact.encodedSize = compressedArtifactBody.byteLength; - compressedArtifact.compressionAlgorithm = 'zlib'; - compressedArtifact.encodedSha256 = createHash('sha256') - .update(compressedArtifactBody) - .digest('hex'); - } - return compressedArtifact; -} - -export function isCompressed(artifact: InternalArtifactSchema) { - return artifact.compressionAlgorithm === 'zlib'; -} - export async function getFilteredEndpointExceptionList( eClient: ExceptionListClient, schemaVersion: string, @@ -297,15 +272,3 @@ function translateEntry( } } } - -export async function compressExceptionList(buffer: Buffer): Promise { - return new Promise((resolve, reject) => { - deflate(buffer, function (err, buf) { - if (err) { - reject(err); - } else { - resolve(buf); - } - }); - }); -} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 2eb72566bb4764..3948c51f6c5d8f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -37,8 +37,8 @@ describe('manifest', () => { let ARTIFACT_COPY_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; beforeAll(async () => { - ARTIFACTS = await getMockArtifacts({ compress: true }); - ARTIFACTS_COPY = await getMockArtifacts({ compress: true }); + ARTIFACTS = await getMockArtifacts(); + ARTIFACTS_COPY = await getMockArtifacts(); ARTIFACT_EXCEPTIONS_MACOS = ARTIFACTS[0]; ARTIFACT_EXCEPTIONS_WINDOWS = ARTIFACTS[1]; ARTIFACT_EXCEPTIONS_LINUX = ARTIFACTS[2]; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts index 43d4fc49161bb5..7c1906cdd7a88e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -12,6 +12,7 @@ import { InternalArtifactSchema, InternalManifestSchema, InternalManifestEntrySchema, + InternalArtifactCompleteSchema, } from '../../schemas/artifacts'; import { ManifestSchemaVersion, @@ -139,6 +140,27 @@ export class Manifest { return this.allEntries.get(getArtifactId(artifact))?.specificTargetPolicies; } + /** + * Replaces an artifact from all the collections. + * + * @param artifact An InternalArtifactCompleteSchema representing the artifact. + */ + public replaceArtifact(artifact: InternalArtifactCompleteSchema) { + const existingEntry = this.allEntries.get(getArtifactId(artifact)); + if (existingEntry) { + existingEntry.entry = new ManifestEntry(artifact); + + this.allEntries.set(getArtifactId(artifact), existingEntry); + this.defaultEntries.set(getArtifactId(artifact), existingEntry.entry); + + existingEntry.specificTargetPolicies.forEach((policyId) => { + const entries = this.policySpecificEntries.get(policyId) || new Map(); + entries.set(existingEntry.entry.getDocId(), existingEntry.entry); + this.policySpecificEntries.set(policyId, entries); + }); + } + } + public diff(manifest: Manifest): ManifestDiff { const diff: ManifestDiff = { additions: [], diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts index 99f08103ece065..c60ccca5253b70 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts @@ -15,7 +15,7 @@ describe('manifest_entry', () => { let manifestEntry: ManifestEntry; beforeAll(async () => { - artifact = await getInternalArtifactMock('windows', 'v1', { compress: true }); + artifact = await getInternalArtifactMock('windows', 'v1'); manifestEntry = new ManifestEntry(artifact); }); @@ -35,7 +35,7 @@ describe('manifest_entry', () => { test('Correct sha256 is returned', () => { expect(manifestEntry.getEncodedSha256()).toEqual( - '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e' + '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' ); expect(manifestEntry.getDecodedSha256()).toEqual( '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' @@ -43,7 +43,7 @@ describe('manifest_entry', () => { }); test('Correct size is returned', () => { - expect(manifestEntry.getEncodedSize()).toEqual(147); + expect(manifestEntry.getEncodedSize()).toEqual(432); expect(manifestEntry.getDecodedSize()).toEqual(432); }); @@ -59,12 +59,12 @@ describe('manifest_entry', () => { test('Correct record is returned', () => { expect(manifestEntry.getRecord()).toEqual({ - compression_algorithm: 'zlib', + compression_algorithm: 'none', encryption_algorithm: 'none', decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', decoded_size: 432, - encoded_size: 147, + encoded_size: 432, relative_url: '/api/fleet/artifacts/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts index b74492103dbdc4..2071d4b8c27b71 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts @@ -49,7 +49,7 @@ describe('When migrating artifacts to fleet', () => { type: '', id: 'abc123', references: [], - attributes: await getInternalArtifactMock('windows', 'v1', { compress: true }), + attributes: await getInternalArtifactMock('windows', 'v1'), }, ]) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts index cda42bdf3f585e..172e4656b97549 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -17,49 +17,49 @@ import { import { ArtifactConstants } from './common'; import { Manifest } from './manifest'; -export const getMockArtifacts = async (opts?: { compress: boolean }) => { +export const getMockArtifacts = async () => { return Promise.all([ // Exceptions items ...ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( async (os) => { - return getInternalArtifactMock(os, 'v1', opts); + return getInternalArtifactMock(os, 'v1'); } ), // Trusted Apps items ...ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS.map< Promise >(async (os) => { - return getInternalArtifactMock(os, 'v1', opts, ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME); + return getInternalArtifactMock(os, 'v1', ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME); }), ]); }; -export const getMockArtifactsWithDiff = async (opts?: { compress: boolean }) => { +export const getMockArtifactsWithDiff = async () => { return Promise.all( ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( async (os) => { if (os === 'macos') { return getInternalArtifactMockWithDiffs(os, 'v1'); } - return getInternalArtifactMock(os, 'v1', opts); + return getInternalArtifactMock(os, 'v1'); } ) ); }; -export const getEmptyMockArtifacts = async (opts?: { compress: boolean }) => { +export const getEmptyMockArtifacts = async () => { return Promise.all( ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( async (os) => { - return getEmptyInternalArtifactMock(os, 'v1', opts); + return getEmptyInternalArtifactMock(os, 'v1'); } ) ); }; -export const getMockManifest = async (opts?: { compress: boolean }) => { +export const getMockManifest = async () => { const manifest = new Manifest(); - const artifacts = await getMockArtifacts(opts); + const artifacts = await getMockArtifacts(); artifacts.forEach((artifact) => manifest.addEntry(artifact)); return manifest; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts index 03b2f831ba7b57..b6fd384cdd6462 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts @@ -106,7 +106,7 @@ describe('task', () => { let ARTIFACT_TRUSTED_APPS_MACOS: InternalArtifactCompleteSchema; beforeAll(async () => { - const artifacts = await getMockArtifacts({ compress: true }); + const artifacts = await getMockArtifacts(); ARTIFACT_EXCEPTIONS_MACOS = artifacts[0]; ARTIFACT_EXCEPTIONS_WINDOWS = artifacts[1]; ARTIFACT_TRUSTED_APPS_MACOS = artifacts[2]; @@ -167,7 +167,7 @@ describe('task', () => { expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([], newManifest); expect(manifestManager.commit).not.toHaveBeenCalled(); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([]); @@ -192,10 +192,10 @@ describe('task', () => { expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ - ARTIFACT_EXCEPTIONS_MACOS, - ARTIFACT_TRUSTED_APPS_MACOS, - ]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_TRUSTED_APPS_MACOS], + newManifest + ); expect(manifestManager.commit).not.toHaveBeenCalled(); expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); @@ -221,10 +221,10 @@ describe('task', () => { expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ - ARTIFACT_EXCEPTIONS_MACOS, - ARTIFACT_TRUSTED_APPS_MACOS, - ]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_TRUSTED_APPS_MACOS], + newManifest + ); expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); @@ -251,10 +251,10 @@ describe('task', () => { expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ - ARTIFACT_EXCEPTIONS_MACOS, - ARTIFACT_TRUSTED_APPS_MACOS, - ]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_TRUSTED_APPS_MACOS], + newManifest + ); expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); @@ -284,7 +284,10 @@ describe('task', () => { expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ARTIFACT_TRUSTED_APPS_MACOS]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_TRUSTED_APPS_MACOS], + newManifest + ); expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([ARTIFACT_ID_1]); @@ -314,7 +317,7 @@ describe('task', () => { expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([], newManifest); expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([]); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index fc6d00d0c3e4fb..8588a30aceb897 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -130,7 +130,8 @@ export class ManifestTask { const diff = newManifest.diff(oldManifest); const persistErrors = await manifestManager.pushArtifacts( - diff.additions as InternalArtifactCompleteSchema[] + diff.additions as InternalArtifactCompleteSchema[], + newManifest ); if (persistErrors.length) { reportErrors(this.logger, persistErrors); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index 1975c2a92cc16f..7137c94ec71d20 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -5,33 +5,13 @@ * 2.0. */ -import { - buildArtifact, - maybeCompressArtifact, - isCompressed, - ArtifactConstants, -} from '../../lib/artifacts'; +import { buildArtifact, ArtifactConstants } from '../../lib/artifacts'; import { getTranslatedExceptionListMock } from './lists.mock'; -import { - InternalManifestSchema, - internalArtifactCompleteSchema, - InternalArtifactCompleteSchema, -} from './saved_objects'; - -const compressArtifact = async (artifact: InternalArtifactCompleteSchema) => { - const compressedArtifact = await maybeCompressArtifact(artifact); - if (!isCompressed(compressedArtifact)) { - throw new Error(`Unable to compress artifact: ${artifact.identifier}`); - } else if (!internalArtifactCompleteSchema.is(compressedArtifact)) { - throw new Error(`Incomplete artifact detected: ${artifact.identifier}`); - } - return compressedArtifact; -}; +import { InternalManifestSchema, InternalArtifactCompleteSchema } from './saved_objects'; export const getInternalArtifactMock = async ( os: string, schemaVersion: string, - opts?: { compress: boolean }, artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { const artifact = await buildArtifact( @@ -40,23 +20,21 @@ export const getInternalArtifactMock = async ( os, artifactName ); - return opts?.compress ? compressArtifact(artifact) : artifact; + return artifact; }; export const getEmptyInternalArtifactMock = async ( os: string, schemaVersion: string, - opts?: { compress: boolean }, artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { const artifact = await buildArtifact({ entries: [] }, schemaVersion, os, artifactName); - return opts?.compress ? compressArtifact(artifact) : artifact; + return artifact; }; export const getInternalArtifactMockWithDiffs = async ( os: string, - schemaVersion: string, - opts?: { compress: boolean } + schemaVersion: string ): Promise => { const mock = getTranslatedExceptionListMock(); mock.entries.pop(); @@ -66,7 +44,7 @@ export const getInternalArtifactMockWithDiffs = async ( os, ArtifactConstants.GLOBAL_ALLOWLIST_NAME ); - return opts?.compress ? compressArtifact(artifact) : artifact; + return artifact; }; export const getInternalManifestMock = (): InternalManifestSchema => ({ diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index 1dcac108338bb7..740068a836d057 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -29,7 +29,7 @@ describe('artifact_client', () => { }); test('can create artifact', async () => { - const artifact = await getInternalArtifactMock('linux', 'v1', { compress: true }); + const artifact = await getInternalArtifactMock('linux', 'v1'); await artifactClient.createArtifact(artifact); expect(fleetArtifactClient.createArtifact).toHaveBeenCalledWith({ identifier: artifact.identifier, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index ef48ed1dd43f67..c0930980dffb9f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -5,13 +5,9 @@ * 2.0. */ -import { inflate as _inflate } from 'zlib'; -import { promisify } from 'util'; import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; import { Artifact, ArtifactsClientInterface } from '../../../../../fleet/server'; -const inflateAsync = promisify(_inflate); - export interface EndpointArtifactClientInterface { getArtifact(id: string): Promise; @@ -56,12 +52,8 @@ export class EndpointArtifactClient implements EndpointArtifactClientInterface { async createArtifact( artifact: InternalArtifactCompleteSchema ): Promise { - // FIXME:PT refactor to make this more efficient by passing through the uncompressed artifact content - // Artifact `.body` is compressed/encoded. We need it decoded and as a string - const artifactContent = await inflateAsync(Buffer.from(artifact.body, 'base64')); - const createdArtifact = await this.fleetArtifacts.createArtifact({ - content: artifactContent.toString(), + content: Buffer.from(artifact.body, 'base64').toString(), identifier: artifact.identifier, type: this.parseArtifactId(artifact.identifier).type, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index e0bbfc351a20f1..87a73e0130113f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -140,7 +140,7 @@ export const getManifestManagerMock = ( case ManifestManagerMockType.InitialSystemState: return null; case ManifestManagerMockType.NormalFlow: - return getMockManifest({ compress: true }); + return getMockManifest(); } }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 7719dbf30c72bf..e3dc66c20bb671 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { inflateSync } from 'zlib'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { ENDPOINT_LIST_ID, @@ -27,7 +26,6 @@ import { import { ManifestConstants, getArtifactId, - isCompressed, translateToEndpointExceptions, Manifest, } from '../../../lib/artifacts'; @@ -40,10 +38,8 @@ import { import { ManifestManager } from './manifest_manager'; import { EndpointArtifactClientInterface } from '../artifact_client'; -const uncompressData = async (data: Buffer) => JSON.parse(await inflateSync(data).toString()); - -const uncompressArtifact = async (artifact: InternalArtifactSchema) => - uncompressData(Buffer.from(artifact.body!, 'base64')); +const getArtifactObject = (artifact: InternalArtifactSchema) => + JSON.parse(Buffer.from(artifact.body!, 'base64').toString()); describe('ManifestManager', () => { const TEST_POLICY_ID_1 = 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'; @@ -77,7 +73,7 @@ describe('ManifestManager', () => { let ARTIFACT_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; beforeAll(async () => { - ARTIFACTS = await getMockArtifacts({ compress: true }); + ARTIFACTS = await getMockArtifacts(); ARTIFACTS_BY_ID = { [ARTIFACT_ID_EXCEPTIONS_MACOS]: ARTIFACTS[0], [ARTIFACT_ID_EXCEPTIONS_WINDOWS]: ARTIFACTS[1], @@ -270,10 +266,9 @@ describe('ManifestManager', () => { expect(artifacts.length).toBe(9); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); - expect(artifacts.every(isCompressed)).toBe(true); for (const artifact of artifacts) { - expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifact)).toStrictEqual({ entries: [] }); expect(manifest.isDefaultArtifact(artifact)).toBe(true); expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual( new Set([TEST_POLICY_ID_1]) @@ -308,21 +303,20 @@ describe('ManifestManager', () => { expect(artifacts.length).toBe(9); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); - expect(artifacts.every(isCompressed)).toBe(true); - expect(await uncompressArtifact(artifacts[0])).toStrictEqual({ + expect(getArtifactObject(artifacts[0])).toStrictEqual({ entries: translateToEndpointExceptions([exceptionListItem], 'v1'), }); - expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[4])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[5])).toStrictEqual({ + expect(getArtifactObject(artifacts[1])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[2])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[3])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[4])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[5])).toStrictEqual({ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), }); - expect(await uncompressArtifact(artifacts[6])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[7])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -364,19 +358,18 @@ describe('ManifestManager', () => { expect(artifacts.length).toBe(9); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); - expect(artifacts.every(isCompressed)).toBe(true); expect(artifacts[0]).toStrictEqual(oldManifest.getAllArtifacts()[0]); - expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[4])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[5])).toStrictEqual({ + expect(getArtifactObject(artifacts[1])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[2])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[3])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[4])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[5])).toStrictEqual({ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), }); - expect(await uncompressArtifact(artifacts[6])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[7])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -426,27 +419,26 @@ describe('ManifestManager', () => { expect(artifacts.length).toBe(10); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); - expect(artifacts.every(isCompressed)).toBe(true); - expect(await uncompressArtifact(artifacts[0])).toStrictEqual({ + expect(getArtifactObject(artifacts[0])).toStrictEqual({ entries: translateToEndpointExceptions([exceptionListItem], 'v1'), }); - expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[4])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[5])).toStrictEqual({ + expect(getArtifactObject(artifacts[1])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[2])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[3])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[4])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[5])).toStrictEqual({ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), }); - expect(await uncompressArtifact(artifacts[6])).toStrictEqual({ + expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: translateToEndpointExceptions( [trustedAppListItem, trustedAppListItemPolicy2], 'v1' ), }); - expect(await uncompressArtifact(artifacts[7])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[8])).toStrictEqual({ entries: [] }); - expect(await uncompressArtifact(artifacts[9])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[9])).toStrictEqual({ entries: [] }); for (const artifact of artifacts.slice(0, 5)) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -520,9 +512,13 @@ describe('ManifestManager', () => { const context = buildManifestManagerContextMock({}); const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); + const newManifest = ManifestManager.createDefaultManifest(); await expect( - manifestManager.pushArtifacts([ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_EXCEPTIONS_WINDOWS]) + manifestManager.pushArtifacts( + [ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_EXCEPTIONS_WINDOWS], + newManifest + ) ).resolves.toStrictEqual([]); expect(artifactClient.createArtifact).toHaveBeenCalledTimes(2); @@ -533,17 +529,18 @@ describe('ManifestManager', () => { ...ARTIFACT_EXCEPTIONS_WINDOWS, }); expect( - await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!) - ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_MACOS)); + JSON.parse(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!.toString()) + ).toStrictEqual(getArtifactObject(ARTIFACT_EXCEPTIONS_MACOS)); expect( - await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_WINDOWS))!) - ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)); + JSON.parse(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_WINDOWS))!.toString()) + ).toStrictEqual(getArtifactObject(ARTIFACT_EXCEPTIONS_WINDOWS)); }); test('Returns errors for partial failures', async () => { const context = buildManifestManagerContextMock({}); const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); + const newManifest = ManifestManager.createDefaultManifest(); const error = new Error(); const { body, ...incompleteArtifact } = ARTIFACT_TRUSTED_APPS_MACOS; @@ -558,11 +555,14 @@ describe('ManifestManager', () => { ); await expect( - manifestManager.pushArtifacts([ - ARTIFACT_EXCEPTIONS_MACOS, - ARTIFACT_EXCEPTIONS_WINDOWS, - incompleteArtifact as InternalArtifactCompleteSchema, - ]) + manifestManager.pushArtifacts( + [ + ARTIFACT_EXCEPTIONS_MACOS, + ARTIFACT_EXCEPTIONS_WINDOWS, + incompleteArtifact as InternalArtifactCompleteSchema, + ], + newManifest + ) ).resolves.toStrictEqual([ error, new Error(`Incomplete artifact: ${ARTIFACT_ID_TRUSTED_APPS_MACOS}`), @@ -573,8 +573,8 @@ describe('ManifestManager', () => { ...ARTIFACT_EXCEPTIONS_MACOS, }); expect( - await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!) - ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_MACOS)); + JSON.parse(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!.toString()) + ).toStrictEqual(getArtifactObject(ARTIFACT_EXCEPTIONS_MACOS)); expect(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_WINDOWS))).toBeUndefined(); }); }); @@ -843,10 +843,7 @@ describe('ManifestManager', () => { artifacts: toArtifactRecords({ [ARTIFACT_NAME_EXCEPTIONS_MACOS]: await getEmptyInternalArtifactMock( 'macos', - 'v1', - { - compress: true, - } + 'v1' ), }), manifest_version: '1.0.0', diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 6c25b6152938f7..27108a03f34033 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -25,9 +25,7 @@ import { getEndpointEventFiltersList, getEndpointExceptionList, getEndpointTrustedAppsList, - isCompressed, Manifest, - maybeCompressArtifact, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -239,13 +237,16 @@ export class ManifestManager { * Writes new artifact SO. * * @param artifact An InternalArtifactCompleteSchema representing the artifact. - * @returns {Promise} An error, if encountered, or null. + * @returns {Promise<[Error | null, InternalArtifactCompleteSchema | undefined]>} An array with the error if encountered or null and the generated artifact or null. */ - protected async pushArtifact(artifact: InternalArtifactCompleteSchema): Promise { + protected async pushArtifact( + artifact: InternalArtifactCompleteSchema + ): Promise<[Error | null, InternalArtifactCompleteSchema | undefined]> { const artifactId = getArtifactId(artifact); + let fleetArtifact; try { // Write the artifact SO - await this.artifactClient.createArtifact(artifact); + fleetArtifact = await this.artifactClient.createArtifact(artifact); // Cache the compressed body of the artifact this.cache.set(artifactId, Buffer.from(artifact.body, 'base64')); @@ -253,26 +254,32 @@ export class ManifestManager { if (this.savedObjectsClient.errors.isConflictError(err)) { this.logger.debug(`Tried to create artifact ${artifactId}, but it already exists.`); } else { - return err; + return [err, undefined]; } } - return null; + return [null, fleetArtifact]; } /** * Writes new artifact SOs. * * @param artifacts An InternalArtifactCompleteSchema array representing the artifacts. + * @param newManifest A Manifest representing the new manifest * @returns {Promise} Any errors encountered. */ - public async pushArtifacts(artifacts: InternalArtifactCompleteSchema[]): Promise { + public async pushArtifacts( + artifacts: InternalArtifactCompleteSchema[], + newManifest: Manifest + ): Promise { const errors: Error[] = []; for (const artifact of artifacts) { if (internalArtifactCompleteSchema.is(artifact)) { - const err = await this.pushArtifact(artifact); + const [err, fleetArtifact] = await this.pushArtifact(artifact); if (err) { errors.push(err); + } else if (fleetArtifact) { + newManifest.replaceArtifact(fleetArtifact); } } else { errors.push(new Error(`Incomplete artifact: ${getArtifactId(artifact)}`)); @@ -372,16 +379,10 @@ export class ManifestManager { for (const result of results) { await iterateArtifactsBuildResult(result, async (artifact, policyId) => { - let artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; - - if (!isCompressed(artifactToAdd)) { - artifactToAdd = await maybeCompressArtifact(artifactToAdd); - - if (!isCompressed(artifactToAdd)) { - throw new Error(`Unable to compress artifact: ${getArtifactId(artifactToAdd)}`); - } else if (!internalArtifactCompleteSchema.is(artifactToAdd)) { - throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); - } + const artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; + artifactToAdd.compressionAlgorithm = 'none'; + if (!internalArtifactCompleteSchema.is(artifactToAdd)) { + throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); } manifest.addEntry(artifactToAdd, policyId); diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index c0dc3a9343a7d8..1edcef6dec7224 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -100,7 +100,7 @@ describe('ingest_integration tests ', () => { let ARTIFACT_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; beforeAll(async () => { - const artifacts = await getMockArtifacts({ compress: true }); + const artifacts = await getMockArtifacts(); ARTIFACT_EXCEPTIONS_MACOS = artifacts[0]; ARTIFACT_EXCEPTIONS_WINDOWS = artifacts[1]; ARTIFACT_TRUSTED_APPS_MACOS = artifacts[3]; @@ -147,7 +147,10 @@ describe('ingest_integration tests ', () => { ); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_EXCEPTIONS_MACOS], + newManifest + ); expect(manifestManager.commit).not.toHaveBeenCalled(); }); @@ -170,7 +173,10 @@ describe('ingest_integration tests ', () => { ); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_EXCEPTIONS_MACOS], + newManifest + ); expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); }); @@ -197,10 +203,10 @@ describe('ingest_integration tests ', () => { ); expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); - expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ - ARTIFACT_EXCEPTIONS_MACOS, - ARTIFACT_TRUSTED_APPS_MACOS, - ]); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith( + [ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_TRUSTED_APPS_MACOS], + newManifest + ); expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); }); diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_policy_artifact_manifest.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_policy_artifact_manifest.ts index 8c2d612709ff33..2440a0a93187da 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_policy_artifact_manifest.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_policy_artifact_manifest.ts @@ -25,7 +25,8 @@ const getManifest = async (logger: Logger, manifestManager: ManifestManager): Pr // Persist new artifacts const persistErrors = await manifestManager.pushArtifacts( - newManifest.getAllArtifacts() as InternalArtifactCompleteSchema[] + newManifest.getAllArtifacts() as InternalArtifactCompleteSchema[], + newManifest ); if (persistErrors.length) { reportErrors(logger, persistErrors); From 85710b344fcf359604b0b1cfb0a3ac5b42da7ca2 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 14 Jun 2021 07:29:46 -0400 Subject: [PATCH 52/99] [Uptime] Add owner and description properties to Uptime's kibana.json (#101963) Co-authored-by: Shahzad Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/uptime/kibana.json | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 4d5ab531af7c47..625406c0cb97bd 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,16 +1,8 @@ { - "configPath": [ - "xpack", - "uptime" - ], + "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": [ - "data", - "home", - "ml", - "fleet" - ], + "optionalPlugins": ["data", "home", "ml", "fleet"], "requiredPlugins": [ "alerting", "embeddable", @@ -23,12 +15,10 @@ "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": [ - "observability", - "kibanaReact", - "kibanaUtils", - "home", - "data", - "ml" - ] + "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml"], + "owner": { + "name": "Uptime", + "githubTeam": "uptime" + }, + "description": "This plugin visualizes data from from Synthetics and Heartbeat, and integrates with other Observability solutions." } From f6ce964d9c4627de971f3bb84cabcd5a4a122e72 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 14 Jun 2021 12:31:55 +0100 Subject: [PATCH 53/99] [ML] Switching to new datafeed preview (#101780) * [ML] Switching to new datafeed preview * fixing wizard test button * adding schema validator * fixing tests * adding check for empty detectors list Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/custom_url_editor/utils.js | 114 ++++++------- .../job_details/datafeed_preview_tab.js | 6 +- .../datafeed_preview.tsx | 29 ++-- .../application/services/job_service.d.ts | 1 - .../application/services/job_service.js | 9 - .../services/ml_api_service/jobs.ts | 4 +- .../ml/server/models/job_service/datafeeds.ts | 155 +----------------- .../plugins/ml/server/routes/job_service.ts | 29 ++-- .../schemas/anomaly_detectors_schema.ts | 2 +- .../routes/schemas/job_service_schema.ts | 35 +++- .../apis/ml/jobs/datafeed_preview.ts | 36 ++-- 11 files changed, 124 insertions(+), 296 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index 9da97f40f5ec66..4f4cd0f6ef1c9a 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -17,10 +17,8 @@ import { parseInterval } from '../../../../../common/util/parse_interval'; import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; import { getIndexPatternIdFromName } from '../../../util/index_utils'; import { ml } from '../../../services/ml_api_service'; -import { mlJobService } from '../../../services/job_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; import { getSavedObjectsClient, getGetUrlGenerator } from '../../../util/dependency_cache'; -import { getProcessedFields } from '../../../components/data_grid'; export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { // Returns the settings object in the format used by the custom URL editor @@ -266,8 +264,7 @@ function buildAppStateQueryParam(queryFieldNames) { // Builds the full URL for testing out a custom URL configuration, which // may contain dollar delimited partition / influencer entity tokens and // drilldown time range settings. -export function getTestUrl(job, customUrl) { - const urlValue = customUrl.url_value; +export async function getTestUrl(job, customUrl) { const bucketSpanSecs = parseInterval(job.analysis_config.bucket_span).asSeconds(); // By default, return configured url_value. Look to substitute any dollar-delimited @@ -289,64 +286,55 @@ export function getTestUrl(job, customUrl) { sort: [{ record_score: { order: 'desc' } }], }; - return new Promise((resolve, reject) => { - ml.results - .anomalySearch( - { - body, - }, - [job.job_id] - ) - .then((resp) => { - if (resp.hits.total.value > 0) { - const record = resp.hits.hits[0]._source; - testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, record, 'timestamp'); - resolve(testUrl); - } else { - // No anomalies yet for this job, so do a preview of the search - // configured in the job datafeed to obtain sample docs. - mlJobService.searchPreview(job).then((response) => { - let testDoc; - const docTimeFieldName = job.data_description.time_field; - - // Handle datafeeds which use aggregations or documents. - if (response.aggregations) { - // Create a dummy object which contains the fields necessary to build the URL. - const firstBucket = response.aggregations.buckets.buckets[0]; - testDoc = { - [docTimeFieldName]: firstBucket.key, - }; - - // Look for bucket aggregations which match the tokens in the URL. - urlValue.replace(/\$([^?&$\'"]{1,40})\$/g, (match, name) => { - if (name !== 'earliest' && name !== 'latest' && firstBucket[name] !== undefined) { - const tokenBuckets = firstBucket[name]; - if (tokenBuckets.buckets) { - testDoc[name] = tokenBuckets.buckets[0].key; - } - } - }); - } else { - if (response.hits.total.value > 0) { - testDoc = getProcessedFields(response.hits.hits[0].fields); - } - } - - if (testDoc !== undefined) { - testUrl = replaceTokensInUrlValue( - customUrl, - bucketSpanSecs, - testDoc, - docTimeFieldName - ); - } + let resp; + try { + resp = await ml.results.anomalySearch( + { + body, + }, + [job.job_id] + ); + } catch (error) { + // search may fail if the job doesn't already exist + // ignore this error as the outer function call will raise a toast + } - resolve(testUrl); - }); - } - }) - .catch((resp) => { - reject(resp); - }); - }); + if (resp && resp.hits.total.value > 0) { + const record = resp.hits.hits[0]._source; + testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, record, 'timestamp'); + return testUrl; + } else { + // No anomalies yet for this job, so do a preview of the search + // configured in the job datafeed to obtain sample docs. + + let { datafeed_config: datafeedConfig, ...jobConfig } = job; + try { + // attempt load the non-combined job and datafeed so they can be used in the datafeed preview + const [{ jobs }, { datafeeds }] = await Promise.all([ + ml.getJobs({ jobId: job.job_id }), + ml.getDatafeeds({ datafeedId: job.datafeed_config.datafeed_id }), + ]); + datafeedConfig = datafeeds[0]; + jobConfig = jobs[0]; + } catch (error) { + // jobs may not exist as this might be called from the AD job wizards + // ignore this error as the outer function call will raise a toast + } + + if (jobConfig === undefined || datafeedConfig === undefined) { + return testUrl; + } + + const preview = await ml.jobs.datafeedPreview(undefined, jobConfig, datafeedConfig); + + const docTimeFieldName = job.data_description.time_field; + + // Create a dummy object which contains the fields necessary to build the URL. + const firstBucket = preview[0]; + if (firstBucket !== undefined) { + testUrl = replaceTokensInUrlValue(customUrl, bucketSpanSecs, firstBucket, docTimeFieldName); + } + + return testUrl; + } } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index e5fdae201eb04f..c24d8df3909fe7 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiLoadingSpinner } from '@elastic/eui'; -import { mlJobService } from '../../../../services/job_service'; +import { ml } from '../../../../services/ml_api_service'; import { checkPermission } from '../../../../capabilities/check_capabilities'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils'; import { MLJobEditor } from '../ml_job_editor'; @@ -88,8 +88,8 @@ DatafeedPreviewPane.propTypes = { function updateDatafeedPreview(job, canPreviewDatafeed) { return new Promise((resolve, reject) => { if (canPreviewDatafeed) { - mlJobService - .getDatafeedPreview(job.datafeed_config.datafeed_id) + ml.jobs + .datafeedPreview(job.datafeed_config.datafeed_id) .then((resp) => { if (Array.isArray(resp)) { resolve(JSON.stringify(resp.slice(0, ML_DATA_PREVIEW_COUNT), null, 2)); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index a4d9293e9369dc..c6d6e6789bd950 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -19,15 +19,15 @@ import { import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; -import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; -import { isPopulatedObject } from '../../../../../../../../common/util/object_utils'; -import { isMultiBucketAggregate } from '../../../../../../../../common/types/es_client'; +import { useMlApiContext } from '../../../../../../contexts/kibana'; export const DatafeedPreview: FC<{ combinedJob: CombinedJob | null; heightOffset?: number; }> = ({ combinedJob, heightOffset = 0 }) => { + const { + jobs: { datafeedPreview }, + } = useMlApiContext(); // the ace editor requires a fixed height const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ heightOffset, @@ -63,18 +63,17 @@ export const DatafeedPreview: FC<{ if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { try { - const resp = await mlJobService.searchPreview(combinedJob); - let data = resp.hits.hits; - // the first item under aggregations can be any name - if (isPopulatedObject(resp.aggregations)) { - const accessor = Object.keys(resp.aggregations)[0]; - const aggregate = resp.aggregations[accessor]; - if (isMultiBucketAggregate(aggregate)) { - data = aggregate.buckets.slice(0, ML_DATA_PREVIEW_COUNT); - } + const { datafeed_config: datafeed, ...job } = combinedJob; + if (job.analysis_config.detectors.length === 0) { + setPreviewJsonString( + i18n.translate('xpack.ml.newJob.wizard.datafeedPreviewFlyout.noDetectors', { + defaultMessage: 'No detectors configured', + }) + ); + } else { + const preview = await datafeedPreview(undefined, job, datafeed); + setPreviewJsonString(JSON.stringify(preview, null, 2)); } - - setPreviewJsonString(JSON.stringify(data, null, 2)); } catch (error) { setPreviewJsonString(JSON.stringify(error, null, 2)); } diff --git a/x-pack/plugins/ml/public/application/services/job_service.d.ts b/x-pack/plugins/ml/public/application/services/job_service.d.ts index ceadca12f87575..667f23da34aa04 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/job_service.d.ts @@ -41,7 +41,6 @@ declare interface JobService { ): Promise; createResultsUrl(jobId: string[], start: number, end: number, location: string): string; getJobAndGroupIds(): Promise; - searchPreview(job: CombinedJob): Promise>; getJob(jobId: string): CombinedJob; loadJobsWrapper(): Promise; } diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 2fa60b8db83a7b..3c93c8a1ae85ac 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -347,11 +347,6 @@ class JobService { return job; } - searchPreview(combinedJob) { - const { datafeed_config: datafeed, ...job } = combinedJob; - return ml.jobs.datafeedPreview(job, datafeed); - } - openJob(jobId) { return ml.openJob({ jobId }); } @@ -435,10 +430,6 @@ class JobService { return datafeedId; } - getDatafeedPreview(datafeedId) { - return ml.datafeedPreview({ datafeedId }); - } - // get the list of job group ids as well as how many jobs are in each group getJobGroups() { const groups = []; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 144492eacc2473..7cd08bc1fd15c1 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -330,8 +330,8 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, - datafeedPreview(job: Job, datafeed: Datafeed) { - const body = JSON.stringify({ job, datafeed }); + datafeedPreview(datafeedId?: string, job?: Job, datafeed?: Datafeed) { + const body = JSON.stringify({ datafeedId, job, datafeed }); return httpService.http<{ total: number; categories: Array<{ count?: number; category: Category }>; diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index 5dfe1b5934fe9f..8b3f7f4b0b0eea 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -10,12 +10,8 @@ import { i18n } from '@kbn/i18n'; import { IScopedClusterClient } from 'kibana/server'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; -import { Datafeed, DatafeedStats, Job } from '../../../common/types/anomaly_detection_jobs'; -import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; -import { fieldsServiceProvider } from '../fields_service'; +import { Datafeed, DatafeedStats } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; -import { parseInterval } from '../../../common/util/parse_interval'; -import { isPopulatedObject } from '../../../common/util/object_utils'; export interface MlDatafeedsResponse { datafeeds: Datafeed[]; @@ -235,154 +231,6 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie } } - async function datafeedPreview(job: Job, datafeed: Datafeed) { - let query: any = { match_all: {} }; - if (datafeed.query) { - query = datafeed.query; - } - const { getTimeFieldRange } = fieldsServiceProvider(client); - const { start } = await getTimeFieldRange( - datafeed.indices, - job.data_description.time_field, - query, - datafeed.runtime_mappings, - datafeed.indices_options - ); - - // Get bucket span - // Get first doc time for datafeed - // Create a new query - must user query and must range query. - // Time range 'to' first doc time plus < 10 buckets - - // Do a preliminary search to get the date of the earliest doc matching the - // query in the datafeed. This will be used to apply a time range criteria - // on the datafeed search preview. - // This time filter is required for datafeed searches using aggregations to ensure - // the search does not create too many buckets (default 10000 max_bucket limit), - // but apply it to searches without aggregations too for consistency. - const bucketSpan = parseInterval(job.analysis_config.bucket_span); - if (bucketSpan === null) { - return; - } - const earliestMs = start.epoch; - const latestMs = +start.epoch + 10 * bucketSpan.asMilliseconds(); - - const body: any = { - query: { - bool: { - must: [ - { - range: { - [job.data_description.time_field]: { - gte: earliestMs, - lt: latestMs, - format: 'epoch_millis', - }, - }, - }, - query, - ], - }, - }, - }; - - // if aggs or aggregations is set, add it to the search - const aggregations = datafeed.aggs ?? datafeed.aggregations; - if (isPopulatedObject(aggregations)) { - body.size = 0; - body.aggregations = aggregations; - - // add script_fields if present - const scriptFields = datafeed.script_fields; - if (isPopulatedObject(scriptFields)) { - body.script_fields = scriptFields; - } - - // add runtime_mappings if present - const runtimeMappings = datafeed.runtime_mappings; - if (isPopulatedObject(runtimeMappings)) { - body.runtime_mappings = runtimeMappings; - } - } else { - // if aggregations is not set and retrieveWholeSource is not set, add all of the fields from the job - body.size = ML_DATA_PREVIEW_COUNT; - - // add script_fields if present - const scriptFields = datafeed.script_fields; - if (isPopulatedObject(scriptFields)) { - body.script_fields = scriptFields; - } - - // add runtime_mappings if present - const runtimeMappings = datafeed.runtime_mappings; - if (isPopulatedObject(runtimeMappings)) { - body.runtime_mappings = runtimeMappings; - } - - const fields = new Set(); - - // get fields from detectors - if (job.analysis_config.detectors) { - job.analysis_config.detectors.forEach((dtr) => { - if (dtr.by_field_name) { - fields.add(dtr.by_field_name); - } - if (dtr.field_name) { - fields.add(dtr.field_name); - } - if (dtr.over_field_name) { - fields.add(dtr.over_field_name); - } - if (dtr.partition_field_name) { - fields.add(dtr.partition_field_name); - } - }); - } - - // get fields from influencers - if (job.analysis_config.influencers) { - job.analysis_config.influencers.forEach((inf) => { - fields.add(inf); - }); - } - - // get fields from categorizationFieldName - if (job.analysis_config.categorization_field_name) { - fields.add(job.analysis_config.categorization_field_name); - } - - // get fields from summary_count_field_name - if (job.analysis_config.summary_count_field_name) { - fields.add(job.analysis_config.summary_count_field_name); - } - - // get fields from time_field - if (job.data_description.time_field) { - fields.add(job.data_description.time_field); - } - - // add runtime fields - if (runtimeMappings) { - Object.keys(runtimeMappings).forEach((fieldName) => { - fields.add(fieldName); - }); - } - - const fieldsList = [...fields]; - if (fieldsList.length) { - body.fields = fieldsList; - body._source = false; - } - } - const data = { - index: datafeed.indices, - body, - ...(datafeed.indices_options ?? {}), - }; - - return (await client.asCurrentUser.search(data)).body; - } - return { forceStartDatafeeds, stopDatafeeds, @@ -390,6 +238,5 @@ export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClie getDatafeedIdsByJobId, getJobIdsByDatafeedId, getDatafeedByJobId, - datafeedPreview, }; } diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 2dd85a8772f924..992822f6d6eb82 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; @@ -806,23 +807,21 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { try { - // @ts-ignore schema mismatch const { datafeedId, job, datafeed } = request.body; - if (datafeedId !== undefined) { - const { body } = await mlClient.previewDatafeed( - { - datafeed_id: datafeedId, - }, - getAuthorizationHeader(request) - ); - return response.ok({ - body, - }); - } - - const { datafeedPreview } = jobServiceProvider(client, mlClient); - const body = await datafeedPreview(job, datafeed); + const payload = + datafeedId !== undefined + ? { + datafeed_id: datafeedId, + } + : ({ + body: { + job_config: job, + datafeed_config: datafeed, + }, + } as estypes.MlPreviewDatafeedRequest); + + const { body } = await mlClient.previewDatafeed(payload, getAuthorizationHeader(request)); return response.ok({ body, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 392c0d3514d648..ec39d08ee357d5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -123,7 +123,7 @@ export const anomalyDetectionJobSchema = { job_id: schema.string(), job_type: schema.maybe(schema.string()), job_version: schema.maybe(schema.string()), - groups: schema.arrayOf(schema.maybe(schema.string())), + groups: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), model_plot_config: schema.maybe(schema.any()), model_plot: schema.maybe(schema.any()), model_size_stats: schema.maybe(schema.any()), diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index 45ef3e3f73b6ec..df91dea101c7cb 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -109,15 +109,32 @@ export const revertModelSnapshotSchema = schema.object({ ), }); -export const datafeedPreviewSchema = schema.oneOf([ - schema.object({ - job: schema.object(anomalyDetectionJobSchema), - datafeed: datafeedConfigSchema, - }), - schema.object({ - datafeedId: schema.string(), - }), -]); +export const datafeedPreviewSchema = schema.object( + { + job: schema.maybe(schema.object(anomalyDetectionJobSchema)), + datafeed: schema.maybe(datafeedConfigSchema), + datafeedId: schema.maybe(schema.string()), + }, + { + validate: (v) => { + const msg = 'supply either a datafeed_id for an existing job or a job and datafeed config'; + if (v.datafeedId !== undefined && (v.job !== undefined || v.datafeed !== undefined)) { + // datafeed_id is supplied but job and datafeed configs are also supplied + return msg; + } + + if (v.datafeedId === undefined && (v.job === undefined || v.datafeed === undefined)) { + // datafeed_id is not supplied but job or datafeed configs are missing + return msg; + } + + if (v.datafeedId === undefined && v.job === undefined && v.datafeed === undefined) { + // everything is missing + return msg; + } + }, + } +); export const jobsExistSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), diff --git a/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts b/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts index d299795826c266..4a0545049a76e4 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/datafeed_preview.ts @@ -72,13 +72,10 @@ export default ({ getService }: FtrProviderContext) => { .send({ job, datafeed }) .expect(200); - expect(body.hits.total.value).to.eql(3207, 'Response body total hits should be 3207'); - expect(Array.isArray(body.hits?.hits[0]?.fields?.airline)).to.eql( - true, - 'Response body airlines should be an array' - ); + expect(body.length).to.eql(1000, 'Response body total hits should be 1000'); + expect(typeof body[0]?.airline).to.eql('string', 'Response body airlines should be a string'); - const airlines: string[] = body.hits.hits.map((a: any) => a.fields.airline[0]); + const airlines: string[] = body.map((a: any) => a.airline); expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); expect(airlines.every((a) => isUpperCase(a))).to.eql( true, @@ -112,13 +109,10 @@ export default ({ getService }: FtrProviderContext) => { .send({ job, datafeed }) .expect(200); - expect(body.hits.total.value).to.eql(300, 'Response body total hits should be 300'); - expect(Array.isArray(body.hits?.hits[0]?.fields?.airline)).to.eql( - true, - 'Response body airlines should be an array' - ); + expect(body.length).to.eql(1000, 'Response body total hits should be 1000'); + expect(typeof body[0]?.airline).to.eql('string', 'Response body airlines should be a string'); - const airlines: string[] = body.hits.hits.map((a: any) => a.fields.airline[0]); + const airlines: string[] = body.map((a: any) => a.airline); expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); expect(airlines.every((a) => a === 'AAL')).to.eql( true, @@ -157,13 +151,10 @@ export default ({ getService }: FtrProviderContext) => { .send({ job, datafeed }) .expect(200); - expect(body.hits.total.value).to.eql(3207, 'Response body total hits should be 3207'); - expect(Array.isArray(body.hits?.hits[0]?.fields?.lowercase_airline)).to.eql( - true, - 'Response body airlines should be an array' - ); + expect(body.length).to.eql(1000, 'Response body total hits should be 1000'); + expect(typeof body[0]?.airline).to.eql('string', 'Response body airlines should be a string'); - const airlines: string[] = body.hits.hits.map((a: any) => a.fields.lowercase_airline[0]); + const airlines: string[] = body.map((a: any) => a.lowercase_airline); expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); expect(isLowerCase(airlines[0])).to.eql( true, @@ -205,13 +196,10 @@ export default ({ getService }: FtrProviderContext) => { .send({ job, datafeed }) .expect(200); - expect(body.hits.total.value).to.eql(300, 'Response body total hits should be 300'); - expect(Array.isArray(body.hits?.hits[0]?.fields?.airline)).to.eql( - true, - 'Response body airlines should be an array' - ); + expect(body.length).to.eql(1000, 'Response body total hits should be 1000'); + expect(typeof body[0]?.airline).to.eql('string', 'Response body airlines should be a string'); - const airlines: string[] = body.hits.hits.map((a: any) => a.fields.airline[0]); + const airlines: string[] = body.map((a: any) => a.airline); expect(airlines.length).to.not.eql(0, 'airlines length should not be 0'); expect(isLowerCase(airlines[0])).to.eql( true, From b60a438dd328f3a63ea2bd995d3fdab7be922617 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 14 Jun 2021 12:51:58 +0100 Subject: [PATCH 54/99] skip flaky suite (#100438) --- x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js index 02f65fd5579da0..9c03faf72eff1f 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js @@ -249,7 +249,8 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('with only online nodes', () => { + // FLAKY: https://github.com/elastic/kibana/issues/100438 + describe.skip('with only online nodes', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); before(async () => { From fa855b35b6deefcd926401f9e9a9fe9bd4a310ef Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 14 Jun 2021 14:23:14 +0200 Subject: [PATCH 55/99] Management app locator (#101795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 create management app locator * refactor: 💡 simplify management locator * feat: 🎸 export management app locator from plugin contract * feat: 🎸 improve share plugin exports * test: 💍 fix test mock * test: 💍 adjust test mocks * Update src/plugins/management/public/plugin.ts Co-authored-by: Tim Roes * Update src/plugins/management/public/types.ts Co-authored-by: Tim Roes * Update src/plugins/management/public/types.ts Co-authored-by: Tim Roes * Update src/plugins/management/server/plugin.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tim Roes --- src/plugins/management/common/locator.test.ts | 53 +++++++++++++++++++ src/plugins/management/common/locator.ts | 32 +++++++++++ src/plugins/management/kibana.json | 2 +- src/plugins/management/public/mocks/index.ts | 8 +++ src/plugins/management/public/plugin.ts | 22 ++++++-- src/plugins/management/public/types.ts | 3 ++ src/plugins/management/server/plugin.ts | 22 ++++++-- src/plugins/share/common/index.ts | 9 ++++ src/plugins/share/public/index.ts | 2 + src/plugins/share/server/index.ts | 2 + .../management/management_service.test.ts | 2 + 11 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 src/plugins/management/common/locator.test.ts create mode 100644 src/plugins/management/common/locator.ts create mode 100644 src/plugins/share/common/index.ts diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts new file mode 100644 index 00000000000000..dda393a4203ecd --- /dev/null +++ b/src/plugins/management/common/locator.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MANAGEMENT_APP_ID } from './contants'; +import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator'; + +test('locator has the right ID', () => { + const locator = new ManagementAppLocator(); + + expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR); +}); + +test('returns management app ID', async () => { + const locator = new ManagementAppLocator(); + const location = await locator.getLocation({ + sectionId: 'a', + appId: 'b', + }); + + expect(location).toMatchObject({ + app: MANAGEMENT_APP_ID, + }); +}); + +test('returns Kibana location for section ID and app ID pair', async () => { + const locator = new ManagementAppLocator(); + const location = await locator.getLocation({ + sectionId: 'ingest', + appId: 'index', + }); + + expect(location).toMatchObject({ + route: '/ingest/index', + state: {}, + }); +}); + +test('when app ID is not provided, returns path to just the section ID', async () => { + const locator = new ManagementAppLocator(); + const location = await locator.getLocation({ + sectionId: 'data', + }); + + expect(location).toMatchObject({ + route: '/data', + state: {}, + }); +}); diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts new file mode 100644 index 00000000000000..4a4a50f468adc6 --- /dev/null +++ b/src/plugins/management/common/locator.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { LocatorDefinition } from 'src/plugins/share/common'; +import { MANAGEMENT_APP_ID } from './contants'; + +export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; + +export interface ManagementAppLocatorParams extends SerializableState { + sectionId: string; + appId?: string; +} + +export class ManagementAppLocator implements LocatorDefinition { + public readonly id = MANAGEMENT_APP_LOCATOR; + + public readonly getLocation = async (params: ManagementAppLocatorParams) => { + const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; + + return { + app: MANAGEMENT_APP_ID, + route, + state: {}, + }; + }; +} diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 6c8574f0242290..44c3f861709cee 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "share"], "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] } diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 4dcdd22d5d2095..70d853f32dfcc3 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -30,6 +30,14 @@ const createSetupContract = (): ManagementSetup => ({ stack: createManagementSectionMock(), } as unknown) as DefinedSections, }, + locator: { + getLocation: jest.fn(async () => ({ + app: 'MANAGEMENT', + route: '', + state: {}, + })), + navigate: jest.fn(), + }, }); const createStartContract = (): ManagementStart => ({ diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 1f96ec87171c5c..3289b2f6f5446a 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; +import type { SharePluginSetup, SharePluginStart } from 'src/plugins/share/public'; import { ManagementSetup, ManagementStart } from './types'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; import { @@ -24,6 +25,7 @@ import { } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; +import { ManagementAppLocator } from '../common/locator'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -32,9 +34,21 @@ import { ManagementSection } from './utils'; interface ManagementSetupDependencies { home?: HomePublicPluginSetup; + share: SharePluginSetup; } -export class ManagementPlugin implements Plugin { +interface ManagementStartDependencies { + share: SharePluginStart; +} + +export class ManagementPlugin + implements + Plugin< + ManagementSetup, + ManagementStart, + ManagementSetupDependencies, + ManagementStartDependencies + > { private readonly managementSections = new ManagementSectionsService(); private readonly appUpdater = new BehaviorSubject(() => { @@ -58,8 +72,9 @@ export class ManagementPlugin implements Plugin; } export interface DefinedSections { diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts index 5bb6a14e0b4504..349cab6206babc 100644 --- a/src/plugins/management/server/plugin.ts +++ b/src/plugins/management/server/plugin.ts @@ -7,21 +7,37 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { LocatorPublic } from 'src/plugins/share/common'; +import type { SharePluginSetup } from 'src/plugins/share/server'; +import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator'; import { capabilitiesProvider } from './capabilities_provider'; -export class ManagementServerPlugin implements Plugin { +interface ManagementSetupDependencies { + share: SharePluginSetup; +} + +export interface ManagementSetup { + locator: LocatorPublic; +} + +export class ManagementServerPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { share }: ManagementSetupDependencies) { this.logger.debug('management: Setup'); + const locator = share.url.locators.create(new ManagementAppLocator()); + core.capabilities.registerProvider(capabilitiesProvider); - return {}; + return { + locator, + }; } public start(core: CoreStart) { diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts new file mode 100644 index 00000000000000..8b5d8d45571942 --- /dev/null +++ b/src/plugins/share/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { LocatorDefinition, LocatorPublic } from './url_service'; diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 46fad0dee13b01..d13bb15f8c72ca 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -7,10 +7,12 @@ */ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; +export { LocatorDefinition } from '../common/url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; export { SharePluginSetup, SharePluginStart } from './plugin'; + export { ShareContext, ShareMenuProvider, diff --git a/src/plugins/share/server/index.ts b/src/plugins/share/server/index.ts index d1a0ed1f016f03..d820a362131a49 100644 --- a/src/plugins/share/server/index.ts +++ b/src/plugins/share/server/index.ts @@ -9,6 +9,8 @@ import { PluginInitializerContext } from '../../../core/server'; import { SharePlugin } from './plugin'; +export { SharePluginSetup, SharePluginStart } from './plugin'; + export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts index b21897377d5eb2..e969c8fcd0dbc8 100644 --- a/x-pack/plugins/security/public/management/management_service.test.ts +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -40,6 +40,7 @@ describe('ManagementService', () => { security: mockSection, } as DefinedSections, }, + locator: {} as any, }; const service = new ManagementService(); @@ -101,6 +102,7 @@ describe('ManagementService', () => { security: mockSection, } as DefinedSections, }, + locator: {} as any, }; service.setup({ From 08aba7eb7d80e87ecbaf1de86de11330080e6dcf Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Mon, 14 Jun 2021 14:33:34 +0200 Subject: [PATCH 56/99] =?UTF-8?q?Upgrade=20`polished`=20dependency=20(`1.9?= =?UTF-8?q?.2`=20=E2=86=92=20`3.7.2`,=20`4.0.5`=20=E2=86=92=20`4.1.3`).=20?= =?UTF-8?q?(#101719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 38 +++++++++++++------------------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index cf6bd407d53a43..b4f9109503261c 100644 --- a/package.json +++ b/package.json @@ -318,7 +318,7 @@ "pegjs": "0.10.0", "pluralize": "3.1.0", "pngjs": "^3.4.0", - "polished": "^1.9.2", + "polished": "^3.7.2", "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "proxy-from-env": "1.0.0", diff --git a/yarn.lock b/yarn.lock index bd34c0c4cb4b85..9543f209d6849c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1147,17 +1147,10 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.13.17": - version "7.13.17" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec" - integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.5.tgz#665450911c6031af38f81db530f387ec04cd9a98" + integrity sha512-121rumjddw9c3NCQ55KGkyE1h/nzWhU/owjhw0l4mQrkzz4x9SGS1X8gFLraHwX7td3Yo4QTL+qj0NcIzN87BA== dependencies: regenerator-runtime "^0.13.4" @@ -21583,24 +21576,19 @@ point-in-polygon@^1.0.1: resolved "https://registry.yarnpkg.com/point-in-polygon/-/point-in-polygon-1.0.1.tgz#d59b64e8fee41c49458aac82b56718c5957b2af7" integrity sha1-1Ztk6P7kHElFiqyCtWcYxZV7Kvc= -polished@^1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/polished/-/polished-1.9.2.tgz#d705cac66f3a3ed1bd38aad863e2c1e269baf6b6" - integrity sha512-mPocQrVUSiqQdHNZFGL1iHJmsR/etiv05Nf2oZUbya+GMsQkZVEBl5wonN+Sr/e9zQBEhT6yrMjxAUJ06eyocQ== - -polished@^3.4.4: - version "3.6.5" - resolved "https://registry.yarnpkg.com/polished/-/polished-3.6.5.tgz#dbefdde64c675935ec55119fe2a2ab627ca82e9c" - integrity sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ== +polished@^3.4.4, polished@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/polished/-/polished-3.7.2.tgz#ec5ddc17a7d322a574d5e10ddd2a6f01d3e767d1" + integrity sha512-pQKtpZGmsZrW8UUpQMAnR7s3ppHeMQVNyMDKtUyKwuvDmklzcEyM5Kllb3JyE/sE/x7arDmyd35i+4vp99H6sQ== dependencies: - "@babel/runtime" "^7.9.2" + "@babel/runtime" "^7.12.5" polished@^4.0.5: - version "4.1.2" - resolved "https://registry.yarnpkg.com/polished/-/polished-4.1.2.tgz#c04fcc203e287e2d866e9cfcaf102dae1c01a816" - integrity sha512-jq4t3PJUpVRcveC53nnbEX35VyQI05x3tniwp26WFdm1dwaNUBHAi5awa/roBlwQxx1uRhwNSYeAi/aMbfiJCQ== + version "4.1.3" + resolved "https://registry.yarnpkg.com/polished/-/polished-4.1.3.tgz#7a3abf2972364e7d97770b827eec9a9e64002cfc" + integrity sha512-ocPAcVBUOryJEKe0z2KLd1l9EBa1r5mSwlKpExmrLzsnIzJo4axsoU9O2BjOTkDGDT4mZ0WFE5XKTlR3nLnZOA== dependencies: - "@babel/runtime" "^7.13.17" + "@babel/runtime" "^7.14.0" popper.js@^1.14.4: version "1.15.0" From f4916f429290aee62e32e9995d33665b3a38b213 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 14 Jun 2021 14:47:58 +0200 Subject: [PATCH 57/99] move the example app to be mounted in the developerExamples plugin instead of visible in navbar (#101464) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- examples/screenshot_mode_example/kibana.json | 2 +- .../screenshot_mode_example/public/plugin.ts | 18 ++++++++++++++++-- .../screenshot_mode_example/public/types.ts | 2 ++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/examples/screenshot_mode_example/kibana.json b/examples/screenshot_mode_example/kibana.json index 4cb8c1a1393fbd..28e5b39e5337f9 100644 --- a/examples/screenshot_mode_example/kibana.json +++ b/examples/screenshot_mode_example/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["navigation", "screenshotMode", "usageCollection"], + "requiredPlugins": ["navigation", "screenshotMode", "usageCollection", "developerExamples"], "optionalPlugins": [] } diff --git a/examples/screenshot_mode_example/public/plugin.ts b/examples/screenshot_mode_example/public/plugin.ts index 91bcc2410b5fc5..4108924ca3b8d8 100644 --- a/examples/screenshot_mode_example/public/plugin.ts +++ b/examples/screenshot_mode_example/public/plugin.ts @@ -6,7 +6,13 @@ * Side Public License, v 1. */ -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + AppNavLinkStatus, +} from '../../../src/core/public'; import { AppPluginSetupDependencies, AppPluginStartDependencies } from './types'; import { MetricsTracking } from './services'; import { PLUGIN_NAME } from '../common'; @@ -15,7 +21,7 @@ export class ScreenshotModeExamplePlugin implements Plugin { uiTracking = new MetricsTracking(); public setup(core: CoreSetup, depsSetup: AppPluginSetupDependencies): void { - const { screenshotMode, usageCollection } = depsSetup; + const { screenshotMode, usageCollection, developerExamples } = depsSetup; const isScreenshotMode = screenshotMode.isScreenshotMode(); this.uiTracking.setup({ @@ -27,6 +33,7 @@ export class ScreenshotModeExamplePlugin implements Plugin { core.application.register({ id: 'screenshotModeExample', title: PLUGIN_NAME, + navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { // Load application bundle const { renderApp } = await import('./application'); @@ -40,6 +47,13 @@ export class ScreenshotModeExamplePlugin implements Plugin { return renderApp(coreStart, depsSetup, depsStart as AppPluginStartDependencies, params); }, }); + + developerExamples.register({ + appId: 'screenshotModeExample', + title: 'Screenshot mode integration', + description: + 'Demonstrate how a plugin can adapt appearance based on whether we are in screenshot mode', + }); } public start(core: CoreStart): void {} diff --git a/examples/screenshot_mode_example/public/types.ts b/examples/screenshot_mode_example/public/types.ts index 88812a4a507c91..2eb9bd8e144a0f 100644 --- a/examples/screenshot_mode_example/public/types.ts +++ b/examples/screenshot_mode_example/public/types.ts @@ -9,10 +9,12 @@ import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/public'; import { UsageCollectionSetup } from '../../../src/plugins/usage_collection/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; export interface AppPluginSetupDependencies { usageCollection: UsageCollectionSetup; screenshotMode: ScreenshotModePluginSetup; + developerExamples: DeveloperExamplesSetup; } export interface AppPluginStartDependencies { From daa3f62cda14736e9349250e94457ab24a8043b8 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 14 Jun 2021 13:52:30 +0100 Subject: [PATCH 58/99] [Task Manager] adds capacity estimation to the TM health endpoint (#100475) Adds Capacity Estimation to the Task Manager Health Endpoint. Below is a diagram depicting what information we use to estimate the varying capacity variables. Please use the user facing docs to understand how it fits together. If the docs aren't clear enough - make a review comment and I'll clarify in the docs. --- .../task-manager-health-monitoring.asciidoc | 8 + ...manager-production-considerations.asciidoc | 40 +- .../task-manager-troubleshooting.asciidoc | 261 ++++- .../monitoring/capacity_estimation.test.ts | 939 ++++++++++++++++++ .../server/monitoring/capacity_estimation.ts | 253 +++++ .../monitoring/monitoring_stats_stream.ts | 62 +- .../monitoring/task_run_statistics.test.ts | 143 +++ .../server/monitoring/task_run_statistics.ts | 65 +- .../monitoring/workload_statistics.test.ts | 140 +++ .../server/monitoring/workload_statistics.ts | 109 +- .../task_manager/server/routes/health.test.ts | 247 ++--- .../test_suites/task_manager/health_route.ts | 65 ++ 12 files changed, 2152 insertions(+), 180 deletions(-) create mode 100644 x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts create mode 100644 x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts diff --git a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc index d6b90a4f19e112..8f2c8d106c77cf 100644 --- a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc +++ b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc @@ -92,10 +92,18 @@ a| Runtime | This section tracks excution performance of Task Manager, tracking task _drift_, worker _load_, and execution stats broken down by type, including duration and execution results. +a| Capacity Estimation + +| This section provides a rough estimate about the sufficiency of its capacity. As the name suggests, these are estimates based on historical data and should not be used as predictions. Use these estimations when following the Task Manager <>. + |=== Each section has a `timestamp` and a `status` that indicates when the last update to this section took place and whether the health of this section was evaluated as `OK`, `Warning` or `Error`. The root `status` indicates the `status` of the system overall. +The Runtime `status` indicates whether task executions have exceeded any of the <>. An `OK` status means none of the threshold have been exceeded. A `Warning` status means that at least one warning threshold has been exceeded. An `Error` status means that at least one error threshold has been exceeded. + +The Capacity Estimation `status` indicates the sufficiency of the observed capacity. An `OK` status means capacity is sufficient. A `Warning` status means that capacity is sufficient for the scheduled recurring tasks, but non-recurring tasks often cause the cluster to exceed capacity. An `Error` status means that there is insufficient capacity across all types of tasks. + By monitoring the `status` of the system overall, and the `status` of specific task types of interest, you can evaluate the health of the {kib} Task Management system. diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 606f113b2274f1..17eae59ff2f9c2 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -68,11 +68,7 @@ This means that you can expect a single {kib} instance to support up to 200 _tas In practice, a {kib} instance will only achieve the upper bound of `200/tpm` if the duration of task execution is below the polling rate of 3 seconds. For the most part, the duration of tasks is below that threshold, but it can vary greatly as {es} and {kib} usage grow and task complexity increases (such as alerts executing heavy queries across large datasets). -By <>, you can make a rough estimate as to the required throughput as a _tasks per minute_ measurement. - -For example, suppose your current workload reveals a required throughput of `440/tpm`. You can address this scale by provisioning 3 {kib} instances, with an upper throughput of `600/tpm`. This scale would provide aproximately 25% additional capacity to handle ad-hoc non-recurring tasks and potential growth in recurring tasks. - -It is highly recommended that you maintain at least 20% additional capacity, beyond your expected workload, as spikes in ad-hoc tasks is possible at times of high activity (such as a spike in actions in response to an active alert). +By <>, you can estimate the number of {kib} instances required to reliably execute tasks in a timely manner. An appropriate number of {kib} instances can be estimated to match the required scale. For details on monitoring the health of {kib} Task Manager, follow the guidance in <>. @@ -126,6 +122,35 @@ Throughput is best thought of as a measurements in tasks per minute. A default {kib} instance can support up to `200/tpm`. +[float] +===== Automatic estimation + +experimental[] + +As demonstrated in <>, the Task Manager <> performs these estimations automatically. + +These estimates are based on historical data and should not be used as predictions, but can be used as a rough guide when scaling the system. + +We recommend provisioning enough {kib} instances to ensure a buffer between the observed maximum throughput (as estimated under `observed.max_throughput_per_minute`) and the average required throughput (as estimated under `observed.avg_required_throughput_per_minute`). Otherwise there might be insufficient capacity to handle spikes of ad-hoc tasks. How much of a buffer is needed largely depends on your use case, but keep in mind that estimated throughput takes into account recent spikes and, as long as they are representative of your system's behaviour, shouldn't require much of a buffer. + +We recommend provisioning at least as many {kib} instances as proposed by `proposed.provisioned_kibana`, but keep in mind that this number is based on the estimated required throughput, which is based on average historical performance, and cannot accurately predict future requirements. + +[WARNING] +============================================================================ +Automatic capacity estimation is performed by each {kib} instance independently. This estimation is performed by observing the task throughput in that instance, the number of {kib} instances executing tasks at that moment in time, and the recurring workload in {es}. + +If a {kib} instance is idle at the moment of capacity estimation, the number of active {kib} instances might be miscounted and the available throughput miscalculated. + +When evaluating the proposed {kib} instance number under `proposed.provisioned_kibana`, we highly recommend verifying that the `observed.observed_kibana_instances` matches the number of provisioned {kib} instances. +============================================================================ + +[float] +===== Manual estimation + +By <>, you can make a rough estimate as to the required throughput as a _tasks per minute_ measurement. + +For example, suppose your current workload reveals a required throughput of `440/tpm`. You can address this scale by provisioning 3 {kib} instances, with an upper throughput of `600/tpm`. This scale would provide aproximately 25% additional capacity to handle ad-hoc non-recurring tasks and potential growth in recurring tasks. + Given a deployment of 100 recurring tasks, estimating the required throughput depends on the scheduled cadence. Suppose you expect to run 50 tasks at a cadence of `10s`, the other 50 tasks at `20m`. In addition, you expect a couple dozen non-recurring tasks every minute. @@ -136,8 +161,11 @@ A recurring task requires as many executions as its cadence can fit in a minute. For this reason, we recommend grouping tasks by _tasks per minute_ and _tasks per hour_, as demonstrated in <>, averaging the _per hour_ measurement across all minutes. +It is highly recommended that you maintain at least 20% additional capacity, beyond your expected workload, as spikes in ad-hoc tasks is possible at times of high activity (such as a spike in actions in response to an active alert). + Given the predicted workload, you can estimate a lower bound throughput of `340/tpm` (`6/tpm` * 50 + `3/tph` * 50 + 20% buffer). As a default, a {kib} instance provides a throughput of `200/tpm`. A good starting point for your deployment is to provision 2 {kib} instances. You could then monitor their performance and reassess as the required throughput becomes clearer. Although this is a _rough_ estimate, the _tasks per minute_ provides the lower bound needed to execute tasks on time. -Once you calculate the rough _tasks per minute_ estimate, add a 20% buffer for non-recurring tasks. How much of a buffer is required largely depends on your use case, so <> as it grows to ensure enough of a buffer is provisioned. + +Once you estimate _tasks per minute_ , add a buffer for non-recurring tasks. How much of a buffer is required largely depends on your use case. Ensure enough of a buffer is provisioned by <> as it grows and tracking the ratio of recurring to non-recurring tasks by <>. diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc index c6a7b7f3d53fdc..4b63313b2b96e0 100644 --- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc +++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc @@ -74,6 +74,7 @@ By analyzing the different sections of the output, you can evaluate different th ** <> ** <> * <> +* <> Retrieve the latest monitored health stats of a {kib} instance Task Manager: @@ -178,6 +179,11 @@ The API returns the following: "p99": 166 } }, + "persistence": { + "recurring": 88, + "non_recurring": 4, + "ephemeral": 8 + }, "result_frequency_percent_as_number": { "alerting:.index-threshold": { "Success": 100, @@ -233,12 +239,44 @@ The API returns the following: ["1m", 2], ["60s", 2], ["5m", 2], - ["60m", 4] + ["60m", 4], + ["3600s", 1], + ["720m", 1] ], - "overdue": 0, - "estimated_schedule_density": [0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0] + "non_recurring": 18, + "owner_ids": 0, + "overdue": 10, + "overdue_non_recurring": 10, + "estimated_schedule_density": [0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0], + "capacity_requirments": { + "per_minute": 6, + "per_hour": 28, + "per_day": 2 + } }, "status": "OK" + }, + "capacity_estimation": { + "timestamp": "2021-02-16T11:38:06.826Z", + "value": { + "observed": { + "observed_kibana_instances": 1, + "max_throughput_per_minute_per_kibana": 200, + "max_throughput_per_minute": 200, + "minutes_to_drain_overdue": 1, + "avg_recurring_required_throughput_per_minute": 28, + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute": 28, + "avg_required_throughput_per_minute_per_kibana": 28 + }, + "proposed": { + "min_required_kibana": 1, + "provisioned_kibana": 1, + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute_per_kibana": 28 + } + } + "status": "OK" } } } @@ -530,7 +568,7 @@ Evaluating the health stats in this hypothetical scenario, you see the following You can infer from these stats that the high drift the Task Manager is experiencing is most likely due to Elasticsearch query alerts that are running for a long time. Resolving this issue is context dependent and changes from case to case. -In the preceding example above, this would be resolved by modifying the queries in these alerts to make them faster, or improving the {es} throughput to speed up the exiting query. +In the preceding example, this would be resolved by modifying the queries in these alerts to make them faster, or improving the {es} throughput to speed up the exiting query. [[task-manager-theory-high-fail-rate]] *Theory*: @@ -571,6 +609,82 @@ Evaluating the preceding health stats, you see the following output under `stats You can infer from these stats that most `actions:.index` tasks, which back the ES Index {kib} action, fail. Resolving that would require deeper investigation into the {kib} Server Log, where the exact errors are logged, and addressing these specific errors. +[[task-manager-theory-spikes-in-non-recurring-tasks]] +*Theory*: +Spikes in non-recurring and ephemeral tasks are consuming a high percentage of the available capacity + +*Diagnosis*: +Task Manager uses ad-hoc non-recurring tasks to load balance operations across multiple {kib} instances. +Additionally, {kib} can use Task Manager to allocate resources for expensive operations by executing an ephemeral task. Ephemeral tasks are identical in operation to non-recurring tasks, but are not persisted and cannot be load balanced across {kib} instances. + +Evaluating the preceding health stats, you see the following output under `stats.runtime.value.execution.persistence`: + +[source,json] +-------------------------------------------------- +{ + "recurring": 88, # <1> + "non_recurring": 4, # <2> + "ephemeral": 8 # <3> +}, +-------------------------------------------------- +<1> 88% of executed tasks are recurring tasks +<2> 4% of executed tasks are non-recurring tasks +<3> 8% of executed tasks are ephemeral tasks + +You can infer from these stats that the majority of executions consist of recurring tasks at 88%. +You can use the `execution.persistence` stats to evaluate the ratio of consumed capacity, but on their own, you should not make assumptions about the sufficiency of the available capacity. + +To assess the capacity, you should evaluate these stats against the `load` under `stats.runtime.value`: + +[source,json] +-------------------------------------------------- +{ + "load": { # <2> + "p50": 40, + "p90": 40, + "p95": 60, + "p99": 80 + } +} +-------------------------------------------------- + +You can infer from these stats that it is very unusual for Task Manager to run out of capacity, so the capacity is likely sufficient to handle the amount of non-recurring and ephemeral tasks. + +Suppose you have an alternate scenario, where you see the following output under `stats.runtime.value.execution.persistence`: + +[source,json] +-------------------------------------------------- +{ + "recurring": 60, # <1> + "non_recurring": 30, # <2> + "ephemeral": 10 # <3> +}, +-------------------------------------------------- +<1> 60% of executed tasks are recurring tasks +<2> 30% of executed tasks are non-recurring tasks +<3> 10% of executed tasks are ephemeral tasks + +You can infer from these stats that even though most executions are recurring tasks, a substantial percentage of executions are non-recurring and ephemeral tasks at 40%. + +Evaluating the `load` under `stats.runtime.value`, you see the following: + +[source,json] +-------------------------------------------------- +{ + "load": { # <2> + "p50": 70, + "p90": 100, + "p95": 100, + "p99": 100 + } +} +-------------------------------------------------- + +You can infer from these stats that it is quite common for this {kib} instance to run out of capacity. +Given the high rate of non-recurring and ephemeral tasks, it would be reasonable to assess that there is insufficient capacity in the {kib} cluster to handle the amount of tasks. + +Keep in mind that these stats give you a glimpse at a moment in time, and even though there has been insufficient capacity in recent minutes, this might not be true in other times where fewer non-recurring or ephemeral tasks are used. We recommend tracking these stats over time and identifying the source of these tasks before making sweeping changes to your infrastructure. + [[task-manager-health-evaluate-the-workload]] ===== Evaluate the Workload @@ -579,7 +693,7 @@ Predicting the required throughput a deplyment might need to support Task Manage <> provides statistics that make it easier to monitor the adequacy of the existing throughput. By evaluating the workload, the required throughput can be estimated, which is used when following the Task Manager <>. -Evaluating the preceding health stats above, you see the following output under `stats.workload.value`: +Evaluating the preceding health stats in the previous example, you see the following output under `stats.workload.value`: [source,json] -------------------------------------------------- @@ -607,27 +721,39 @@ Evaluating the preceding health stats above, you see the following output under } }, }, - "schedule": [ # <4> + "non_recurring": 0, # <4> + "owner_ids": 1, # <5> + "schedule": [ # <6> ["10s", 2], ["1m", 2], ["90s", 2], ["5m", 8] ], - "overdue": 0, # <5> - "estimated_schedule_density": [ # <6> + "overdue_non_recurring": 0, # <7> + "overdue": 0, # <8> + "estimated_schedule_density": [ # <9> 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0 - ] + ], + "capacity_requirments": { # <10> + "per_minute": 14, + "per_hour": 240, + "per_day": 0 + } } -------------------------------------------------- <1> There are 26 tasks in the system, including regular tasks, recurring tasks, and failed tasks. <2> There are 2 `idle` index threshold alert tasks, meaning they are scheduled to run at some point in the future. <3> Of the 14 tasks backing the ES index action, 10 have failed and 2 are running. -<4> A histogram of all scheduled recurring tasks shows that 2 tasks are scheduled to run every 10 seconds, 2 tasks are scheduled to run once a minute, and so on. -<5> There are no tasks overdue, which means that all tasks that *should* have run by now *have* run. -<6> This histogram shows the tasks scheduled to run throughout the upcoming 20 polling cycles. The histogram represents the entire deployment, rather than just this {kib} instance +<4> There are no non-recurring tasks in the queue. +<5> There is one Task Manager actively executing tasks. There might be additional idle Task Managers, but they aren't actively executing tasks at this moment in time. +<6> A histogram of all scheduled recurring tasks shows that 2 tasks are scheduled to run every 10 seconds, 2 tasks are scheduled to run once a minute, and so on. +<7> There are no overdue non-recurring tasks. Non-recurring tasks are usually scheduled to execute immediately, so overdue non-recurring tasks are often a symptom of a congested system. +<8> There are no overdue tasks, which means that all tasks that *should* have run by now *have* run. +<9> This histogram shows the tasks scheduled to run throughout the upcoming 20 polling cycles. The histogram represents the entire deployment, rather than just this {kib} instance. +<10> The capacity required to handle the recurring tasks in the system. These are buckets, rather than aggregated sums, and we recommend <> section, rather than evaluating these buckets yourself. The `workload` section summarizes the work load across the cluster, listing the tasks in the system, their types, schedules, and current status. @@ -674,6 +800,8 @@ Suppose the output of `stats.workload.value` looked something like this: } }, }, + "non_recurring": 0, + "owner_ids": 1, "schedule": [ # <2> ["10s", 38], ["1m", 101], @@ -683,32 +811,133 @@ Suppose the output of `stats.workload.value` looked something like this: ["60m", 106], ["1d", 61] ], + "overdue_non_recurring": 0, "overdue": 0, # <5> "estimated_schedule_density": [ # <3> 10, 1, 0, 10, 0, 20, 0, 1, 0, 1, 9, 0, 3, 10, 0, 0, 10, 10, 7, 0, 0, 31, 0, 12, 16, 31, 0, 10, 0, 10, 3, 22, 0, 10, 0, 2, 10, 10, 1, 0 - ] + ], + "capacity_requirments": { + "per_minute": 329, # <4> + "per_hour": 4272, # <5> + "per_day": 61 # <6> + } } -------------------------------------------------- <1> There are 2,191 tasks in the system. <2> The scheduled tasks are distributed across a variety of cadences. <3> The schedule density shows that you expect to exceed the default 10 concurrent tasks. +<4> There are 329 task executions that recur within the space of every minute. +<5> There are 4,273 task executions that recur within the space of every hour. +<6> There are 61 task executions that recur within the space of every day. You can infer several important attributes of your workload from this output: * There are many tasks in your system and ensuring these tasks run on their scheduled cadence will require attention to the Task Manager throughput. -* Assessing the high frequency tasks (tasks that recur at a cadence of a couple of minutes or less), you must support a throughput of approximately 400 tasks per minute (38 every 10 seconds + 101 every minute + 55 every 90 seconds). -* Assessing the medium frequency tasks (tasks that recur at a cadence of an hour or less), you must support an additional throughput of over 2000 tasks per hour (89 every 5 minutes, + 62 every 20 minutes + 106 each hour). You can average the needed throughput for the hour by counting these tasks as an additional 30 to 40 tasks per minute. +* Assessing the high frequency tasks (tasks that recur at a cadence of a couple of minutes or less), you must support a throughput of approximately 330 task executions per minute (38 every 10 seconds + 101 every minute). +* Assessing the medium frequency tasks (tasks that recur at a cadence of an hour or less), you must support an additional throughput of over 4,272 task executions per hour (55 every 90 seconds + 89 every 5 minutes, + 62 every 20 minutes + 106 each hour). You can average the needed throughput for the hour by counting these tasks as an additional 70 - 80 tasks per minute. * Assessing the estimated schedule density, there are cycles that are due to run upwards of 31 tasks concurrently, and along side these cycles, there are empty cycles. You can expect Task Manager to load balance these tasks throughout the empty cycles, but this won't leave much capacity to handle spikes in fresh tasks that might be scheduled in the future. -These rough calculations give you a lower bound to the required throughput, which is _at least_ 440 tasks per minute to ensure recurring tasks are executed, at their scheduled time. This throughput doesn't account for nonrecurring tasks that might have been scheduled, nor does it account for tasks (recurring or otherwise) that might be scheduled in the future. +These rough calculations give you a lower bound to the required throughput, which is _at least_ 410 tasks per minute to ensure recurring tasks are executed, at their scheduled time. This throughput doesn't account for nonrecurring tasks that might have been scheduled, nor does it account for tasks (recurring or otherwise) that might be scheduled in the future. Given these inferred attributes, it would be safe to assume that a single {kib} instance with default settings **would not** provide the required throughput. It is possible that scaling horizontally by adding a couple more {kib} instances will. For details on scaling Task Manager, see <>. + +[[task-manager-health-evaluate-the-capacity-estimation]] +===== Evaluate the Capacity Estimation + +Task Manager is constantly evaluating its runtime operations and workload. This enables Task Manager to make rough estimates about the sufficiency of its capacity. + +As the name suggests, these are estimates based on historical data and should not be used as predictions. These estimations should be evaluated alongside the detailed <> stats before making changes to infrastructure. These estimations assume all {kib} instances are configured identically. + +We recommend using these estimations when following the Task Manager <>. + +Evaluating the health stats in the previous example, you can see the following output under `stats.capacity_estimation.value`: + +[source,json] +-------------------------------------------------- +{ + "observed": { + "observed_kibana_instances": 1, # <1> + "minutes_to_drain_overdue": 1, # <2> + "max_throughput_per_minute_per_kibana": 200, + "max_throughput_per_minute": 200, # <3> + "avg_recurring_required_throughput_per_minute": 28, # <4> + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute": 28, # <5> + "avg_required_throughput_per_minute_per_kibana": 28 + }, + "proposed": { + "min_required_kibana": 1, # <6> + "provisioned_kibana": 1, # <7> + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute_per_kibana": 28 + } +} +-------------------------------------------------- +<1> These estimates assume that there is one {kib} instance actively executing tasks. +<2> Based on past throughput the overdue tasks in the system could be executed within 1 minute. +<3> Assuming all {kib} instances in the cluster are configured the same as this instance, the maximum available throughput is 200 tasks per minute. +<4> On average, the recurring tasks in the system have historically required a throughput of 28 tasks per minute. +<5> On average, regardless of whether they are recurring or otherwise, the tasks in the system have historically required a throughput of 28 tasks per minute. +<6> One {kib} instance should be sufficient to run the current recurring workload. +<7> We propose waiting for the workload to change before additional {kib} instances are provisioned. + +The `capacity_estimation` section is made up of two subsections: + +* `observed` estimates the current capacity by observing historical runtime and workload statistics +* `proposed` estimates the baseline {kib} cluster size and the expected throughput under such a deployment strategy + +You can infer from these estimates that the current system is under-utilized and has enough capacity to handle many more tasks than it currently does. + +Suppose an alternate scenario, where you see the following output under `stats.capacity_estimation.value`: + +[source,json] +-------------------------------------------------- +{ + "observed": { + "observed_kibana_instances": 2, # <1> + "max_throughput_per_minute_per_kibana": 200, + "max_throughput_per_minute": 400, # <2> + "minutes_to_drain_overdue": 12, # <3> + "avg_recurring_required_throughput_per_minute": 354, # <4> + "avg_recurring_required_throughput_per_minute_per_kibana": 177, # <5> + "avg_required_throughput_per_minute": 434, # <6> + "avg_required_throughput_per_minute_per_kibana": 217 + }, + "proposed": { + "min_required_kibana": 2, # <7> + "provisioned_kibana": 3, # <8> + "avg_recurring_required_throughput_per_minute_per_kibana": 118, # <9> + "avg_required_throughput_per_minute_per_kibana": 145 # <10> + } +} +-------------------------------------------------- +<1> These estimates assume that there are two {kib} instance actively executing tasks. +<2> The maximum available throughput in the system currently is 400 tasks per minute. +<3> Based on past throughput the overdue tasks in the system should be executed within 12 minutes. +<4> On average, the recurring tasks in the system have historically required a throughput of 354 tasks per minute. +<5> On average, each {kib} instance utilizes 177 tasks per minute of its capacity to execute recurring tasks. +<6> On average the tasks in the system have historically required a throughput of 434 tasks per minute. +<7> The system estimates that at least two {kib} instances are required to run the current recurring workload. +<8> The system recommends provisioning three {kib} instances to handle the workload. +<9> Once a third {kib} instance is provisioned, the capacity utilized by each instance to execute recurring tasks should drop from 177 to 118 tasks per minute. +<10> Taking into account historical ad-hoc task execution, we estimate the throughput required of each {kib} instance will drop from 217 task per minute to 145, once a third {kib} instance is provisioned. + +Evaluating by these estimates, we can infer some interesting attributes of our system: + +* These estimates are produced based on the assumption that there are two {kib} instances in the cluster. This number is based on the number of {kib} instances actively executing tasks in recent minutes. At times this number might fluctuate if {kib} instances remain idle, so validating these estimates against what you know about the system is recommended. +* There appear to be so many overdue tasks that it would take 12 minutes of executions to catch up with that backlog. This does not take into account tasks that might become overdue during those 12 minutes. Although this congestion might be temporary, the system could also remain consistently under provisioned and might never drain the backlog entirely. +* Evaluating the recurring tasks in the workload, the system requires a throughput of 354 tasks per minute on average to execute tasks on time, which is lower then the estimated maximum throughput of 400 tasks per minute. Once we take into account historical throughput though, we estimate the required throughput at 434 tasks per minute. This suggests that, historically, approximately 20% of tasks have been ad-hoc non-recurring tasks, the scale of which are harder to predict than recurring tasks. + +You can infer from these estimates that the capacity in the current system is insufficient and at least one additional {kib} instance is required to keep up with the workload. + +For details on scaling Task Manager, see <>. + [float] [[task-manager-cannot-operate-when-inline-scripts-are-disabled]] ==== Inline scripts are disabled in {es} diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts new file mode 100644 index 00000000000000..c68e307dbec03f --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.test.ts @@ -0,0 +1,939 @@ +/* + * Copyright 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 { CapacityEstimationParams, estimateCapacity } from './capacity_estimation'; +import { HealthStatus, RawMonitoringStats } from './monitoring_stats_stream'; + +describe('estimateCapacity', () => { + test('estimates the max throughput per minute based on the workload and the assumed kibana instances', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 60, + per_hour: 0, + per_day: 0, + }, + }, + { + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 0, + recurring: 100, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: 1, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: 200, + }); + }); + + test('reduces the available capacity per kibana when average task duration exceeds the poll interval', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 60, + per_hour: 0, + per_day: 0, + }, + }, + { + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 2400, + p90: 2500, + p95: 3200, + p99: 3500, + }, + non_recurring: { + p50: 1400, + p90: 1500, + p95: 2200, + p99: 3500, + }, + recurring: { + p50: 8345, + p90: 140651.5, + p95: 249199, + p99: 253150, + }, + }, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 0, + recurring: 100, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: 1, + minutes_to_drain_overdue: 0, + // on average it takes at least 50% of tasks 2 polling cycles before they complete + // this reduces the overall throughput of task manager + max_throughput_per_minute: 100, + }); + }); + + test('estimates the max throughput per minute when duration by persistence is empty', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 60, + per_hour: 0, + per_day: 0, + }, + }, + { + execution: { + duration: {}, + duration_by_persistence: {}, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 0, + recurring: 100, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: 1, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: 200, + }); + }); + + test('estimates the max throughput per minute based on the workload and the assumed kibana instances when there are tasks that repeat each hour or day', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 0, + per_hour: 12000, + per_day: 200, + }, + }, + { + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 0, + recurring: 100, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: 1, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: 200, + }); + }); + + test('estimates the max throughput available when there are no active Kibana', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + // 0 active tasks at this moment in time, so no owners identifiable + owner_ids: 0, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 60, + per_hour: 0, + per_day: 0, + }, + }, + { + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 0, + recurring: 100, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: 1, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: 200, + }); + }); + + test('estimates the max throughput available to handle the workload when there are multiple active kibana instances', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 3, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 150, + per_hour: 60, + per_day: 0, + }, + }, + { + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 0, + recurring: 100, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: 3, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: 3 * 200, // 3 kibana, 200tpm each + avg_required_throughput_per_minute: 150 + 1, // 150 every minute, plus 60 every hour + avg_required_throughput_per_minute_per_kibana: Math.ceil((150 + 1) / 3), + }); + }); + + test('estimates the max throughput available to handle the workload and historical non-recurring tasks when there are multiple active kibana instances', async () => { + const provisionedKibanaInstances = 2; + // 50% for non-recurring/epehemral + a 3rd of recurring task workload + const expectedAverageRequiredCapacityPerKibana = 200 * 0.5 + (150 + 1) / 2; + + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: provisionedKibanaInstances, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 150, + per_hour: 60, + per_day: 0, + }, + }, + { + load: { + p50: 40, + // assume running at 100% capacity + p90: 100, + p95: 100, + p99: 100, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + persistence: { + // 50% of tasks are non-recurring/ephemeral executions in the system in recent history + ephemeral: 25, + non_recurring: 25, + recurring: 50, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value.observed + ).toMatchObject({ + observed_kibana_instances: provisionedKibanaInstances, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: provisionedKibanaInstances * 200, // 2 kibana, 200tpm each + avg_required_throughput_per_minute_per_kibana: Math.ceil( + expectedAverageRequiredCapacityPerKibana + ), + avg_required_throughput_per_minute: Math.ceil( + provisionedKibanaInstances * expectedAverageRequiredCapacityPerKibana + ), // same as above but for both instances + }); + }); + + test('estimates the min required kibana instances when there is sufficient capacity for recurring but not for non-recurring/ephemeral', async () => { + const provisionedKibanaInstances = 2; + const recurringTasksPerMinute = 251; + // 50% for non-recurring/epehemral + half of recurring task workload + // there is insufficent capacity for this, but this is what the workload requires of the Kibana instances + const expectedAverageRequiredCapacityPerKibanaCurrently = + 200 * 0.5 + recurringTasksPerMinute / provisionedKibanaInstances; + const expectedAverageRequiredCapacityPerKibanaOnceThereAreEnoughServers = + // the non-recurring task load should now be shared between 3 server instead of 2 + (200 * 0.5 * provisionedKibanaInstances) / (provisionedKibanaInstances + 1) + + // so will the recurring tasks + recurringTasksPerMinute / (provisionedKibanaInstances + 1); + + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: provisionedKibanaInstances, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: recurringTasksPerMinute, + per_hour: 0, + per_day: 0, + }, + }, + { + load: { + p50: 40, + // assume running at 100% capacity + p90: 100, + p95: 100, + p99: 100, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + persistence: { + // 50% of tasks are non-recurring/ephemeral executions in the system in recent history + ephemeral: 25, + non_recurring: 25, + recurring: 50, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ).value + ).toMatchObject({ + observed: { + observed_kibana_instances: provisionedKibanaInstances, + minutes_to_drain_overdue: 0, + max_throughput_per_minute: provisionedKibanaInstances * 200, // 2 kibana, 200tpm each + avg_recurring_required_throughput_per_minute: Math.ceil(recurringTasksPerMinute), + avg_required_throughput_per_minute_per_kibana: Math.ceil( + expectedAverageRequiredCapacityPerKibanaCurrently + ), + avg_required_throughput_per_minute: Math.ceil( + provisionedKibanaInstances * expectedAverageRequiredCapacityPerKibanaCurrently + ), // same as above bt for both instances + }, + proposed: { + provisioned_kibana: provisionedKibanaInstances + 1, + min_required_kibana: provisionedKibanaInstances, + avg_recurring_required_throughput_per_minute_per_kibana: Math.ceil( + recurringTasksPerMinute / (provisionedKibanaInstances + 1) + ), + avg_required_throughput_per_minute_per_kibana: Math.ceil( + expectedAverageRequiredCapacityPerKibanaOnceThereAreEnoughServers + ), + }, + }); + }); + + test('marks estimated capacity as OK state when workload and load suggest capacity is sufficient', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 170, + per_hour: 0, + per_day: 0, + }, + }, + { + load: { + p50: 40, + // as avg p90 load is only 50%, it seems we have sufficient + // capacity, but if we saw a higher load (say 80% here), it would fail + // as status would be Warn (as seen in a previous test) + p90: 50, + p95: 80, + p99: 80, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // 20% average of non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 20, + recurring: 80, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ) + ).toMatchObject({ + status: 'OK', + timestamp: expect.any(String), + value: expect.any(Object), + }); + }); + + test('marks estimated capacity as Warning state when capacity is insufficient for recent spikes of non-recurring workload, but sufficient for the recurring workload', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 175, + per_hour: 0, + per_day: 0, + }, + }, + { + load: { + p50: 40, + p90: 100, + p95: 100, + p99: 100, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // no non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 20, + recurring: 80, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ) + ).toMatchObject({ + status: 'warn', + timestamp: expect.any(String), + value: expect.any(Object), + }); + }); + + test('marks estimated capacity as Error state when workload and load suggest capacity is insufficient', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 210, + per_hour: 0, + per_day: 0, + }, + }, + { + load: { + p50: 80, + p90: 100, + p95: 100, + p99: 100, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + // 20% average of non-recurring executions in the system in recent history + persistence: { + ephemeral: 0, + non_recurring: 20, + recurring: 80, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ) + ).toMatchObject({ + status: 'error', + timestamp: expect.any(String), + value: expect.any(Object), + }); + }); + + test('recommmends a 20% increase in kibana when a spike in non-recurring tasks forces recurring task capacity to zero', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 28, + per_hour: 27, + per_day: 2, + }, + }, + { + load: { + p50: 80, + p90: 100, + p95: 100, + p99: 100, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + persistence: { + recurring: 0, + non_recurring: 70, + ephemeral: 30, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ) + ).toMatchObject({ + status: 'warn', + timestamp: expect.any(String), + value: { + observed: { + observed_kibana_instances: 1, + avg_recurring_required_throughput_per_minute: 29, + // we obesrve 100% capacity on non-recurring/ephemeral tasks, which is 200tpm + // and add to that the 29tpm for recurring tasks + avg_required_throughput_per_minute_per_kibana: 229, + }, + proposed: { + provisioned_kibana: 2, + min_required_kibana: 1, + avg_recurring_required_throughput_per_minute_per_kibana: 15, + // once 2 kibana are provisioned, avg_required_throughput_per_minute_per_kibana is divided by 2, hence 115 + avg_required_throughput_per_minute_per_kibana: 115, + }, + }, + }); + }); + + test('recommmends a 20% increase in kibana when a spike in non-recurring tasks in a system with insufficient capacity even for recurring tasks', async () => { + expect( + estimateCapacity( + mockStats( + { max_workers: 10, poll_interval: 3000 }, + { + owner_ids: 1, + overdue_non_recurring: 0, + capacity_requirments: { + per_minute: 210, + per_hour: 0, + per_day: 0, + }, + }, + { + load: { + p50: 80, + p90: 100, + p95: 100, + p99: 100, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + persistence: { + recurring: 0, + non_recurring: 70, + ephemeral: 30, + }, + result_frequency_percent_as_number: {}, + }, + } + ) + ) + ).toMatchObject({ + status: 'error', + timestamp: expect.any(String), + value: { + observed: { + observed_kibana_instances: 1, + avg_recurring_required_throughput_per_minute: 210, + // we obesrve 100% capacity on non-recurring/ephemeral tasks, which is 200tpm + // and add to that the 210tpm for recurring tasks + avg_required_throughput_per_minute_per_kibana: 410, + }, + proposed: { + // we propose provisioning 3 instances for recurring + non-recurring/ephemeral + provisioned_kibana: 3, + // but need at least 2 for recurring + min_required_kibana: 2, + avg_recurring_required_throughput_per_minute_per_kibana: 210 / 3, + // once 3 kibana are provisioned, avg_required_throughput_per_minute_per_kibana is divided by 3, hence 137 + avg_required_throughput_per_minute_per_kibana: Math.ceil(410 / 3), + }, + }, + }); + }); +}); + +function mockStats( + configuration: Partial['configuration']['value']> = {}, + workload: Partial['workload']['value']> = {}, + runtime: Partial['runtime']['value']> = {} +): CapacityEstimationParams { + return { + configuration: { + status: HealthStatus.OK, + timestamp: new Date().toISOString(), + value: { + max_workers: 0, + poll_interval: 0, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + ...configuration, + }, + }, + workload: { + status: HealthStatus.OK, + timestamp: new Date().toISOString(), + value: { + count: 4, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 1, status: { idle: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + schedule: [], + overdue: 0, + overdue_non_recurring: 0, + estimated_schedule_density: [], + non_recurring: 20, + owner_ids: 2, + capacity_requirments: { + per_minute: 150, + per_hour: 360, + per_day: 820, + }, + ...workload, + }, + }, + runtime: { + status: HealthStatus.OK, + timestamp: new Date().toISOString(), + value: { + drift: { + p50: 4, + p90: 6, + p95: 6, + p99: 6, + }, + drift_by_type: {}, + load: { + p50: 40, + p90: 60, + p95: 60, + p99: 60, + }, + execution: { + duration: {}, + duration_by_persistence: { + ephemeral: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + non_recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + recurring: { + p50: 400, + p90: 500, + p95: 1200, + p99: 1500, + }, + }, + persistence: { + ephemeral: 0, + non_recurring: 30, + recurring: 70, + }, + result_frequency_percent_as_number: {}, + }, + polling: { + last_successful_poll: new Date().toISOString(), + duration: [], + claim_conflicts: [], + claim_mismatches: [], + result_frequency_percent_as_number: [], + }, + ...runtime, + }, + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts new file mode 100644 index 00000000000000..35eb0dfca7a6bc --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapValues } from 'lodash'; +import stats from 'stats-lite'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { RawMonitoringStats, RawMonitoredStat, HealthStatus } from './monitoring_stats_stream'; +import { AveragedStat } from './task_run_calcultors'; +import { TaskPersistenceTypes } from './task_run_statistics'; +import { asErr, asOk, map, Result } from '../lib/result_type'; + +export interface CapacityEstimationStat extends JsonObject { + observed: { + observed_kibana_instances: number; + max_throughput_per_minute: number; + max_throughput_per_minute_per_kibana: number; + minutes_to_drain_overdue: number; + avg_required_throughput_per_minute: number; + avg_required_throughput_per_minute_per_kibana: number; + avg_recurring_required_throughput_per_minute: number; + avg_recurring_required_throughput_per_minute_per_kibana: number; + }; + proposed: { + provisioned_kibana: number; + min_required_kibana: number; + avg_recurring_required_throughput_per_minute_per_kibana: number; + avg_required_throughput_per_minute_per_kibana: number; + }; +} + +export type CapacityEstimationParams = Omit< + Required, + 'capacity_estimation' +>; + +function isCapacityEstimationParams( + capacityStats: RawMonitoringStats['stats'] +): capacityStats is CapacityEstimationParams { + return !!(capacityStats.configuration && capacityStats.runtime && capacityStats.workload); +} + +export function estimateCapacity( + capacityStats: CapacityEstimationParams +): RawMonitoredStat { + const workload = capacityStats.workload.value; + // if there are no active owners right now, assume there's at least 1 + const assumedKibanaInstances = Math.max(workload.owner_ids, 1); + + const { + load: { p90: averageLoadPercentage }, + execution: { duration_by_persistence: durationByPersistence }, + } = capacityStats.runtime.value; + const { + recurring: percentageOfExecutionsUsedByRecurringTasks, + non_recurring: percentageOfExecutionsUsedByNonRecurringTasks, + } = capacityStats.runtime.value.execution.persistence; + const { overdue, capacity_requirments: capacityRequirments } = workload; + const { + poll_interval: pollInterval, + max_workers: maxWorkers, + } = capacityStats.configuration.value; + + /** + * On average, how many polling cycles does it take to execute a task? + * If this is higher than the polling cycle, then a whole cycle is wasted as + * we won't use the worker until the next cycle. + */ + const averagePollIntervalsPerExecution = Math.ceil( + map( + getAverageDuration(durationByPersistence), + (averageDuration) => Math.max(averageDuration, pollInterval), + () => pollInterval + ) / pollInterval + ); + + /** + * Given the current configuration how much task capacity do we have? + */ + const capacityPerMinutePerKibana = Math.round( + ((60 * 1000) / (averagePollIntervalsPerExecution * pollInterval)) * maxWorkers + ); + + /** + * If our assumption about the number of Kibana is correct - how much capacity do we have available? + */ + const assumedCapacityAvailablePerMinute = capacityPerMinutePerKibana * assumedKibanaInstances; + + /** + * assuming this Kibana is representative what capacity has historically been used for the + * different types at busy times (load at p90) + */ + const averageCapacityUsedByPersistedTasksPerKibana = percentageOf( + capacityPerMinutePerKibana, + percentageOf( + averageLoadPercentage, + percentageOfExecutionsUsedByRecurringTasks + percentageOfExecutionsUsedByNonRecurringTasks + ) + ); + /** + * On average, how much of this kibana's capacity has been historically used to execute + * non-recurring and ephemeral tasks + */ + const averageCapacityUsedByNonRecurringAndEphemeralTasksPerKibana = percentageOf( + capacityPerMinutePerKibana, + percentageOf(averageLoadPercentage, 100 - percentageOfExecutionsUsedByRecurringTasks) + ); + + /** + * On average, how much of this kibana's capacity has been historically available + * for recurring tasks + */ + const averageCapacityAvailableForRecurringTasksPerKibana = + capacityPerMinutePerKibana - averageCapacityUsedByNonRecurringAndEphemeralTasksPerKibana; + + /** + * At times a cluster might experience spikes of NonRecurring/Ephemeral tasks which swamp Task Manager + * causing it to spend all its capacity on NonRecurring/Ephemeral tasks, which makes it much harder + * to estimate the required capacity. + * This is easy to identify as load will usually max out or all the workers are busy executing non-recurring + * or ephemeral tasks, and none are running recurring tasks. + */ + const hasTooLittleCapacityToEstimateRequiredNonRecurringCapacity = + averageLoadPercentage === 100 || averageCapacityAvailableForRecurringTasksPerKibana === 0; + + /** + * On average, how many tasks per minute does this cluster need to execute? + */ + const averageRecurringRequiredPerMinute = + capacityRequirments.per_minute + + capacityRequirments.per_hour / 60 + + capacityRequirments.per_day / 24 / 60; + + /** + * how many Kibana are needed solely for the recurring tasks + */ + const minRequiredKibanaInstancesForRecurringTasks = Math.ceil( + averageRecurringRequiredPerMinute / capacityPerMinutePerKibana + ); + + /** + * assuming each kibana only has as much capacity for recurring tasks as this kibana has historically + * had available - how many kibana are needed to handle the current recurring workload? + */ + const minRequiredKibanaInstances = Math.ceil( + hasTooLittleCapacityToEstimateRequiredNonRecurringCapacity + ? /* + if load is at 100% or there's no capacity for recurring tasks at the moment, then it's really difficult for us to assess how + much capacity is needed for non-recurring tasks at normal times. This might be representative, but it might + also be a spike and we have no way of knowing that. We'll recommend people scale up by 20% and go from there. */ + minRequiredKibanaInstancesForRecurringTasks * 1.2 + : averageRecurringRequiredPerMinute / averageCapacityAvailableForRecurringTasksPerKibana + ); + + /** + * Assuming the `minRequiredKibanaInstances` Kibana instances are provisioned - how much + * of their throughput would we expect to be used by the recurring task workload + */ + const averageRecurringRequiredPerMinutePerKibana = + averageRecurringRequiredPerMinute / minRequiredKibanaInstances; + + /** + * assuming the historical capacity needed for ephemeral and non-recurring tasks, plus + * the amount we know each kibana would need for recurring tasks, how much capacity would + * each kibana need if following the minRequiredKibanaInstances? + */ + const averageRequiredThroughputPerMinutePerKibana = + averageCapacityUsedByNonRecurringAndEphemeralTasksPerKibana * + (assumedKibanaInstances / minRequiredKibanaInstances) + + averageRecurringRequiredPerMinute / minRequiredKibanaInstances; + + const assumedAverageRecurringRequiredThroughputPerMinutePerKibana = + averageRecurringRequiredPerMinute / assumedKibanaInstances; + /** + * assuming the historical capacity needed for ephemeral and non-recurring tasks, plus + * the amount we know each kibana would need for recurring tasks, how much capacity would + * each kibana need if the assumed current number were correct? + */ + const assumedRequiredThroughputPerMinutePerKibana = + averageCapacityUsedByNonRecurringAndEphemeralTasksPerKibana + + averageRecurringRequiredPerMinute / assumedKibanaInstances; + + return { + status: + assumedRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana + ? HealthStatus.OK + : assumedAverageRecurringRequiredThroughputPerMinutePerKibana < capacityPerMinutePerKibana + ? HealthStatus.Warning + : HealthStatus.Error, + timestamp: new Date().toISOString(), + value: { + observed: mapValues( + { + observed_kibana_instances: assumedKibanaInstances, + max_throughput_per_minute_per_kibana: capacityPerMinutePerKibana, + max_throughput_per_minute: assumedCapacityAvailablePerMinute, + minutes_to_drain_overdue: + overdue / (assumedKibanaInstances * averageCapacityUsedByPersistedTasksPerKibana), + avg_recurring_required_throughput_per_minute: averageRecurringRequiredPerMinute, + avg_recurring_required_throughput_per_minute_per_kibana: assumedAverageRecurringRequiredThroughputPerMinutePerKibana, + avg_required_throughput_per_minute: + assumedRequiredThroughputPerMinutePerKibana * assumedKibanaInstances, + avg_required_throughput_per_minute_per_kibana: assumedRequiredThroughputPerMinutePerKibana, + }, + Math.ceil + ), + proposed: mapValues( + { + provisioned_kibana: minRequiredKibanaInstances, + min_required_kibana: minRequiredKibanaInstancesForRecurringTasks, + avg_recurring_required_throughput_per_minute_per_kibana: averageRecurringRequiredPerMinutePerKibana, + avg_required_throughput_per_minute_per_kibana: averageRequiredThroughputPerMinutePerKibana, + }, + Math.ceil + ), + }, + }; +} + +export function withCapacityEstimate( + monitoredStats: RawMonitoringStats['stats'] +): RawMonitoringStats['stats'] { + if (isCapacityEstimationParams(monitoredStats)) { + return { + ...monitoredStats, + capacity_estimation: estimateCapacity(monitoredStats), + }; + } + return monitoredStats; +} + +function percentageOf(val: number, percentage: number) { + return Math.round((percentage * val) / 100); +} + +function getAverageDuration( + durations: Partial> +): Result { + const result = stats.mean( + [ + durations.ephemeral?.p50 ?? 0, + durations.non_recurring?.p50 ?? 0, + durations.recurring?.p50 ?? 0, + ].filter((val) => val > 0) + ); + if (isNaN(result)) { + return asErr(result); + } + return asOk(result); +} diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index 885c48a2165b1b..8338bf3197162e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -15,6 +15,7 @@ import { TaskPollingLifecycle } from '../polling_lifecycle'; import { createWorkloadAggregator, summarizeWorkloadStat, + SummarizedWorkloadStat, WorkloadStat, } from './workload_statistics'; import { @@ -27,6 +28,7 @@ import { ConfigStat, createConfigurationAggregator } from './configuration_stati import { TaskManagerConfig } from '../config'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { ManagedConfiguration } from '../lib/create_managed_configuration'; +import { CapacityEstimationStat, withCapacityEstimate } from './capacity_estimation'; export { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; @@ -49,7 +51,8 @@ interface MonitoredStat { timestamp: string; value: T; } -type RawMonitoredStat = MonitoredStat & { + +export type RawMonitoredStat = MonitoredStat & { status: HealthStatus; }; @@ -57,8 +60,9 @@ export interface RawMonitoringStats { last_update: string; stats: { configuration?: RawMonitoredStat; - workload?: RawMonitoredStat; + workload?: RawMonitoredStat; runtime?: RawMonitoredStat; + capacity_estimation?: RawMonitoredStat; }; } @@ -120,33 +124,35 @@ export function summarizeMonitoringStats( }: MonitoringStats, config: TaskManagerConfig ): RawMonitoringStats { + const summarizedStats = withCapacityEstimate({ + ...(configuration + ? { + configuration: { + ...configuration, + status: HealthStatus.OK, + }, + } + : {}), + ...(runtime + ? { + runtime: { + timestamp: runtime.timestamp, + ...summarizeTaskRunStat(runtime.value, config), + }, + } + : {}), + ...(workload + ? { + workload: { + timestamp: workload.timestamp, + ...summarizeWorkloadStat(workload.value), + }, + } + : {}), + }); + return { last_update, - stats: { - ...(configuration - ? { - configuration: { - ...configuration, - status: HealthStatus.OK, - }, - } - : {}), - ...(runtime - ? { - runtime: { - timestamp: runtime.timestamp, - ...summarizeTaskRunStat(runtime.value, config), - }, - } - : {}), - ...(workload - ? { - workload: { - timestamp: workload.timestamp, - ...summarizeWorkloadStat(workload.value), - }, - } - : {}), - }, + stats: summarizedStats, }; } diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts index 7040d5acd4eaf3..38fdc89278e893 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts @@ -387,6 +387,149 @@ describe('Task Run Statistics', () => { }); }); + test('frequency of executed tasks by their persistence', async () => { + const events$ = new Subject(); + + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const runningAverageWindowSize = 5; + const taskRunAggregator = createTaskRunAggregator( + taskPollingLifecycle, + runningAverageWindowSize + ); + + return new Promise((resolve, reject) => { + taskRunAggregator + .pipe( + // skip initial stat which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + // Use 'summarizeTaskRunStat' to receive summarize stats + map(({ key, value }: AggregatedStat) => ({ + key, + value: summarizeTaskRunStat( + value, + getTaskManagerConfig({ + monitored_task_execution_thresholds: { + custom: { + 'alerting:test': { + error_threshold: 59, + warn_threshold: 39, + }, + }, + }, + }) + ).value, + })), + take(10), + bufferCount(10) + ) + .subscribe((taskStats: Array>) => { + try { + /** + * At any given time we only keep track of the last X Polling Results + * In the tests this is ocnfiugured to a window size of 5 + */ + expect(taskStats.map((taskStat) => taskStat.value.execution.persistence)) + .toMatchInlineSnapshot(` + Array [ + Object { + "ephemeral": 0, + "non_recurring": 100, + "recurring": 0, + }, + Object { + "ephemeral": 0, + "non_recurring": 100, + "recurring": 0, + }, + Object { + "ephemeral": 0, + "non_recurring": 67, + "recurring": 33, + }, + Object { + "ephemeral": 0, + "non_recurring": 75, + "recurring": 25, + }, + Object { + "ephemeral": 0, + "non_recurring": 80, + "recurring": 20, + }, + Object { + "ephemeral": 0, + "non_recurring": 60, + "recurring": 40, + }, + Object { + "ephemeral": 0, + "non_recurring": 40, + "recurring": 60, + }, + Object { + "ephemeral": 0, + "non_recurring": 60, + "recurring": 40, + }, + Object { + "ephemeral": 0, + "non_recurring": 60, + "recurring": 40, + }, + Object { + "ephemeral": 0, + "non_recurring": 40, + "recurring": 60, + }, + ] + `); + resolve(); + } catch (e) { + reject(e); + } + }); + + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next( + mockTaskRunEvent( + { schedule: { interval: '3s' } }, + { start: 0, stop: 0 }, + TaskRunResult.Success + ) + ); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next( + mockTaskRunEvent( + { schedule: { interval: '3s' } }, + { start: 0, stop: 0 }, + TaskRunResult.Failed + ) + ); + events$.next( + mockTaskRunEvent( + { schedule: { interval: '3s' } }, + { start: 0, stop: 0 }, + TaskRunResult.RetryScheduled + ) + ); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.RetryScheduled)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next( + mockTaskRunEvent( + { schedule: { interval: '3s' } }, + { start: 0, stop: 0 }, + TaskRunResult.Success + ) + ); + }); + }); + test('returns polling stats', async () => { const expectedTimestamp: string[] = []; const events$ = new Subject(); diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index 3185d3c449c32c..eb6cb0796c33cc 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -7,7 +7,7 @@ import { combineLatest, Observable } from 'rxjs'; import { filter, startWith, map } from 'rxjs/operators'; -import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { JsonObject, JsonValue } from 'src/plugins/kibana_utils/common'; import { isNumber, mapValues } from 'lodash'; import { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; import { TaskLifecycleEvent } from '../polling_lifecycle'; @@ -36,6 +36,16 @@ import { HealthStatus } from './monitoring_stats_stream'; import { TaskPollingLifecycle } from '../polling_lifecycle'; import { TaskExecutionFailureThreshold, TaskManagerConfig } from '../config'; +export enum TaskPersistence { + Recurring = 'recurring', + NonRecurring = 'non_recurring', + Ephemeral = 'ephemeral', +} + +function persistenceOf(task: ConcreteTaskInstance) { + return task.schedule ? TaskPersistence.Recurring : TaskPersistence.NonRecurring; +} + interface FillPoolStat extends JsonObject { last_successful_poll: string; last_polling_delay: string; @@ -48,7 +58,9 @@ interface FillPoolStat extends JsonObject { interface ExecutionStat extends JsonObject { duration: Record; + duration_by_persistence: Record; result_frequency_percent_as_number: Record; + persistence: TaskPersistence[]; } export interface TaskRunStat extends JsonObject { @@ -79,6 +91,11 @@ interface ResultFrequency extends JsonObject { [TaskRunResult.RetryScheduled]: number; [TaskRunResult.Failed]: number; } +export interface TaskPersistenceTypes extends JsonObject { + [TaskPersistence.Recurring]: T; + [TaskPersistence.NonRecurring]: T; + [TaskPersistence.Ephemeral]: T; +} type ResultFrequencySummary = ResultFrequency & { status: HealthStatus; @@ -89,7 +106,9 @@ export interface SummarizedTaskRunStat extends JsonObject { load: AveragedStat; execution: { duration: Record; + duration_by_persistence: Record; result_frequency_percent_as_number: Record; + persistence: TaskPersistenceTypes; }; polling: FillPoolRawStat | Omit; } @@ -100,7 +119,7 @@ export function createTaskRunAggregator( ): AggregatedStatProvider { const taskRunEventToStat = createTaskRunEventToStat(runningAverageWindowSize); const taskRunEvents$: Observable< - Pick + Pick > = taskPollingLifecycle.events.pipe( filter((taskEvent: TaskLifecycleEvent) => isTaskRunEvent(taskEvent) && hasTiming(taskEvent)), map((taskEvent: TaskLifecycleEvent) => { @@ -202,7 +221,16 @@ export function createTaskRunAggregator( startWith({ drift: [], drift_by_type: {}, - execution: { duration: {}, result_frequency_percent_as_number: {} }, + execution: { + duration: {}, + duration_by_persistence: { + [TaskPersistence.Recurring]: [], + [TaskPersistence.NonRecurring]: [], + [TaskPersistence.Ephemeral]: [], + }, + result_frequency_percent_as_number: {}, + persistence: [], + }, }) ), taskManagerLoadStatEvents$.pipe(startWith({ load: [] })), @@ -243,8 +271,12 @@ function hasTiming(taskEvent: TaskLifecycleEvent) { function createTaskRunEventToStat(runningAverageWindowSize: number) { const driftQueue = createRunningAveragedStat(runningAverageWindowSize); + const taskPersistenceQueue = createRunningAveragedStat(runningAverageWindowSize); const driftByTaskQueue = createMapOfRunningAveragedStats(runningAverageWindowSize); const taskRunDurationQueue = createMapOfRunningAveragedStats(runningAverageWindowSize); + const taskRunDurationByPersistenceQueue = createMapOfRunningAveragedStats( + runningAverageWindowSize + ); const resultFrequencyQueue = createMapOfRunningAveragedStats( runningAverageWindowSize ); @@ -252,13 +284,17 @@ function createTaskRunEventToStat(runningAverageWindowSize: number) { task: ConcreteTaskInstance, timing: TaskTiming, result: TaskRunResult - ): Omit => { + ): Pick => { const drift = timing!.start - task.runAt.getTime(); + const duration = timing!.stop - timing!.start; + const persistence = persistenceOf(task); return { drift: driftQueue(drift), drift_by_type: driftByTaskQueue(task.taskType, drift), execution: { - duration: taskRunDurationQueue(task.taskType, timing!.stop - timing!.start), + persistence: taskPersistenceQueue(persistence), + duration: taskRunDurationQueue(task.taskType, duration), + duration_by_persistence: taskRunDurationByPersistenceQueue(persistence as string, duration), result_frequency_percent_as_number: resultFrequencyQueue(task.taskType, result), }, }; @@ -279,6 +315,11 @@ const DEFAULT_POLLING_FREQUENCIES = { [FillPoolResult.RunningAtCapacity]: 0, [FillPoolResult.PoolFilled]: 0, }; +const DEFAULT_PERSISTENCE_FREQUENCIES = { + [TaskPersistence.Recurring]: 0, + [TaskPersistence.NonRecurring]: 0, + [TaskPersistence.Ephemeral]: 0, +}; export function summarizeTaskRunStat( { @@ -298,7 +339,12 @@ export function summarizeTaskRunStat( // eslint-disable-next-line @typescript-eslint/naming-convention drift_by_type, load, - execution: { duration, result_frequency_percent_as_number: executionResultFrequency }, + execution: { + duration, + duration_by_persistence: durationByPersistence, + persistence, + result_frequency_percent_as_number: executionResultFrequency, + }, }: TaskRunStat, config: TaskManagerConfig ): { value: SummarizedTaskRunStat; status: HealthStatus } { @@ -323,6 +369,13 @@ export function summarizeTaskRunStat( load: calculateRunningAverage(load), execution: { duration: mapValues(duration, (typedDurations) => calculateRunningAverage(typedDurations)), + duration_by_persistence: mapValues(durationByPersistence, (typedDurations) => + calculateRunningAverage(typedDurations) + ), + persistence: { + ...DEFAULT_PERSISTENCE_FREQUENCIES, + ...calculateFrequency(persistence), + }, result_frequency_percent_as_number: mapValues( executionResultFrequency, (typedResultFrequencies, taskType) => diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index f3ee21bc7d7704..e88144f2b4a355 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -61,6 +61,12 @@ describe('Workload Statistics Aggregator', () => { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, }, + nonRecurringTasks: { + doc_count: 13, + }, + ownerIds: { + value: 1, + }, // The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate` // would not have a `buckets` property, but rather a keyed property that's inferred from the request. // @ts-expect-error @@ -68,6 +74,9 @@ describe('Workload Statistics Aggregator', () => { doc_count: 0, overdue: { doc_count: 0, + nonRecurring: { + doc_count: 0, + }, }, scheduleDensity: { buckets: [ @@ -114,6 +123,14 @@ describe('Workload Statistics Aggregator', () => { field: 'task.schedule.interval', }, }, + nonRecurringTasks: { + missing: { field: 'task.schedule' }, + }, + ownerIds: { + cardinality: { + field: 'task.ownerId', + }, + }, idleTasks: { filter: { term: { 'task.status': 'idle' }, @@ -146,6 +163,11 @@ describe('Workload Statistics Aggregator', () => { 'task.runAt': { lt: 'now' }, }, }, + aggs: { + nonRecurring: { + missing: { field: 'task.schedule' }, + }, + }, }, }, }, @@ -238,6 +260,12 @@ describe('Workload Statistics Aggregator', () => { }, ], }, + nonRecurringTasks: { + doc_count: 13, + }, + ownerIds: { + value: 1, + }, // The `FiltersAggregate` doesn't cover the case of a nested `AggregationsAggregationContainer`, in which `FiltersAggregate` // would not have a `buckets` property, but rather a keyed property that's inferred from the request. // @ts-expect-error @@ -245,6 +273,9 @@ describe('Workload Statistics Aggregator', () => { doc_count: 13, overdue: { doc_count: 6, + nonRecurring: { + doc_count: 6, + }, }, scheduleDensity: { buckets: [ @@ -496,6 +527,115 @@ describe('Workload Statistics Aggregator', () => { }); }); + test('returns an estimate of the workload by task type', async () => { + // poll every 3 seconds + const pollingIntervalInSeconds = 3; + + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue( + asApiResponse({ + hits: { + hits: [], + max_score: 0, + total: { value: 4, relation: 'eq' }, + }, + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 1, + failed: 0, + }, + aggregations: { + schedule: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + // repeats each cycle + { + key: `${pollingIntervalInSeconds}s`, + doc_count: 1, + }, + { + key: `10s`, // 6 times per minute + doc_count: 20, + }, + { + key: `60s`, // 1 times per minute + doc_count: 10, + }, + { + key: '15m', // 4 times per hour + doc_count: 90, + }, + { + key: '720m', // 2 times per day + doc_count: 10, + }, + { + key: '3h', // 8 times per day + doc_count: 100, + }, + ], + }, + taskType: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [], + }, + nonRecurringTasks: { + doc_count: 13, + }, + ownerIds: { + value: 3, + }, + // The `FiltersAggregate` doesn't cover the case of a nested `AggregationContainer`, in which `FiltersAggregate` + // would not have a `buckets` property, but rather a keyed property that's inferred from the request. + // @ts-expect-error + idleTasks: { + doc_count: 13, + overdue: { + doc_count: 6, + nonRecurring: { + doc_count: 0, + }, + }, + scheduleDensity: { + buckets: [ + mockHistogram(0, 7 * 3000 + 500, 60 * 1000, 3000, [2, 2, 5, 0, 0, 0, 0, 0, 0, 1]), + ], + }, + }, + }, + }) + ); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 10, + pollingIntervalInSeconds * 1000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + + expect(result.value).toMatchObject({ + capacity_requirments: { + // these are buckets of required capacity, rather than aggregated requirmenets. + per_minute: 150, + per_hour: 360, + per_day: 820, + }, + }); + resolve(); + }); + }); + }); + test('recovery after errors occurrs at the next interval', async () => { const refreshInterval = 1000; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index e251ce07679ffb..669f6198325485 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -16,6 +16,7 @@ import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '. import { AggregationResultOf } from '../../../../../typings/elasticsearch'; import { HealthStatus } from './monitoring_stats_stream'; import { TaskStore } from '../task_store'; +import { createRunningAveragedStat } from './task_run_calcultors'; interface StatusStat extends JsonObject { [status: string]: number; @@ -27,12 +28,27 @@ interface TaskTypeStat extends JsonObject { }; } -export interface WorkloadStat extends JsonObject { +interface RawWorkloadStat extends JsonObject { count: number; task_types: TaskTypeStat; schedule: Array<[string, number]>; + non_recurring: number; overdue: number; + overdue_non_recurring: number; estimated_schedule_density: number[]; + capacity_requirments: CapacityRequirments; +} + +export interface WorkloadStat extends RawWorkloadStat { + owner_ids: number[]; +} +export interface SummarizedWorkloadStat extends RawWorkloadStat { + owner_ids: number; +} +export interface CapacityRequirments extends JsonObject { + per_minute: number; + per_hour: number; + per_day: number; } export interface WorkloadAggregation { @@ -109,6 +125,8 @@ export function createWorkloadAggregator( MAX_SHCEDULE_DENSITY_BUCKETS ); + const ownerIdsQueue = createRunningAveragedStat(scheduleDensityBuckets); + return combineLatest([timer(0, refreshInterval), elasticsearchAndSOAvailability$]).pipe( filter(([, areElasticsearchAndSOAvailable]) => areElasticsearchAndSOAvailable), mergeMap(() => @@ -125,6 +143,14 @@ export function createWorkloadAggregator( schedule: { terms: { field: 'task.schedule.interval' }, }, + nonRecurringTasks: { + missing: { field: 'task.schedule' }, + }, + ownerIds: { + cardinality: { + field: 'task.ownerId', + }, + }, idleTasks: { filter: { term: { 'task.status': 'idle' }, @@ -163,6 +189,11 @@ export function createWorkloadAggregator( 'task.runAt': { lt: 'now' }, }, }, + aggs: { + nonRecurring: { + missing: { field: 'task.schedule' }, + }, + }, }, }, }, @@ -181,13 +212,51 @@ export function createWorkloadAggregator( } const taskTypes = aggregations.taskType.buckets; - const schedules = aggregations.schedule.buckets; + const nonRecurring = aggregations.nonRecurringTasks.doc_count; + const ownerIds = aggregations.ownerIds.value; const { - overdue: { doc_count: overdue }, + overdue: { + doc_count: overdue, + nonRecurring: { doc_count: overdueNonRecurring }, + }, scheduleDensity: { buckets: [scheduleDensity] = [] } = {}, } = aggregations.idleTasks; + const { schedules, cadence } = aggregations.schedule.buckets.reduce( + (accm, schedule) => { + const parsedSchedule = { + interval: schedule.key as string, + asSeconds: parseIntervalAsSecond(schedule.key as string), + count: schedule.doc_count, + }; + accm.schedules.push(parsedSchedule); + if (parsedSchedule.asSeconds <= 60) { + accm.cadence.perMinute += + parsedSchedule.count * Math.round(60 / parsedSchedule.asSeconds); + } else if (parsedSchedule.asSeconds <= 3600) { + accm.cadence.perHour += + parsedSchedule.count * Math.round(3600 / parsedSchedule.asSeconds); + } else { + accm.cadence.perDay += + parsedSchedule.count * Math.round((3600 * 24) / parsedSchedule.asSeconds); + } + return accm; + }, + { + cadence: { + perMinute: 0, + perHour: 0, + perDay: 0, + }, + schedules: [] as Array<{ + interval: string; + asSeconds: number; + count: number; + }>, + } + ); + const summary: WorkloadStat = { count, task_types: mapValues(keyBy(taskTypes, 'key'), ({ doc_count: docCount, status }) => { @@ -196,19 +265,23 @@ export function createWorkloadAggregator( status: mapValues(keyBy(status.buckets, 'key'), 'doc_count'), }; }), + non_recurring: nonRecurring, + owner_ids: ownerIdsQueue(ownerIds), schedule: schedules - .sort( - (scheduleLeft, scheduleRight) => - parseIntervalAsSecond(scheduleLeft.key as string) - - parseIntervalAsSecond(scheduleRight.key as string) - ) - .map((schedule) => [schedule.key as string, schedule.doc_count]), + .sort((scheduleLeft, scheduleRight) => scheduleLeft.asSeconds - scheduleRight.asSeconds) + .map((schedule) => [schedule.interval, schedule.count]), overdue, + overdue_non_recurring: overdueNonRecurring, estimated_schedule_density: padBuckets( scheduleDensityBuckets, pollInterval, scheduleDensity ), + capacity_requirments: { + per_minute: cadence.perMinute, + per_hour: cadence.perHour, + per_day: cadence.perDay, + }, }; return { key: 'workload', @@ -344,9 +417,14 @@ export function estimateRecurringTaskScheduling( export function summarizeWorkloadStat( workloadStats: WorkloadStat -): { value: WorkloadStat; status: HealthStatus } { +): { value: SummarizedWorkloadStat; status: HealthStatus } { return { - value: workloadStats, + value: { + ...workloadStats, + // assume the largest number we've seen of active owner IDs + // matches the number of active Task Managers in the cluster + owner_ids: Math.max(...workloadStats.owner_ids), + }, status: HealthStatus.OK, }; } @@ -365,6 +443,12 @@ export interface WorkloadAggregationResponse { taskType: TaskTypeAggregation; schedule: ScheduleAggregation; idleTasks: IdleTasksAggregation; + nonRecurringTasks: { + doc_count: number; + }; + ownerIds: { + value: number; + }; [otherAggs: string]: estypes.AggregationsAggregate; } export interface TaskTypeAggregation extends estypes.AggregationsFiltersAggregate { @@ -415,6 +499,9 @@ export interface IdleTasksAggregation extends estypes.AggregationsFiltersAggrega }; overdue: { doc_count: number; + nonRecurring: { + doc_count: number; + }; }; } diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index 0a9671d9ac37e9..ae883585e7085e 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -15,7 +15,7 @@ import { mockHandlerArguments } from './_mock_handler_arguments'; import { sleep } from '../test_utils'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { Logger } from '../../../../../src/core/server'; -import { MonitoringStats, summarizeMonitoringStats } from '../monitoring'; +import { MonitoringStats, RawMonitoringStats, summarizeMonitoringStats } from '../monitoring'; import { ServiceStatusLevels } from 'src/core/server'; import { configSchema, TaskManagerConfig } from '../config'; @@ -72,7 +72,7 @@ describe('healthRoute', () => { id, timestamp: expect.any(String), status: expect.any(String), - ...summarizeMonitoringStats(mockStat, getTaskManagerConfig({})), + ...ignoreCapacityEstimation(summarizeMonitoringStats(mockStat, getTaskManagerConfig({}))), }); const secondDebug = JSON.parse( @@ -82,13 +82,15 @@ describe('healthRoute', () => { id, timestamp: expect.any(String), status: expect.any(String), - ...summarizeMonitoringStats(skippedMockStat, getTaskManagerConfig({})), + ...ignoreCapacityEstimation( + summarizeMonitoringStats(skippedMockStat, getTaskManagerConfig({})) + ), }); expect(secondDebug).toMatchObject({ id, timestamp: expect.any(String), status: expect.any(String), - ...summarizeMonitoringStats(nextMockStat, getTaskManagerConfig({})), + ...ignoreCapacityEstimation(summarizeMonitoringStats(nextMockStat, getTaskManagerConfig({}))), }); expect(logger.debug).toHaveBeenCalledTimes(2); @@ -127,27 +129,29 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchObject({ body: { status: 'error', - ...summarizeMonitoringStats( - mockHealthStats({ - last_update: expect.any(String), - stats: { - configuration: { - timestamp: expect.any(String), - }, - workload: { - timestamp: expect.any(String), - }, - runtime: { - timestamp: expect.any(String), - value: { - polling: { - last_successful_poll: expect.any(String), + ...ignoreCapacityEstimation( + summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll: expect.any(String), + }, }, }, }, - }, - }), - getTaskManagerConfig({}) + }), + getTaskManagerConfig({}) + ) ), }, }); @@ -196,27 +200,29 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchObject({ body: { status: 'error', - ...summarizeMonitoringStats( - mockHealthStats({ - last_update: expect.any(String), - stats: { - configuration: { - timestamp: expect.any(String), - }, - workload: { - timestamp: expect.any(String), - }, - runtime: { - timestamp: expect.any(String), - value: { - polling: { - last_successful_poll: expect.any(String), + ...ignoreCapacityEstimation( + summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll: expect.any(String), + }, }, }, }, - }, - }), - getTaskManagerConfig() + }), + getTaskManagerConfig() + ) ), }, }); @@ -262,98 +268,113 @@ describe('healthRoute', () => { expect(await handler(context, req, res)).toMatchObject({ body: { status: 'error', - ...summarizeMonitoringStats( - mockHealthStats({ - last_update: expect.any(String), - stats: { - configuration: { - timestamp: expect.any(String), - }, - workload: { - timestamp: expect.any(String), - }, - runtime: { - timestamp: expect.any(String), - value: { - polling: { - last_successful_poll, + ...ignoreCapacityEstimation( + summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll, + }, }, }, }, - }, - }), - getTaskManagerConfig() + }), + getTaskManagerConfig() + ) ), }, }); }); }); +function ignoreCapacityEstimation(stats: RawMonitoringStats) { + stats.stats.capacity_estimation = expect.any(Object); + return stats; +} + function mockHealthStats(overrides = {}) { - return (merge( - { - last_update: new Date().toISOString(), - stats: { - configuration: { - timestamp: new Date().toISOString(), - value: { - value: { - max_workers: 10, - poll_interval: 6000000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, - }, - custom: {}, - }, + const stub: MonitoringStats = { + last_update: new Date().toISOString(), + stats: { + configuration: { + timestamp: new Date().toISOString(), + value: { + max_workers: 10, + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, }, + custom: {}, }, }, - workload: { - timestamp: new Date().toISOString(), - value: { - count: 4, - taskTypes: { - actions_telemetry: { count: 2, status: { idle: 2 } }, - alerting_telemetry: { count: 1, status: { idle: 1 } }, - session_cleanup: { count: 1, status: { idle: 1 } }, - }, - schedule: {}, - overdue: 0, - estimatedScheduleDensity: [], + }, + workload: { + timestamp: new Date().toISOString(), + value: { + count: 4, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 1, status: { idle: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + schedule: [], + overdue: 0, + overdue_non_recurring: 0, + estimatedScheduleDensity: [], + non_recurring: 20, + owner_ids: [0, 0, 0, 1, 2, 0, 0, 2, 2, 2, 1, 2, 1, 1], + estimated_schedule_density: [], + capacity_requirments: { + per_minute: 150, + per_hour: 360, + per_day: 820, }, }, - runtime: { - timestamp: new Date().toISOString(), - value: { - drift: [1000, 60000], - load: [0, 100, 75], - execution: { - duration: [], - result_frequency_percent_as_number: [], - }, - polling: { - last_successful_poll: new Date().toISOString(), - duration: [500, 400, 3000], - claim_conflicts: [0, 100, 75], - claim_mismatches: [0, 100, 75], - result_frequency_percent_as_number: [ - 'NoTasksClaimed', - 'NoTasksClaimed', - 'NoTasksClaimed', - ], - }, + }, + runtime: { + timestamp: new Date().toISOString(), + value: { + drift: [1000, 60000], + drift_by_type: {}, + load: [0, 100, 75], + execution: { + duration: {}, + duration_by_persistence: {}, + persistence: [], + result_frequency_percent_as_number: {}, + }, + polling: { + last_successful_poll: new Date().toISOString(), + duration: [500, 400, 3000], + claim_conflicts: [0, 100, 75], + claim_mismatches: [0, 100, 75], + result_frequency_percent_as_number: [ + 'NoTasksClaimed', + 'NoTasksClaimed', + 'NoTasksClaimed', + ], }, }, }, }, - overrides - ) as unknown) as MonitoringStats; + }; + return (merge(stub, overrides) as unknown) as MonitoringStats; } async function getLatest(stream$: Observable) { diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts index d99c1dac9a25e9..2626ef2421f0b8 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts @@ -27,7 +27,14 @@ interface MonitoringStats { task_types: Record; schedule: Array<[string, number]>; overdue: number; + non_recurring: number; + owner_ids: number; estimated_schedule_density: number[]; + capacity_requirments: { + per_minute: number; + per_hour: number; + per_day: number; + }; }; }; runtime: { @@ -38,6 +45,7 @@ interface MonitoringStats { load: Record; execution: { duration: Record>; + persistence: Record; result_frequency_percent_as_number: Record>; }; polling: { @@ -49,6 +57,27 @@ interface MonitoringStats { }; }; }; + capacity_estimation: { + timestamp: string; + value: { + observed: { + observed_kibana_instances: number; + max_throughput_per_minute: number; + max_throughput_per_minute_per_kibana: number; + minutes_to_drain_overdue: number; + avg_required_throughput_per_minute: number; + avg_required_throughput_per_minute_per_kibana: number; + avg_recurring_required_throughput_per_minute: number; + avg_recurring_required_throughput_per_minute_per_kibana: number; + }; + proposed: { + min_required_kibana: number; + avg_recurring_required_throughput_per_minute_per_kibana: number; + avg_required_throughput_per_minute: number; + avg_required_throughput_per_minute_per_kibana: number; + }; + }; + }; }; } @@ -155,12 +184,44 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return a breakdown of idleTasks in the task manager workload', async () => { + const { + capacity_estimation: { + value: { observed, proposed }, + }, + } = (await getHealth()).stats; + + expect(typeof observed.observed_kibana_instances).to.eql('number'); + expect(typeof observed.max_throughput_per_minute).to.eql('number'); + expect(typeof observed.max_throughput_per_minute_per_kibana).to.eql('number'); + expect(typeof observed.minutes_to_drain_overdue).to.eql('number'); + expect(typeof observed.avg_required_throughput_per_minute).to.eql('number'); + expect(typeof observed.avg_required_throughput_per_minute_per_kibana).to.eql('number'); + expect(typeof observed.avg_recurring_required_throughput_per_minute).to.eql('number'); + expect(typeof observed.avg_recurring_required_throughput_per_minute_per_kibana).to.eql( + 'number' + ); + + expect(typeof proposed.min_required_kibana).to.eql('number'); + expect(typeof proposed.avg_recurring_required_throughput_per_minute_per_kibana).to.eql( + 'number' + ); + expect(typeof proposed.avg_required_throughput_per_minute_per_kibana).to.eql('number'); + }); + + it('should return an estimation of task manager capacity', async () => { const { workload: { value: workload }, } = (await getHealth()).stats; expect(typeof workload.overdue).to.eql('number'); + expect(typeof workload.non_recurring).to.eql('number'); + expect(typeof workload.owner_ids).to.eql('number'); + + expect(typeof workload.capacity_requirments.per_minute).to.eql('number'); + expect(typeof workload.capacity_requirments.per_hour).to.eql('number'); + expect(typeof workload.capacity_requirments.per_day).to.eql('number'); + expect(Array.isArray(workload.estimated_schedule_density)).to.eql(true); // test run with the default poll_interval of 3s and a monitored_aggregated_stats_refresh_rate of 5s, @@ -220,6 +281,10 @@ export default function ({ getService }: FtrProviderContext) { expect(typeof execution.duration.sampleTask.p95).to.eql('number'); expect(typeof execution.duration.sampleTask.p99).to.eql('number'); + expect(typeof execution.persistence.ephemeral).to.eql('number'); + expect(typeof execution.persistence.non_recurring).to.eql('number'); + expect(typeof execution.persistence.recurring).to.eql('number'); + expect(typeof execution.result_frequency_percent_as_number.sampleTask.Success).to.eql( 'number' ); From 150198404a8fc6f6b6b5b2a048f0af0df6cc745a Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 14 Jun 2021 14:55:26 +0200 Subject: [PATCH 59/99] [ML] Functional tests - stabilize alerting flyout test (#102030) This PR stabilizes and re-activates the ML alerting flyout test suite. --- x-pack/test/functional/services/ml/alerting.ts | 4 ++-- x-pack/test/functional/services/ml/api.ts | 2 +- .../test/functional_with_es_ssl/apps/ml/alert_flyout.ts | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/services/ml/alerting.ts b/x-pack/test/functional/services/ml/alerting.ts index 327a0e574f0fde..241ffde6f7628e 100644 --- a/x-pack/test/functional/services/ml/alerting.ts +++ b/x-pack/test/functional/services/ml/alerting.ts @@ -94,10 +94,10 @@ export function MachineLearningAlertingProvider( await this.assertPreviewCalloutVisible(); }, - async checkPreview(expectedMessage: string) { + async checkPreview(expectedMessagePattern: RegExp) { await this.clickPreviewButton(); const previewMessage = await testSubjects.getVisibleText('mlAnomalyAlertPreviewMessage'); - expect(previewMessage).to.eql(expectedMessage); + expect(previewMessage).to.match(expectedMessagePattern); }, async assertPreviewCalloutVisible() { diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 9a96b1ec372dc5..317f2dfe605143 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -541,7 +541,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }, async waitForDatafeedToNotExist(datafeedId: string) { - await retry.waitForWithTimeout(`'${datafeedId}' to exist`, 5 * 1000, async () => { + await retry.waitForWithTimeout(`'${datafeedId}' to not exist`, 5 * 1000, async () => { if ((await this.datafeedExist(datafeedId)) === false) { return true; } else { diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts index 96e6bd4b302bd0..63326448ec1e51 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts @@ -67,8 +67,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { let testJobId = ''; - // Failing: See https://github.com/elastic/kibana/issues/102012 - describe.skip('anomaly detection alert', function () { + describe('anomaly detection alert', function () { this.tags('ciGroup13'); before(async () => { @@ -93,6 +92,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => { + await ml.api.deleteAnomalyDetectionJobES(testJobId); await ml.api.cleanMlIndices(); await ml.alerting.cleanAnomalyDetectionRules(); }); @@ -120,7 +120,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await ml.alerting.assertPreviewButtonState(false); await ml.alerting.setTestInterval('2y'); await ml.alerting.assertPreviewButtonState(true); - await ml.alerting.checkPreview('Found 13 anomalies in the last 2y.'); + + // don't check the exact number provided by the backend, just make sure it's > 0 + await ml.alerting.checkPreview(/Found [1-9]\d* anomalies in the last 2y/); await ml.testExecution.logTestStep('should create an alert'); await pageObjects.triggersActionsUI.setAlertName('ml-test-alert'); From 0993a1c32114a03251bb0dabf8662024e7f13fef Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 14 Jun 2021 15:26:13 +0200 Subject: [PATCH 60/99] [APM] Display automatic deployment annotations correctly (#102020) --- .../get_derived_service_annotations.ts | 2 +- .../test/apm_api_integration/tests/index.ts | 1 + .../tests/services/derived_annotations.ts | 181 ++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/apm_api_integration/tests/services/derived_annotations.ts diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 028c8c042c8dc3..611f9b18a0b1a2 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -92,7 +92,7 @@ export async function getDerivedServiceAnnotations({ }, }, sort: { - '@timestamp': 'desc', + '@timestamp': 'asc', }, }, }); diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 7c38f37093fa43..813e0e4f3cdb89 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -84,6 +84,7 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte describe('services/annotations', function () { loadTestFile(require.resolve('./services/annotations')); + loadTestFile(require.resolve('./services/derived_annotations')); }); describe('services/service_details', function () { diff --git a/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts b/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts new file mode 100644 index 00000000000000..2ff4eb7e733066 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function annotationApiTests({ getService }: FtrProviderContext) { + const supertestRead = createApmApiSupertest(getService('supertestAsApmReadUser')); + const es = getService('es'); + + const dates = [ + new Date('2021-02-01T00:00:00.000Z'), + new Date('2021-02-01T01:00:00.000Z'), + new Date('2021-02-01T02:00:00.000Z'), + new Date('2021-02-01T03:00:00.000Z'), + ]; + + const indexName = 'apm-8.0.0-transaction'; + + registry.when( + 'Derived deployment annotations with a basic license', + { config: 'basic', archives: [] }, + () => { + describe('when there are multiple service versions', () => { + let response: APIReturnType<'GET /api/apm/services/{serviceName}/annotation/search'>; + + before(async () => { + const { body: indexExists } = await es.indices.exists({ index: indexName }); + if (indexExists) { + await es.indices.delete({ + index: indexName, + }); + } + + await es.indices.create({ + index: indexName, + body: { + mappings: { + properties: { + service: { + properties: { + name: { + type: 'keyword', + }, + version: { + type: 'keyword', + }, + environment: { + type: 'keyword', + }, + }, + }, + transaction: { + properties: { + type: { + type: 'keyword', + }, + duration: { + type: 'long', + }, + }, + }, + observer: { + properties: { + version_major: { + type: 'byte', + }, + }, + }, + processor: { + properties: { + event: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }); + + const docs = dates.flatMap((date, index) => { + const baseAnnotation = { + transaction: { + type: 'request', + duration: 1000000, + }, + + service: { + name: 'opbeans-java', + environment: 'production', + version: index + 1, + }, + observer: { + version_major: 8, + }, + processor: { + event: 'transaction', + }, + }; + return [ + { + ...baseAnnotation, + '@timestamp': date.toISOString(), + }, + { + ...baseAnnotation, + '@timestamp': new Date(date.getTime() + 30000), + }, + { + ...baseAnnotation, + '@timestamp': new Date(date.getTime() + 60000), + }, + ]; + }); + + await es.bulk({ + index: indexName, + body: docs.flatMap((doc) => [{ index: {} }, doc]), + refresh: true, + }); + + response = ( + await supertestRead({ + endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: dates[1].toISOString(), + end: dates[2].toISOString(), + environment: 'production', + }, + }, + }) + ).body; + }); + + it('annotations are displayed for the service versions in the given time range', async () => { + expect(response.annotations.length).to.be(2); + expect(response.annotations[0]['@timestamp']).to.be(dates[1].getTime()); + expect(response.annotations[1]['@timestamp']).to.be(dates[2].getTime()); + + expectSnapshot(response.annotations[0]).toMatchInline(` + Object { + "@timestamp": 1612141200000, + "id": "2", + "text": "2", + "type": "version", + } + `); + }); + + it('annotations are not displayed for the service versions outside of the given time range', () => { + expect( + response.annotations.some((annotation) => { + return ( + annotation['@timestamp'] !== dates[0].getTime() && + annotation['@timestamp'] !== dates[2].getTime() + ); + }) + ); + }); + + after(async () => { + await es.indices.delete({ + index: indexName, + }); + }); + }); + } + ); +} From 71a81f7cd6bc8aec7f52d8c26b1b1aee937859e4 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Mon, 14 Jun 2021 15:39:11 +0200 Subject: [PATCH 61/99] [Security Solution][Endpoint] Actions Log API (#101032) * WIP add tabs for endpoint details * fetch activity log for endpoint this is work in progress with dummy data * refactor to hold host details and activity log within endpointDetails * api for fetching actions log * add a selector for getting selected agent id * use the new api to show actions log * review changes * move util function to common/utils in order to use it in endpoint_hosts as well as in trusted _apps review suggestion * use util function to get API path review suggestion * sync url params with details active tab review suggestion * fix types due to merge commit refs 3722552f739f74d3c457e8ed6cf80444aa6dfd06 * use AsyncResourseState type review suggestions * sort entries chronologically with recent at the top * adjust icon sizes within entries to match mocks * remove endpoint list paging stuff (not for now) * fix import after sync with master * make the search bar work (sort of) this needs to be fleshed out in a later PR * add tests to middleware for now * use snake case for naming routes review changes * rename and use own relative time function review change * use euiTheme tokens review change * add a comment review changes * log errors to kibana log and unwind stack review changes * search on two indices * fix types * use modified data * distinguish between responses and actions and respective states in UI * use indices explicitly and tune the query * fix types after sync with master * fix lint * do better types review suggestion * add paging to API call * add paging info to redux store for activityLog * decouple paging action from other API requests * use a button for now to fetch more data * add index to fleet indices else we get a type check error about the constant not being exported correctly from `x-pack/plugins/fleet/common/constants/agent` * add tests for audit log API * do semantic paging from first request * fix ts error review changes * add document id and total to API review suggestions * update test * update frontend to consume the modified api correctly * update mock * rename action review changes * wrap mock into function to create anew on each test review changes * wrap with schema.maybe and increase page size review changes * ignore 404 review changes * use i18n review changes * abstract logEntry component logic review changes * move handler logic to a service review changes * update response object review changes * fix paging to use 50 as initial fetch size * fix translations and move custom hook to component file review changes * add return type review changes * update default value for page_size review changes * remove default values review changes https://github.com/elastic/kibana/tree/master/packages/kbn-config-schema#schemamaybe https://github.com/elastic/kibana/tree/master/packages/kbn-config-schema#default-values * fix mock data refs 1f9ae7019498c616455f1109a524741e212106ca * add selectors for data review changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/constants/agent.ts | 1 + .../plugins/fleet/common/constants/index.ts | 1 + .../common/endpoint/schema/actions.ts | 11 +- .../common/endpoint/types/actions.ts | 26 +++ .../pages/endpoint_hosts/store/action.ts | 10 +- .../pages/endpoint_hosts/store/builders.ts | 6 +- .../pages/endpoint_hosts/store/index.test.ts | 4 +- .../endpoint_hosts/store/middleware.test.ts | 45 +++- .../pages/endpoint_hosts/store/middleware.ts | 70 +++++- .../pages/endpoint_hosts/store/reducer.ts | 30 ++- .../pages/endpoint_hosts/store/selectors.ts | 28 ++- .../management/pages/endpoint_hosts/types.ts | 8 +- .../view/details/components/log_entry.tsx | 199 ++++++++++++++---- .../components/log_entry_timeline_icon.tsx | 42 ++++ .../view/details/endpoint_activity_log.tsx | 49 ++++- .../view/details/endpoints.stories.tsx | 142 ++++++++----- .../endpoint_hosts/view/details/index.tsx | 12 +- .../pages/endpoint_hosts/view/translations.ts | 50 ++++- .../endpoint/routes/actions/audit_log.test.ts | 186 ++++++++++++++++ .../routes/actions/audit_log_handler.ts | 49 ++--- .../server/endpoint/routes/actions/mocks.ts | 23 ++ .../server/endpoint/routes/actions/service.ts | 110 ++++++++++ 22 files changed, 919 insertions(+), 183 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts index 6d85f658f2240b..e38b7a6b5832b8 100644 --- a/x-pack/plugins/fleet/common/constants/agent.ts +++ b/x-pack/plugins/fleet/common/constants/agent.ts @@ -25,3 +25,4 @@ export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL = 5; export const AGENTS_INDEX = '.fleet-agents'; export const AGENT_ACTIONS_INDEX = '.fleet-actions'; +export const AGENT_ACTIONS_RESULTS_INDEX = '.fleet-actions-results'; diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index e3001542e3e6f9..ef8cb63f132b44 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -31,6 +31,7 @@ export const FLEET_SERVER_SERVERS_INDEX = '.fleet-servers'; export const FLEET_SERVER_INDICES = [ '.fleet-actions', + '.fleet-actions-results', '.fleet-agents', FLEET_SERVER_ARTIFACTS_INDEX, '.fleet-enrollment-api-keys', diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index 09776b57ed8eaf..f58dd1f3370d4a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; export const HostIsolationRequestSchema = { body: schema.object({ @@ -22,13 +22,18 @@ export const HostIsolationRequestSchema = { }; export const EndpointActionLogRequestSchema = { - // TODO improve when using pagination with query params - query: schema.object({}), + query: schema.object({ + page: schema.number({ defaultValue: 1, min: 1 }), + page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }), + }), params: schema.object({ agent_id: schema.string(), }), }; +export type EndpointActionLogRequestParams = TypeOf; +export type EndpointActionLogRequestQuery = TypeOf; + export const ActionStatusRequestSchema = { query: schema.object({ agent_ids: schema.oneOf([ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 937025f35dadcd..99753242e76279 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -38,6 +38,32 @@ export interface EndpointActionResponse { action_data: EndpointActionData; } +export interface ActivityLogAction { + type: 'action'; + item: { + // document _id + id: string; + // document _source + data: EndpointAction; + }; +} +export interface ActivityLogActionResponse { + type: 'response'; + item: { + // document id + id: string; + // document _source + data: EndpointActionResponse; + }; +} +export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; +export interface ActivityLog { + total: number; + page: number; + pageSize: number; + data: ActivityLogEntry[]; +} + export type HostIsolationRequestBody = TypeOf; export interface HostIsolationResponse { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 178f27caa10853..a8885b904d9c8d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -148,8 +148,15 @@ export type EndpointIsolationRequestStateChange = Action<'endpointIsolationReque payload: EndpointState['isolationRequestState']; }; +export interface AppRequestedEndpointActivityLog { + type: 'appRequestedEndpointActivityLog'; + payload: { + page: number; + pageSize: number; + }; +} export type EndpointDetailsActivityLogChanged = Action<'endpointDetailsActivityLogChanged'> & { - payload: EndpointState['endpointDetails']['activityLog']; + payload: EndpointState['endpointDetails']['activityLog']['logData']; }; export type EndpointAction = @@ -157,6 +164,7 @@ export type EndpointAction = | ServerFailedToReturnEndpointList | ServerReturnedEndpointDetails | ServerFailedToReturnEndpointDetails + | AppRequestedEndpointActivityLog | EndpointDetailsActivityLogChanged | ServerReturnedEndpointPolicyResponse | ServerFailedToReturnEndpointPolicyResponse diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index d5416d9f8ec965..273b4279851fd3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -19,7 +19,11 @@ export const initialEndpointPageState = (): Immutable => { loading: false, error: undefined, endpointDetails: { - activityLog: createUninitialisedResourceState(), + activityLog: { + page: 1, + pageSize: 50, + logData: createUninitialisedResourceState(), + }, hostDetails: { details: undefined, detailsLoading: false, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 5be67a3581c9ec..455c6538bcdf26 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -43,7 +43,9 @@ describe('EndpointList store concerns', () => { error: undefined, endpointDetails: { activityLog: { - type: 'UninitialisedResourceState', + page: 1, + pageSize: 50, + logData: { type: 'UninitialisedResourceState' }, }, hostDetails: { details: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 98ef5a341ac9ef..130f8a56fd0267 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -18,7 +18,7 @@ import { Immutable, HostResultList, HostIsolationResponse, - EndpointAction, + ActivityLog, ISOLATION_ACTIONS, } from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; @@ -233,16 +233,40 @@ describe('endpoint list middleware', () => { }, }); }; + const fleetActionGenerator = new FleetActionGenerator(Math.random().toString()); - const activityLog = [ - fleetActionGenerator.generate({ - agents: [endpointList.hosts[0].metadata.agent.id], - }), - ]; + const actionData = fleetActionGenerator.generate({ + agents: [endpointList.hosts[0].metadata.agent.id], + }); + const responseData = fleetActionGenerator.generateResponse({ + agent_id: endpointList.hosts[0].metadata.agent.id, + }); + const getMockEndpointActivityLog = () => + ({ + total: 2, + page: 1, + pageSize: 50, + data: [ + { + type: 'response', + item: { + id: '', + data: responseData, + }, + }, + { + type: 'action', + item: { + id: '', + data: actionData, + }, + }, + ], + } as ActivityLog); const dispatchGetActivityLog = () => { dispatch({ type: 'endpointDetailsActivityLogChanged', - payload: createLoadedResourceState(activityLog), + payload: createLoadedResourceState(getMockEndpointActivityLog()), }); }; @@ -270,11 +294,10 @@ describe('endpoint list middleware', () => { dispatchGetActivityLog(); const loadedDispatchedResponse = await loadedDispatched; - const activityLogData = (loadedDispatchedResponse.payload as LoadedResourceState< - EndpointAction[] - >).data; + const activityLogData = (loadedDispatchedResponse.payload as LoadedResourceState) + .data; - expect(activityLogData).toEqual(activityLog); + expect(activityLogData).toEqual(getMockEndpointActivityLog()); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index a1da3c072293ea..aa0afe5ec980a3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -8,7 +8,7 @@ import { Dispatch } from 'redux'; import { CoreStart, HttpStart } from 'kibana/public'; import { - EndpointAction, + ActivityLog, HostInfo, HostIsolationRequestBody, HostIsolationResponse, @@ -32,6 +32,8 @@ import { getIsIsolationRequestPending, getCurrentIsolationRequestState, getActivityLogData, + getActivityLogDataPaging, + getLastLoadedActivityLogData, } from './selectors'; import { EndpointState, PolicyIds } from '../types'; import { @@ -336,21 +338,25 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())), + payload: createLoadingResourceState(getActivityLogData(getState())), }); try { - const activityLog = await coreStart.http.get( - resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()) }) - ); + const { page, pageSize } = getActivityLogDataPaging(getState()); + const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { + agent_id: selectedAgent(getState()), + }); + const activityLog = await coreStart.http.get(route, { + query: { page, page_size: pageSize }, + }); dispatch({ type: 'endpointDetailsActivityLogChanged', - payload: createLoadedResourceState(activityLog), + payload: createLoadedResourceState(activityLog), }); } catch (error) { dispatch({ type: 'endpointDetailsActivityLogChanged', - payload: createFailedResourceState(error.body ?? error), + payload: createFailedResourceState(error.body ?? error), }); } @@ -371,6 +377,56 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())), + }); + + try { + const { page, pageSize } = getActivityLogDataPaging(getState()); + const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { + agent_id: selectedAgent(getState()), + }); + const activityLog = await coreStart.http.get(route, { + query: { page, page_size: pageSize }, + }); + + const lastLoadedLogData = getLastLoadedActivityLogData(getState()); + if (lastLoadedLogData !== undefined) { + const updatedLogDataItems = [ + ...new Set([...lastLoadedLogData.data, ...activityLog.data]), + ] as ActivityLog['data']; + + const updatedLogData = { + total: activityLog.total, + page: activityLog.page, + pageSize: activityLog.pageSize, + data: updatedLogDataItems, + }; + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createLoadedResourceState(updatedLogData), + }); + // TODO dispatch 'noNewLogData' if !activityLog.length + // resets paging to previous state + } else { + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createLoadedResourceState(activityLog), + }); + } + } catch (error) { + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createFailedResourceState(error.body ?? error), + }); + } + } + // Isolate Host if (action.type === 'endpointIsolationRequest') { return handleIsolateEndpointHost(store, action); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 19235b792b2702..b580664512eb66 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -33,7 +33,10 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer +): Immutable> => { + return { + page: state.endpointDetails.activityLog.page, + pageSize: state.endpointDetails.activityLog.pageSize, + }; +}; + export const getActivityLogData = ( state: Immutable -): Immutable => state.endpointDetails.activityLog; +): Immutable => + state.endpointDetails.activityLog.logData; + +export const getLastLoadedActivityLogData: ( + state: Immutable +) => Immutable | undefined = createSelector(getActivityLogData, (activityLog) => { + return getLastLoadedResourceState(activityLog)?.data; +}); export const getActivityLogRequestLoading: ( state: Immutable @@ -375,6 +394,13 @@ export const getActivityLogRequestLoaded: ( isLoadedResourceState(activityLog) ); +export const getActivityLogIterableData: ( + state: Immutable +) => Immutable = createSelector(getActivityLogData, (activityLog) => { + const emptyArray: ActivityLog['data'] = []; + return isLoadedResourceState(activityLog) ? activityLog.data.data : emptyArray; +}); + export const getActivityLogError: ( state: Immutable ) => ServerApiError | undefined = createSelector(getActivityLogData, (activityLog) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 53ddfaee7aa053..eed2182d41809d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -6,6 +6,7 @@ */ import { + ActivityLog, HostInfo, Immutable, HostMetadata, @@ -14,7 +15,6 @@ import { PolicyData, MetadataQueryStrategyVersions, HostStatus, - EndpointAction, HostIsolationResponse, } from '../../../../common/endpoint/types'; import { ServerApiError } from '../../../common/types'; @@ -36,7 +36,11 @@ export interface EndpointState { /** api error from retrieving host list */ error?: ServerApiError; endpointDetails: { - activityLog: AsyncResourceState; + activityLog: { + page: number; + pageSize: number; + logData: AsyncResourceState; + }; hostDetails: { /** details data for a specific host */ details?: Immutable; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index de6d2ecf36eccf..f8574da7f0a03d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -5,53 +5,162 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; -import { EuiAvatar, EuiComment, EuiText } from '@elastic/eui'; -import { Immutable, EndpointAction } from '../../../../../../../common/endpoint/types'; +import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui'; +import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types'; import { FormattedDateAndTime } from '../../../../../../common/components/endpoint/formatted_date_time'; -import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; - -export const LogEntry = memo( - ({ endpointAction }: { endpointAction: Immutable }) => { - const euiTheme = useEuiTheme(); - const isIsolated = endpointAction?.data.command === 'isolate'; - - // do this better when we can distinguish between endpoint events vs user events - const iconType = endpointAction.user_id === 'sys' ? 'dot' : isIsolated ? 'lock' : 'lockOpen'; - const commentType = endpointAction.user_id === 'sys' ? 'update' : 'regular'; - const timelineIcon = ( - - ); - const event = `${isIsolated ? 'isolated' : 'unisolated'} host`; - const hasComment = !!endpointAction.data.comment; - - return ( - - {hasComment ? ( - -

    {endpointAction.data.comment}

    -
    - ) : undefined} -
    - ); +import { LogEntryTimelineIcon } from './log_entry_timeline_icon'; + +import * as i18 from '../../translations'; + +const useLogEntryUIProps = ( + logEntry: Immutable +): { + actionEventTitle: string; + avatarSize: EuiAvatarProps['size']; + commentText: string; + commentType: EuiCommentProps['type']; + displayComment: boolean; + displayResponseEvent: boolean; + iconType: IconType; + isResponseEvent: boolean; + isSuccessful: boolean; + responseEventTitle: string; + username: string | React.ReactNode; +} => { + return useMemo(() => { + let iconType: IconType = 'dot'; + let commentType: EuiCommentProps['type'] = 'update'; + let commentText: string = ''; + let avatarSize: EuiAvatarProps['size'] = 's'; + let isIsolateAction: boolean = false; + let isResponseEvent: boolean = false; + let isSuccessful: boolean = false; + let displayComment: boolean = false; + let displayResponseEvent: boolean = true; + let username: EuiCommentProps['username'] = ''; + + if (logEntry.type === 'action') { + avatarSize = 'm'; + commentType = 'regular'; + commentText = logEntry.item.data.data.comment ?? ''; + displayResponseEvent = false; + iconType = 'lockOpen'; + username = logEntry.item.data.user_id; + if (logEntry.item.data.data) { + const data = logEntry.item.data.data; + if (data.command === 'isolate') { + iconType = 'lock'; + isIsolateAction = true; + } + if (data.comment) { + displayComment = true; + } + } + } else if (logEntry.type === 'response') { + isResponseEvent = true; + if (logEntry.item.data.action_data.command === 'isolate') { + isIsolateAction = true; + } + if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) { + isSuccessful = true; + } + } + + const actionEventTitle = isIsolateAction + ? i18.ACTIVITY_LOG.LogEntry.action.isolatedAction + : i18.ACTIVITY_LOG.LogEntry.action.unisolatedAction; + + const getResponseEventTitle = () => { + if (isIsolateAction) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; + } else { + return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; + } + } else { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful; + } else { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed; + } + } + }; + + return { + actionEventTitle, + avatarSize, + commentText, + commentType, + displayComment, + displayResponseEvent, + iconType, + isResponseEvent, + isSuccessful, + responseEventTitle: getResponseEventTitle(), + username, + }; + }, [logEntry]); +}; + +const StyledEuiComment = styled(EuiComment)` + .euiCommentEvent__headerTimestamp { + display: flex; + :before { + content: ''; + background-color: ${(props) => props.theme.eui.euiColorInk}; + display: block; + width: ${(props) => props.theme.eui.euiBorderWidthThick}; + height: ${(props) => props.theme.eui.euiBorderWidthThick}; + margin: 0 ${(props) => props.theme.eui.euiSizeXS} 0 ${(props) => props.theme.eui.euiSizeS}; + border-radius: 50%; + content: ''; + margin: 0 8px 0 4px; + border-radius: 50%; + position: relative; + top: 10px; + } } -); +`; + +export const LogEntry = memo(({ logEntry }: { logEntry: Immutable }) => { + const { + actionEventTitle, + avatarSize, + commentText, + commentType, + displayComment, + displayResponseEvent, + iconType, + isResponseEvent, + isSuccessful, + responseEventTitle, + username, + } = useLogEntryUIProps(logEntry); + + return ( + {displayResponseEvent ? responseEventTitle : actionEventTitle}
    } + timelineIcon={ + + } + data-test-subj="timelineEntry" + > + {displayComment ? ( + +

    {commentText}

    +
    + ) : undefined} + + ); +}); LogEntry.displayName = 'LogEntry'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx new file mode 100644 index 00000000000000..3ff311cd8a139c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiAvatar, EuiAvatarProps } from '@elastic/eui'; +import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; + +export const LogEntryTimelineIcon = memo( + ({ + avatarSize, + isResponseEvent, + isSuccessful, + iconType, + }: { + avatarSize: EuiAvatarProps['size']; + isResponseEvent: boolean; + isSuccessful: boolean; + iconType: EuiAvatarProps['iconType']; + }) => { + const euiTheme = useEuiTheme(); + + return ( + + ); + } +); + +LogEntryTimelineIcon.displayName = 'LogEntryTimelineIcon'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 50c91730e332c7..4395e3965ea009 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -7,21 +7,48 @@ import React, { memo, useCallback } from 'react'; -import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingContent, EuiSpacer } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; import * as i18 from '../translations'; import { SearchBar } from '../../../../components/search_bar'; -import { Immutable, EndpointAction } from '../../../../../../common/endpoint/types'; +import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types'; import { AsyncResourceState } from '../../../../state'; +import { useEndpointSelector } from '../hooks'; +import { EndpointAction } from '../../store/action'; +import { + getActivityLogDataPaging, + getActivityLogError, + getActivityLogIterableData, + getActivityLogRequestLoaded, + getActivityLogRequestLoading, +} from '../../store/selectors'; export const EndpointActivityLog = memo( - ({ endpointActions }: { endpointActions: AsyncResourceState> }) => { + ({ activityLog }: { activityLog: AsyncResourceState> }) => { + const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); + const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded); + const activityLogData = useEndpointSelector(getActivityLogIterableData); + const activityLogError = useEndpointSelector(getActivityLogError); + const dispatch = useDispatch<(a: EndpointAction) => void>(); + const { page, pageSize } = useEndpointSelector(getActivityLogDataPaging); // TODO const onSearch = useCallback(() => {}, []); + + const getActivityLog = useCallback(() => { + dispatch({ + type: 'appRequestedEndpointActivityLog', + payload: { + page: page + 1, + pageSize, + }, + }); + }, [dispatch, page, pageSize]); + return ( <> - {endpointActions.type !== 'LoadedResourceState' || !endpointActions.data.length ? ( + {activityLogLoading || activityLogError ? ( - {endpointActions.data.map((endpointAction) => ( - - ))} + {activityLogLoading ? ( + + ) : ( + activityLogLoaded && + activityLogData.map((logEntry) => ( + + )) + )} + + {'show more'} + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index fccd48cbde67cc..d839bbfaae8756 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -8,7 +8,7 @@ import React, { ComponentType } from 'react'; import moment from 'moment'; -import { EndpointAction, Immutable } from '../../../../../../common/endpoint/types'; +import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types'; import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs'; import { EndpointActivityLog } from './endpoint_activity_log'; import { EndpointDetailsFlyout } from '.'; @@ -17,63 +17,93 @@ import { AsyncResourceState } from '../../../../state'; export const dummyEndpointActivityLog = ( selectedEndpoint: string = '' -): AsyncResourceState> => ({ +): AsyncResourceState> => ({ type: 'LoadedResourceState', - data: [ - { - action_id: '1', - '@timestamp': moment().subtract(1, 'hours').fromNow().toString(), - expiration: moment().add(3, 'day').fromNow().toString(), - type: 'INPUT_ACTION', - input_type: 'endpoint', - agents: [`${selectedEndpoint}`], - user_id: 'sys', - data: { - command: 'isolate', + data: { + total: 20, + page: 1, + pageSize: 50, + data: [ + { + type: 'action', + item: { + id: '', + data: { + action_id: '1', + '@timestamp': moment().subtract(1, 'hours').fromNow().toString(), + expiration: moment().add(3, 'day').fromNow().toString(), + type: 'INPUT_ACTION', + input_type: 'endpoint', + agents: [`${selectedEndpoint}`], + user_id: 'sys', + data: { + command: 'isolate', + }, + }, + }, }, - }, - { - action_id: '2', - '@timestamp': moment().subtract(2, 'hours').fromNow().toString(), - expiration: moment().add(1, 'day').fromNow().toString(), - type: 'INPUT_ACTION', - input_type: 'endpoint', - agents: [`${selectedEndpoint}`], - user_id: 'ash', - data: { - command: 'isolate', - comment: 'Sem et tortor consequat id porta nibh venenatis cras sed.', + { + type: 'action', + item: { + id: '', + data: { + action_id: '2', + '@timestamp': moment().subtract(2, 'hours').fromNow().toString(), + expiration: moment().add(1, 'day').fromNow().toString(), + type: 'INPUT_ACTION', + input_type: 'endpoint', + agents: [`${selectedEndpoint}`], + user_id: 'ash', + data: { + command: 'isolate', + comment: 'Sem et tortor consequat id porta nibh venenatis cras sed.', + }, + }, + }, }, - }, - { - action_id: '3', - '@timestamp': moment().subtract(4, 'hours').fromNow().toString(), - expiration: moment().add(1, 'day').fromNow().toString(), - type: 'INPUT_ACTION', - input_type: 'endpoint', - agents: [`${selectedEndpoint}`], - user_id: 'someone', - data: { - command: 'unisolate', - comment: 'Turpis egestas pretium aenean pharetra.', + { + type: 'action', + item: { + id: '', + data: { + action_id: '3', + '@timestamp': moment().subtract(4, 'hours').fromNow().toString(), + expiration: moment().add(1, 'day').fromNow().toString(), + type: 'INPUT_ACTION', + input_type: 'endpoint', + agents: [`${selectedEndpoint}`], + user_id: 'someone', + data: { + command: 'unisolate', + comment: 'Turpis egestas pretium aenean pharetra.', + }, + }, + }, }, - }, - { - action_id: '4', - '@timestamp': moment().subtract(1, 'day').fromNow().toString(), - expiration: moment().add(3, 'day').fromNow().toString(), - type: 'INPUT_ACTION', - input_type: 'endpoint', - agents: [`${selectedEndpoint}`], - user_id: 'ash', - data: { - command: 'isolate', - comment: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, \ - sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + { + type: 'action', + item: { + id: '', + data: { + action_id: '4', + '@timestamp': moment().subtract(1, 'day').fromNow().toString(), + expiration: moment().add(3, 'day').fromNow().toString(), + type: 'INPUT_ACTION', + input_type: 'endpoint', + agents: [`${selectedEndpoint}`], + user_id: 'ash', + data: { + command: 'isolate', + comment: + 'Lorem \ + ipsum dolor sit amet, consectetur adipiscing elit, \ + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + }, + }, }, - }, - ], + ], + }, }); export default { @@ -100,12 +130,12 @@ export const Tabs = () => ( { id: 'activity_log', name: 'Activity Log', - content: ActivityLog(), + content: ActivityLogMarkup(), }, ]} /> ); -export const ActivityLog = () => ( - +export const ActivityLogMarkup = () => ( + ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 89c0e3e6a3e067..c39a17e98c76ab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -33,7 +33,6 @@ import { detailsLoading, getActivityLogData, getActivityLogError, - getActivityLogRequestLoading, showView, policyResponseConfigurations, policyResponseActions, @@ -87,7 +86,6 @@ export const EndpointDetailsFlyout = memo(() => { } = queryParams; const activityLog = useEndpointSelector(getActivityLogData); - const activityLoading = useEndpointSelector(getActivityLogRequestLoading); const activityError = useEndpointSelector(getActivityLogError); const hostDetails = useEndpointSelector(detailsData); const hostDetailsLoading = useEndpointSelector(detailsLoading); @@ -121,12 +119,8 @@ export const EndpointDetailsFlyout = memo(() => { }, { id: EndpointDetailsTabsTypes.activityLog, - name: i18.ACTIVITY_LOG, - content: activityLoading ? ( - ContentLoadingMarkup - ) : ( - - ), + name: i18.ACTIVITY_LOG.tabTitle, + content: , }, ]; @@ -175,7 +169,7 @@ export const EndpointDetailsFlyout = memo(() => { paddingSize="m" > - {hostDetailsLoading || activityLoading ? ( + {hostDetailsLoading ? ( ) : ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index fd2806713183bf..1a7889f22db16c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -11,9 +11,53 @@ export const OVERVIEW = i18n.translate('xpack.securitySolution.endpointDetails.o defaultMessage: 'Overview', }); -export const ACTIVITY_LOG = i18n.translate('xpack.securitySolution.endpointDetails.activityLog', { - defaultMessage: 'Activity Log', -}); +export const ACTIVITY_LOG = { + tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', { + defaultMessage: 'Activity Log', + }), + LogEntry: { + action: { + isolatedAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.isolated', + { + defaultMessage: 'isolated host', + } + ), + unisolatedAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.unisolated', + { + defaultMessage: 'unisolated host', + } + ), + }, + response: { + isolationSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationSuccessful', + { + defaultMessage: 'host isolation successful', + } + ), + isolationFailed: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationFailed', + { + defaultMessage: 'host isolation failed', + } + ), + unisolationSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful', + { + defaultMessage: 'host unisolation successful', + } + ), + unisolationFailed: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationFailed', + { + defaultMessage: 'host unisolation failed', + } + ), + }, + }, +}; export const SEARCH_ACTIVITY_LOG = i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.search', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts new file mode 100644 index 00000000000000..9b737217753824 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -0,0 +1,186 @@ +/* + * Copyright 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { KibanaResponseFactory, RequestHandler, RouteConfig } from 'kibana/server'; +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; +import { + EndpointActionLogRequestParams, + EndpointActionLogRequestQuery, + EndpointActionLogRequestSchema, +} from '../../../../common/endpoint/schema/actions'; +import { ENDPOINT_ACTION_LOG_ROUTE } from '../../../../common/endpoint/constants'; +import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; +import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; +import { + createMockEndpointAppContextServiceStartContract, + createRouteHandlerContext, +} from '../../mocks'; +import { registerActionAuditLogRoutes } from './audit_log'; +import uuid from 'uuid'; +import { aMockAction, aMockResponse, MockAction, mockAuditLog, MockResponse } from './mocks'; +import { SecuritySolutionRequestHandlerContext } from '../../../types'; +import { ActivityLog } from '../../../../common/endpoint/types'; + +describe('Action Log API', () => { + describe('schema', () => { + it('should require at least 1 agent ID', () => { + expect(() => { + EndpointActionLogRequestSchema.params.validate({}); // no agent_ids provided + }).toThrow(); + }); + + it('should accept a single agent ID', () => { + expect(() => { + EndpointActionLogRequestSchema.params.validate({ agent_id: uuid.v4() }); + }).not.toThrow(); + }); + + it('should work without query params', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({}); + }).not.toThrow(); + }); + + it('should work with query params', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ page: 10, page_size: 100 }); + }).not.toThrow(); + }); + + it('should not work without allowed page and page_size params', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ page_size: 101 }); + }).toThrow(); + }); + }); + + describe('response', () => { + const mockID = 'XYZABC-000'; + const actionID = 'some-known-actionid'; + let endpointAppContextService: EndpointAppContextService; + + // convenience for calling the route and handler for audit log + let getActivityLog: ( + query?: EndpointActionLogRequestQuery + ) => Promise>; + // convenience for injecting mock responses for actions index and responses + let havingActionsAndResponses: (actions: MockAction[], responses: any[]) => void; + + let havingErrors: () => void; + + beforeEach(() => { + const esClientMock = elasticsearchServiceMock.createScopedClusterClient(); + const routerMock = httpServiceMock.createRouter(); + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); + + registerActionAuditLogRoutes(routerMock, { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); + + getActivityLog = async (query?: any): Promise> => { + const req = httpServerMock.createKibanaRequest({ + params: { agent_id: mockID }, + query, + }); + const mockResponse = httpServerMock.createResponseFactory(); + const [, routeHandler]: [ + RouteConfig, + RequestHandler< + EndpointActionLogRequestParams, + EndpointActionLogRequestQuery, + unknown, + SecuritySolutionRequestHandlerContext + > + ] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith(ENDPOINT_ACTION_LOG_ROUTE) + )!; + await routeHandler( + createRouteHandlerContext(esClientMock, savedObjectsClientMock.create()), + req, + mockResponse + ); + + return mockResponse; + }; + + havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => { + const actionsData = actions.map((a) => ({ + _index: '.fleet-actions-7', + _source: a.build(), + })); + const responsesData = responses.map((r) => ({ + _index: '.ds-.fleet-actions-results-2021.06.09-000001', + _source: r.build(), + })); + const mockResult = mockAuditLog([...actionsData, ...responsesData]); + esClientMock.asCurrentUser.search = jest + .fn() + .mockImplementationOnce(() => Promise.resolve(mockResult)); + }; + + havingErrors = () => { + esClientMock.asCurrentUser.search = jest.fn().mockImplementationOnce(() => + Promise.resolve(() => { + throw new Error(); + }) + ); + }; + }); + + afterEach(() => { + endpointAppContextService.stop(); + }); + + it('should return an empty array when nothing in audit log', async () => { + havingActionsAndResponses([], []); + const response = await getActivityLog(); + expect(response.ok).toBeCalled(); + expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0); + }); + + it('should have actions and action responses', async () => { + havingActionsAndResponses( + [ + aMockAction().withAgent(mockID).withAction('isolate'), + aMockAction().withAgent(mockID).withAction('unisolate'), + aMockAction().withAgent(mockID).withAction('isolate'), + ], + [aMockResponse(actionID, mockID), aMockResponse(actionID, mockID)] + ); + const response = await getActivityLog(); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(5); + expect(responseBody.data.filter((x: any) => x.type === 'response')).toHaveLength(2); + expect(responseBody.data.filter((x: any) => x.type === 'action')).toHaveLength(3); + }); + + it('should throw errors when no results for some agentID', async () => { + havingErrors(); + + try { + await getActivityLog(); + } catch (error) { + expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`); + } + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts index fdbb9608463e9c..b0cea299af60d7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts @@ -5,55 +5,34 @@ * 2.0. */ -import { TypeOf } from '@kbn/config-schema'; import { RequestHandler } from 'kibana/server'; -import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; -import { EndpointActionLogRequestSchema } from '../../../../common/endpoint/schema/actions'; - +import { + EndpointActionLogRequestParams, + EndpointActionLogRequestQuery, +} from '../../../../common/endpoint/schema/actions'; +import { getAuditLogResponse } from './service'; import { SecuritySolutionRequestHandlerContext } from '../../../types'; import { EndpointAppContext } from '../../types'; export const actionsLogRequestHandler = ( endpointContext: EndpointAppContext ): RequestHandler< - TypeOf, - unknown, + EndpointActionLogRequestParams, + EndpointActionLogRequestQuery, unknown, SecuritySolutionRequestHandlerContext > => { const logger = endpointContext.logFactory.get('audit_log'); + return async (context, req, res) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; - let result; - try { - result = await esClient.search({ - index: AGENT_ACTIONS_INDEX, - body: { - query: { - match: { - agents: req.params.agent_id, - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }); - } catch (error) { - logger.error(error); - throw error; - } - if (result?.statusCode !== 200) { - logger.error(`Error fetching actions log for agent_id ${req.params.agent_id}`); - throw new Error(`Error fetching actions log for agent_id ${req.params.agent_id}`); - } + const { + params: { agent_id: elasticAgentId }, + query: { page, page_size: pageSize }, + } = req; + const body = await getAuditLogResponse({ elasticAgentId, page, pageSize, context, logger }); return res.ok({ - body: result.body.hits.hits.map((e) => e._source), + body, }); }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts index 34f7d140a78de4..f74ae07fdfac4d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts @@ -18,6 +18,29 @@ import { ISOLATION_ACTIONS, } from '../../../../common/endpoint/types'; +export const mockAuditLog = (results: any = []): ApiResponse => { + return { + body: { + hits: { + total: results.length, + hits: results.map((a: any) => { + const _index = a._index; + delete a._index; + const _source = a; + return { + _index, + _source, + }; + }), + }, + }, + statusCode: 200, + headers: {}, + warnings: [], + meta: {} as any, + }; +}; + export const mockSearchResult = (results: any = []): ApiResponse => { return { body: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts new file mode 100644 index 00000000000000..20b29694a1df12 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts @@ -0,0 +1,110 @@ +/* + * Copyright 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 { Logger } from 'kibana/server'; +import type { estypes } from '@elastic/elasticsearch'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../../fleet/common'; +import { SecuritySolutionRequestHandlerContext } from '../../../types'; + +export const getAuditLogESQuery = ({ + elasticAgentId, + from, + size, +}: { + elasticAgentId: string; + from: number; + size: number; +}): estypes.SearchRequest => { + return { + index: [AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX], + size, + from, + body: { + query: { + bool: { + should: [ + { terms: { agents: [elasticAgentId] } }, + { terms: { agent_id: [elasticAgentId] } }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; +}; + +export const getAuditLogResponse = async ({ + elasticAgentId, + page, + pageSize, + context, + logger, +}: { + elasticAgentId: string; + page: number; + pageSize: number; + context: SecuritySolutionRequestHandlerContext; + logger: Logger; +}): Promise<{ + total: number; + page: number; + pageSize: number; + data: Array<{ + type: 'action' | 'response'; + item: { + id: string; + data: unknown; + }; + }>; +}> => { + const size = pageSize; + const from = page <= 1 ? 0 : page * pageSize - pageSize + 1; + + const options = { + headers: { + 'X-elastic-product-origin': 'fleet', + }, + ignore: [404], + }; + const esClient = context.core.elasticsearch.client.asCurrentUser; + let result; + const params = getAuditLogESQuery({ + elasticAgentId, + from, + size, + }); + + try { + result = await esClient.search(params, options); + } catch (error) { + logger.error(error); + throw error; + } + if (result?.statusCode !== 200) { + logger.error(`Error fetching actions log for agent_id ${elasticAgentId}`); + throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`); + } + + return { + total: + typeof result.body.hits.total === 'number' + ? result.body.hits.total + : result.body.hits.total.value, + page, + pageSize, + data: result.body.hits.hits.map((e) => ({ + type: e._index.startsWith('.fleet-actions') ? 'action' : 'response', + item: { id: e._id, data: e._source }, + })), + }; +}; From e0a1a34fbc5a92cae76638514779a89cad853fa1 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 14 Jun 2021 15:57:03 +0200 Subject: [PATCH 62/99] [Uptime] Align synthetics report types (#101855) --- .../PageLoadDistribution/index.tsx | 2 +- .../components/filter_label.test.tsx | 8 +-- .../apm/service_latency_config.ts | 5 +- .../apm/service_throughput_config.ts | 57 --------------- .../configurations/constants/constants.ts | 34 +-------- .../constants/field_names/synthetics.ts | 8 +++ .../configurations/constants/labels.ts | 55 +++------------ .../configurations/default_configs.ts | 64 +++++++---------- .../configurations/lens_attributes.test.ts | 6 +- .../configurations/lens_attributes.ts | 19 +++-- .../logs/logs_frequency_config.ts | 41 ----------- .../metrics/cpu_usage_config.ts | 11 +-- .../metrics/memory_usage_config.ts | 11 +-- .../metrics/network_activity_config.ts | 11 +-- .../rum/core_web_vitals_config.ts | 5 +- ..._config.ts => data_distribution_config.ts} | 5 +- ...ends_config.ts => kpi_over_time_config.ts} | 5 +- ..._config.ts => data_distribution_config.ts} | 25 ++++--- .../synthetics/kpi_over_time_config.ts | 69 +++++++++++++++++++ .../synthetics/monitor_pings_config.ts | 50 -------------- .../exploratory_view.test.tsx | 2 +- .../exploratory_view/exploratory_view.tsx | 9 ++- .../exploratory_view/header/header.test.tsx | 2 +- .../hooks/use_lens_attributes.ts | 13 ++-- .../shared/exploratory_view/rtl_helpers.tsx | 2 +- .../columns/data_types_col.test.tsx | 2 +- .../series_builder/columns/data_types_col.tsx | 3 +- .../columns/operation_type_select.tsx | 6 ++ .../columns/report_breakdowns.test.tsx | 8 +-- .../columns/report_definition_col.test.tsx | 8 +-- .../columns/report_filters.test.tsx | 4 +- .../columns/report_types_col.test.tsx | 8 +-- .../series_builder/series_builder.tsx | 26 ++----- .../series_date_picker.test.tsx | 10 +-- .../series_editor/chart_edit_options.tsx | 2 +- .../series_editor/columns/breakdowns.test.tsx | 7 +- .../series_editor/columns/chart_options.tsx | 7 +- .../series_editor/columns/series_actions.tsx | 3 +- .../series_editor/selected_filters.test.tsx | 5 +- .../series_editor/series_editor.tsx | 30 ++++---- .../shared/exploratory_view/types.ts | 14 +--- .../common/charts/ping_histogram.tsx | 2 +- .../monitor_duration_container.tsx | 2 +- 43 files changed, 247 insertions(+), 419 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/{performance_dist_config.ts => data_distribution_config.ts} (94%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/{kpi_trends_config.ts => kpi_over_time_config.ts} (95%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/{monitor_duration_config.ts => data_distribution_config.ts} (61%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index a8435beca1e4a5..adfb45303a4f34 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -89,7 +89,7 @@ export function PageLoadDistribution() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'pld', + reportType: 'dist', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index c042ba9d0bcf8a..7c772cb8dbdbcd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -26,7 +26,7 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-trends'} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} /> ); @@ -49,7 +49,7 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-trends'} + seriesId={'kpi-over-time'} removeFilter={removeFilter} /> ); @@ -71,7 +71,7 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={'kpi-trends'} + seriesId={'kpi-over-time'} removeFilter={removeFilter} /> ); @@ -96,7 +96,7 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={'kpi-trends'} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts index 1c2627dac30e7d..7c3abba3e5b051 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_latency_config.ts @@ -10,10 +10,9 @@ import { FieldLabels } from '../constants'; import { buildPhraseFilter } from '../utils'; import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames'; -export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { +export function getServiceLatencyLensConfig({ indexPattern }: ConfigProps): DataSeries { return { - id: seriesId, - reportType: 'service-latency', + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts deleted file mode 100644 index 2de2cbdfd75a64..00000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/service_throughput_config.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants/constants'; -import { buildPhraseFilter } from '../utils'; -import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames'; - -export function getServiceThroughputLensConfig({ - seriesId, - indexPattern, -}: ConfigProps): DataSeries { - return { - id: seriesId, - reportType: 'service-throughput', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'average', - sourceField: 'transaction.duration.us', - label: 'Throughput', - }, - ], - hasOperationType: true, - defaultFilters: [ - 'user_agent.name', - 'user_agent.os.name', - 'client.geo.country_name', - 'user_agent.device.name', - ], - breakdowns: [ - 'user_agent.name', - 'user_agent.os.name', - 'client.geo.country_name', - 'user_agent.device.name', - ], - filters: buildPhraseFilter('transaction.type', 'request', indexPattern), - labels: { ...FieldLabels, [TRANSACTION_DURATION]: 'Throughput' }, - reportDefinitions: [ - { - field: 'service.name', - required: true, - }, - { - field: 'service.environment', - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e1142a071aab5a..26459e676de088 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AppDataType, ReportViewTypeId } from '../../types'; +import { ReportViewTypeId } from '../../types'; import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; import { AGENT_HOST_LABEL, @@ -13,7 +13,6 @@ import { BROWSER_VERSION_LABEL, CLS_LABEL, CORE_WEB_VITALS_LABEL, - CPU_USAGE_LABEL, DEVICE_LABEL, ENVIRONMENT_LABEL, FCP_LABEL, @@ -23,25 +22,18 @@ import { KPI_LABEL, LCP_LABEL, LOCATION_LABEL, - LOGS_FREQUENCY_LABEL, - MEMORY_USAGE_LABEL, METRIC_LABEL, - MONITOR_DURATION_LABEL, MONITOR_ID_LABEL, MONITOR_NAME_LABEL, MONITOR_STATUS_LABEL, MONITOR_TYPE_LABEL, - NETWORK_ACTIVITY_LABEL, OBSERVER_LOCATION_LABEL, OS_LABEL, PERF_DIST_LABEL, PORT_LABEL, - SERVICE_LATENCY_LABEL, SERVICE_NAME_LABEL, - SERVICE_THROUGHPUT_LABEL, TAGS_LABEL, TBT_LABEL, - UPTIME_PINGS_LABEL, URL_LABEL, } from './labels'; @@ -83,33 +75,11 @@ export const FieldLabels: Record = { }; export const DataViewLabels: Record = { - pld: PERF_DIST_LABEL, - upd: MONITOR_DURATION_LABEL, - upp: UPTIME_PINGS_LABEL, - svl: SERVICE_LATENCY_LABEL, + dist: PERF_DIST_LABEL, kpi: KIP_OVER_TIME_LABEL, - tpt: SERVICE_THROUGHPUT_LABEL, - cpu: CPU_USAGE_LABEL, - logs: LOGS_FREQUENCY_LABEL, - mem: MEMORY_USAGE_LABEL, - nwk: NETWORK_ACTIVITY_LABEL, cwv: CORE_WEB_VITALS_LABEL, }; -export const ReportToDataTypeMap: Record = { - upd: 'synthetics', - upp: 'synthetics', - tpt: 'apm', - svl: 'apm', - kpi: 'ux', - pld: 'ux', - nwk: 'infra_metrics', - mem: 'infra_metrics', - logs: 'infra_logs', - cpu: 'infra_metrics', - cwv: 'ux', -}; - export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const OPERATION_COLUMN = 'operation'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts new file mode 100644 index 00000000000000..edf8b7fb9d741d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const MONITOR_DURATION_US = 'monitor.duration.us'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index 92150a76319f8a..b5816daa419dfd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -100,6 +100,10 @@ export const PAGES_LOADED_LABEL = i18n.translate( } ); +export const PINGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.pings', { + defaultMessage: 'Pings', +}); + export const MONITOR_ID_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.monitorId', { @@ -165,42 +169,6 @@ export const PERF_DIST_LABEL = i18n.translate( } ); -export const MONITOR_DURATION_LABEL = i18n.translate( - 'xpack.observability.expView.fieldLabels.monitorDuration', - { - defaultMessage: 'Uptime monitor duration', - } -); - -export const UPTIME_PINGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.pings', { - defaultMessage: 'Uptime pings', -}); - -export const SERVICE_LATENCY_LABEL = i18n.translate( - 'xpack.observability.expView.fieldLabels.serviceLatency', - { - defaultMessage: 'APM Service latency', - } -); - -export const SERVICE_THROUGHPUT_LABEL = i18n.translate( - 'xpack.observability.expView.fieldLabels.serviceThroughput', - { - defaultMessage: 'APM Service throughput', - } -); - -export const CPU_USAGE_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.cpuUsage', { - defaultMessage: 'System CPU usage', -}); - -export const NETWORK_ACTIVITY_LABEL = i18n.translate( - 'xpack.observability.expView.fieldLabels.networkActivity', - { - defaultMessage: 'Network activity', - } -); - export const CORE_WEB_VITALS_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.coreWebVitals', { @@ -215,13 +183,6 @@ export const MEMORY_USAGE_LABEL = i18n.translate( } ); -export const LOGS_FREQUENCY_LABEL = i18n.translate( - 'xpack.observability.expView.fieldLabels.logsFrequency', - { - defaultMessage: 'Logs frequency', - } -); - export const KIP_OVER_TIME_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.kpiOverTime', { @@ -243,10 +204,10 @@ export const WEB_APPLICATION_LABEL = i18n.translate( } ); -export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.up', { - defaultMessage: 'Up', +export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.upPings', { + defaultMessage: 'Up Pings', }); -export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.down', { - defaultMessage: 'Down', +export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.downPings', { + defaultMessage: 'Down Pings', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 797ee0c2e09777..13a7900ef5764d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -5,51 +5,37 @@ * 2.0. */ -import { ReportViewTypes } from '../types'; -import { getPerformanceDistLensConfig } from './rum/performance_dist_config'; -import { getMonitorDurationConfig } from './synthetics/monitor_duration_config'; -import { getServiceLatencyLensConfig } from './apm/service_latency_config'; -import { getMonitorPingsConfig } from './synthetics/monitor_pings_config'; -import { getServiceThroughputLensConfig } from './apm/service_throughput_config'; -import { getKPITrendsLensConfig } from './rum/kpi_trends_config'; -import { getCPUUsageLensConfig } from './metrics/cpu_usage_config'; -import { getMemoryUsageLensConfig } from './metrics/memory_usage_config'; -import { getNetworkActivityLensConfig } from './metrics/network_activity_config'; -import { getLogsFrequencyLensConfig } from './logs/logs_frequency_config'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { AppDataType, ReportViewTypes } from '../types'; +import { getRumDistributionConfig } from './rum/data_distribution_config'; +import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config'; +import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; +import { getKPITrendsLensConfig } from './rum/kpi_over_time_config'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; interface Props { reportType: keyof typeof ReportViewTypes; - seriesId: string; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; + dataType: AppDataType; } -export const getDefaultConfigs = ({ reportType, seriesId, indexPattern }: Props) => { - switch (ReportViewTypes[reportType]) { - case 'page-load-dist': - return getPerformanceDistLensConfig({ seriesId, indexPattern }); - case 'kpi-trends': - return getKPITrendsLensConfig({ seriesId, indexPattern }); - case 'core-web-vitals': - return getCoreWebVitalsConfig({ seriesId, indexPattern }); - case 'uptime-duration': - return getMonitorDurationConfig({ seriesId, indexPattern }); - case 'uptime-pings': - return getMonitorPingsConfig({ seriesId, indexPattern }); - case 'service-latency': - return getServiceLatencyLensConfig({ seriesId, indexPattern }); - case 'service-throughput': - return getServiceThroughputLensConfig({ seriesId, indexPattern }); - case 'cpu-usage': - return getCPUUsageLensConfig({ seriesId }); - case 'memory-usage': - return getMemoryUsageLensConfig({ seriesId }); - case 'network-activity': - return getNetworkActivityLensConfig({ seriesId }); - case 'logs-frequency': - return getLogsFrequencyLensConfig({ seriesId }); +export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { + switch (dataType) { + case 'ux': + if (reportType === 'dist') { + return getRumDistributionConfig({ indexPattern }); + } + if (reportType === 'cwv') { + return getCoreWebVitalsConfig({ indexPattern }); + } + return getKPITrendsLensConfig({ indexPattern }); + case 'synthetics': + if (reportType === 'dist') { + return getSyntheticsDistributionConfig({ indexPattern }); + } + return getSyntheticsKPIConfig({ indexPattern }); + default: - return getKPITrendsLensConfig({ seriesId, indexPattern }); + return getKPITrendsLensConfig({ indexPattern }); } }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 6976b55921b090..8b21df64a3c910 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -15,9 +15,9 @@ describe('Lens Attribute', () => { mockAppIndexPattern(); const reportViewConfig = getDefaultConfigs({ - reportType: 'pld', + reportType: 'dist', + dataType: 'ux', indexPattern: mockIndexPattern, - seriesId: 'series-id', }); let lnsAttr: LensAttributes; @@ -73,6 +73,7 @@ describe('Lens Attribute', () => { readFromDocValues: true, }, fieldName: 'transaction.duration.us', + columnLabel: 'Page load time', }) ); }); @@ -95,6 +96,7 @@ describe('Lens Attribute', () => { readFromDocValues: true, }, fieldName: LCP_FIELD, + columnLabel: 'Largest contentful paint', }) ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 89fc9ca5fcc58e..bc535e29ab4350 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -55,6 +55,7 @@ export const parseCustomFieldName = ( let fieldName = sourceField; let columnType; let columnFilters; + let columnLabel; const rdf = reportViewConfig.reportDefinitions ?? []; @@ -69,15 +70,17 @@ export const parseCustomFieldName = ( ); columnType = currField?.columnType; columnFilters = currField?.columnFilters; + columnLabel = currField?.label; } } else if (customField.options?.[0].field || customField.options?.[0].id) { fieldName = customField.options?.[0].field || customField.options?.[0].id; columnType = customField.options?.[0].columnType; columnFilters = customField.options?.[0].columnFilters; + columnLabel = customField.options?.[0].label; } } - return { fieldName, columnType, columnFilters }; + return { fieldName, columnType, columnFilters, columnLabel }; }; export class LensAttributes { @@ -260,12 +263,14 @@ export class LensAttributes { label?: string, colIndex?: number ) { - const { fieldMeta, columnType, fieldName, columnFilters } = this.getFieldMeta(sourceField); + const { fieldMeta, columnType, fieldName, columnFilters, columnLabel } = this.getFieldMeta( + sourceField + ); const { type: fieldType } = fieldMeta ?? {}; if (fieldName === 'Records' || columnType === FILTER_RECORDS) { return this.getRecordsColumn( - label, + columnLabel || label, colIndex !== undefined ? columnFilters?.[colIndex] : undefined ); } @@ -274,7 +279,7 @@ export class LensAttributes { return this.getDateHistogramColumn(fieldName); } if (fieldType === 'number') { - return this.getNumberColumn(fieldName, columnType, operationType, label); + return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label); } // FIXME review my approach again @@ -286,11 +291,13 @@ export class LensAttributes { } getFieldMeta(sourceField: string) { - const { fieldName, columnType, columnFilters } = this.getCustomFieldName(sourceField); + const { fieldName, columnType, columnFilters, columnLabel } = this.getCustomFieldName( + sourceField + ); const fieldMeta = this.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType, columnFilters }; + return { fieldMeta, fieldName, columnType, columnFilters, columnLabel }; } getMainYAxis() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts deleted file mode 100644 index 97d915ede01a90..00000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs/logs_frequency_config.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; - -interface Props { - seriesId: string; -} - -export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries { - return { - id: seriesId, - reportType: 'logs-frequency', - defaultSeriesType: 'line', - seriesTypes: ['line', 'bar'], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'count', - }, - ], - hasOperationType: false, - defaultFilters: [], - breakdowns: ['agent.hostname'], - filters: [], - labels: { ...FieldLabels }, - reportDefinitions: [ - { - field: 'agent.hostname', - required: true, - }, - ], - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts index 28b381bd124733..2d44e122af82da 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/cpu_usage_config.ts @@ -5,17 +5,12 @@ * 2.0. */ -import { DataSeries } from '../../types'; +import { DataSeries, ConfigProps } from '../../types'; import { FieldLabels } from '../constants'; -interface Props { - seriesId: string; -} - -export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries { +export function getCPUUsageLensConfig({}: ConfigProps): DataSeries { return { - id: seriesId, - reportType: 'cpu-usage', + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts index 2bd0e4b032778f..deaa551dce6579 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/memory_usage_config.ts @@ -5,17 +5,12 @@ * 2.0. */ -import { DataSeries } from '../../types'; +import { DataSeries, ConfigProps } from '../../types'; import { FieldLabels } from '../constants'; -interface Props { - seriesId: string; -} - -export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries { +export function getMemoryUsageLensConfig({}: ConfigProps): DataSeries { return { - id: seriesId, - reportType: 'memory-usage', + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts index 924701bc13490c..d27cdba207d633 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/metrics/network_activity_config.ts @@ -5,17 +5,12 @@ * 2.0. */ -import { DataSeries } from '../../types'; +import { DataSeries, ConfigProps } from '../../types'; import { FieldLabels } from '../constants'; -interface Props { - seriesId: string; -} - -export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries { +export function getNetworkActivityLensConfig({}: ConfigProps): DataSeries { return { - id: seriesId, - reportType: 'network-activity', + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index de9ea12be20cf7..e34d8b0dcfdd01 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -27,13 +27,12 @@ import { SERVICE_ENVIRONMENT, } from '../constants/elasticsearch_fieldnames'; -export function getCoreWebVitalsConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { +export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): DataSeries { const statusPallete = euiPaletteForStatus(3); return { - id: seriesId, defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: 'kpi-trends', + reportType: 'core-web-vitals', seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts similarity index 94% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index 4a1521c834806c..812f1b2e4cf33b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/performance_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -39,10 +39,9 @@ import { WEB_APPLICATION_LABEL, } from '../constants/labels'; -export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { +export function getRumDistributionConfig({ indexPattern }: ConfigProps): DataSeries { return { - id: seriesId ?? 'unique-key', - reportType: 'page-load-dist', + reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts similarity index 95% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index f6c683caaa0391..12d66c55c7d00a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_trends_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -39,12 +39,11 @@ import { WEB_APPLICATION_LABEL, } from '../constants/labels'; -export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { +export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): DataSeries { return { - id: seriesId, defaultSeriesType: 'bar_stacked', - reportType: 'kpi-trends', seriesTypes: [], + reportType: 'kpi-over-time', xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts similarity index 61% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts rename to x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 5e8a43ccf2ef46..854f844db047d9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_duration_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -6,27 +6,25 @@ */ import { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; +import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; -import { MONITORS_DURATION_LABEL } from '../constants/labels'; +import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries { return { - id: seriesId, - reportType: 'uptime-duration', + reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { - sourceField: '@timestamp', + sourceField: 'performance.metric', }, yAxisColumns: [ { - operationType: 'average', - sourceField: 'monitor.duration.us', - label: MONITORS_DURATION_LABEL, + sourceField: RECORDS_FIELD, + label: PINGS_LABEL, }, ], - hasOperationType: true, + hasOperationType: false, defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'], breakdowns: [ 'observer.geo.name', @@ -44,6 +42,13 @@ export function getMonitorDurationConfig({ seriesId, indexPattern }: ConfigProps { field: 'url.full', }, + { + field: 'performance.metric', + custom: true, + options: [ + { label: 'Monitor duration', id: 'monitor.duration.us', field: 'monitor.duration.us' }, + ], + }, ], labels: { ...FieldLabels, 'monitor.duration.us': MONITORS_DURATION_LABEL }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts new file mode 100644 index 00000000000000..3e928454363638 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -0,0 +1,69 @@ +/* + * Copyright 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 { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels, OPERATION_COLUMN } from '../constants'; +import { buildExistsFilter } from '../utils'; +import { DOWN_LABEL, MONITORS_DURATION_LABEL, UP_LABEL } from '../constants/labels'; +import { MONITOR_DURATION_US } from '../constants/field_names/synthetics'; +const SUMMARY_UP = 'summary.up'; +const SUMMARY_DOWN = 'summary.down'; + +export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): DataSeries { + return { + reportType: 'kpi-over-time', + defaultSeriesType: 'bar_stacked', + seriesTypes: [], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumns: [ + { + sourceField: 'business.kpi', + operationType: 'median', + }, + ], + hasOperationType: false, + defaultFilters: ['observer.geo.name', 'monitor.type', 'tags'], + breakdowns: ['observer.geo.name', 'monitor.type'], + filters: [...buildExistsFilter('summary.up', indexPattern)], + palette: { type: 'palette', name: 'status' }, + reportDefinitions: [ + { + field: 'monitor.name', + }, + { + field: 'url.full', + }, + { + field: 'business.kpi', + custom: true, + options: [ + { + label: MONITORS_DURATION_LABEL, + field: MONITOR_DURATION_US, + id: MONITOR_DURATION_US, + columnType: OPERATION_COLUMN, + }, + { + field: SUMMARY_UP, + id: SUMMARY_UP, + label: UP_LABEL, + columnType: OPERATION_COLUMN, + }, + { + field: SUMMARY_DOWN, + id: SUMMARY_DOWN, + label: DOWN_LABEL, + columnType: OPERATION_COLUMN, + }, + ], + }, + ], + labels: { ...FieldLabels }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts deleted file mode 100644 index 697a940f666f76..00000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/monitor_pings_config.ts +++ /dev/null @@ -1,50 +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 { ConfigProps, DataSeries } from '../../types'; -import { FieldLabels } from '../constants'; -import { buildExistsFilter } from '../utils'; -import { DOWN_LABEL, UP_LABEL } from '../constants/labels'; - -export function getMonitorPingsConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { - return { - id: seriesId, - reportType: 'uptime-pings', - defaultSeriesType: 'bar_stacked', - seriesTypes: [], - xAxisColumn: { - sourceField: '@timestamp', - }, - yAxisColumns: [ - { - operationType: 'sum', - sourceField: 'summary.up', - label: UP_LABEL, - }, - { - operationType: 'sum', - sourceField: 'summary.down', - label: DOWN_LABEL, - }, - ], - yTitle: 'Pings', - hasOperationType: false, - defaultFilters: ['observer.geo.name', 'monitor.type', 'tags'], - breakdowns: ['observer.geo.name', 'monitor.type'], - filters: [...buildExistsFilter('summary.up', indexPattern)], - palette: { type: 'palette', name: 'status' }, - reportDefinitions: [ - { - field: 'monitor.name', - }, - { - field: 'url.full', - }, - ], - labels: { ...FieldLabels }, - }; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index fc0062694e0a32..487ecdb2bafcc1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -52,7 +52,7 @@ describe('ExploratoryView', () => { data: { 'ux-series': { dataType: 'ux' as const, - reportType: 'pld' as const, + reportType: 'dist' as const, breakdown: 'user_agent .name', reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 7958dca6e396ee..329ed20ffed3d4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -16,7 +16,6 @@ import { useLensAttributes } from './hooks/use_lens_attributes'; import { EmptyView } from './components/empty_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { ReportToDataTypeMap } from './configurations/constants'; import { SeriesBuilder } from './series_builder/series_builder'; export function ExploratoryView({ @@ -61,10 +60,10 @@ export function ExploratoryView({ }; useEffect(() => { - if (series?.reportType || series?.dataType) { - loadIndexPattern({ dataType: series?.dataType ?? ReportToDataTypeMap[series?.reportType] }); + if (series?.dataType) { + loadIndexPattern({ dataType: series?.dataType }); } - }, [series?.reportType, series?.dataType, loadIndexPattern]); + }, [series?.dataType, loadIndexPattern]); useEffect(() => { setLensAttributes(lensAttributesT); @@ -91,7 +90,7 @@ export function ExploratoryView({ timeRange={series?.time} attributes={lensAttributes} onBrushEnd={({ range }) => { - if (series?.reportType !== 'pld') { + if (series?.reportType !== 'dist') { setSeries(seriesId, { ...series, time: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index ca9f2c9e73eb8b..1dedc4142f1747 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'upp' as const, + reportType: 'kpi' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 4e9c360745b6b3..1c85bc5089b2af 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -42,7 +42,8 @@ export const useLensAttributes = ({ }: Props): TypedLensByValueInput['attributes'] | null => { const { getSeries } = useSeriesStorage(); const series = getSeries(seriesId); - const { breakdown, seriesType, operationType, reportType, reportDefinitions = {} } = series ?? {}; + const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } = + series ?? {}; const { indexPattern } = useAppIndexPatternContext(); @@ -52,8 +53,8 @@ export const useLensAttributes = ({ } const dataViewConfig = getDefaultConfigs({ - seriesId, reportType, + dataType, indexPattern, }); @@ -78,12 +79,12 @@ export const useLensAttributes = ({ return lensAttributes.getJSON(); }, [ indexPattern, - breakdown, - seriesType, - operationType, reportType, reportDefinitions, - seriesId, + dataType, series.filters, + seriesType, + operationType, + breakdown, ]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 9118e49a42dfb5..ff766f7e6a1cf5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -259,7 +259,7 @@ function mockSeriesStorageContext({ }) { const mockDataSeries = data || { 'performance-distribution': { - reportType: 'pld', + reportType: 'dist', dataType: 'ux', breakdown: breakdown || 'user_agent.name', time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index 51529a3b1ac175..e3c1666c533ef0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -37,7 +37,7 @@ describe('DataTypesCol', function () { data: { [seriesId]: { dataType: 'synthetics' as const, - reportType: 'upp' as const, + reportType: 'kpi' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 08e7f4ddcd3d05..3fe88de518f759 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -11,7 +11,6 @@ import styled from 'styled-components'; import { AppDataType } from '../../types'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportToDataTypeMap } from '../../configurations/constants'; export const dataTypes: Array<{ id: AppDataType; label: string }> = [ { id: 'synthetics', label: 'Synthetic Monitoring' }, @@ -35,7 +34,7 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { } }; - const selectedDataType = series.dataType ?? ReportToDataTypeMap[series.reportType]; + const selectedDataType = series.dataType; return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx index fa273f61809355..fce1383f30f343 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx @@ -48,6 +48,12 @@ export function OperationTypeSelect({ defaultMessage: 'Median', }), }, + { + value: 'sum' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.sum', { + defaultMessage: 'Sum', + }), + }, { value: '75th' as OperationType, inputDisplay: i18n.translate('xpack.observability.expView.operationType.75thPercentile', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index f576862f18e76c..805186e877d570 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -15,8 +15,8 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel describe('Series Builder ReportBreakdowns', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - seriesId, - reportType: 'pld', + reportType: 'dist', + dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: USER_AGENT_OS, dataType: 'ux', - reportType: 'pld', + reportType: 'dist', time: { from: 'now-15m', to: 'now' }, }); }); @@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: undefined, dataType: 'ux', - reportType: 'pld', + reportType: 'dist', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index fdf6633c0ddb52..8738235f0c54b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -22,16 +22,16 @@ describe('Series Builder ReportDefinitionCol', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - seriesId, - reportType: 'pld', + reportType: 'dist', indexPattern: mockIndexPattern, + dataType: 'ux', }); const initSeries = { data: { [seriesId]: { dataType: 'ux' as const, - reportType: 'pld' as const, + reportType: 'dist' as const, time: { from: 'now-30d', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, }, @@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', reportDefinitions: {}, - reportType: 'pld', + reportType: 'dist', time: { from: 'now-30d', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index dc2dc629cc121d..7ca947fed0bc9f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -15,9 +15,9 @@ describe('Series Builder ReportFilters', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - seriesId, - reportType: 'pld', + reportType: 'dist', indexPattern: mockIndexPattern, + dataType: 'ux', }); it('should render properly', function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index c721a2fa2fe771..f36d64ca5bbbd8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -34,13 +34,13 @@ describe('ReportTypesCol', function () { ); - fireEvent.click(screen.getByText(/monitor duration/i)); + fireEvent.click(screen.getByText(/KPI over time/i)); expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: 'user_agent.name', dataType: 'ux', reportDefinitions: {}, - reportType: 'upd', + reportType: 'kpi', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); @@ -51,7 +51,7 @@ describe('ReportTypesCol', function () { data: { [NEW_SERIES_KEY]: { dataType: 'synthetics' as const, - reportType: 'upp' as const, + reportType: 'kpi' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, @@ -64,7 +64,7 @@ describe('ReportTypesCol', function () { ); const button = screen.getByRole('button', { - name: /pings histogram/i, + name: /KPI over time/i, }); expect(button.classList).toContain('euiButton--fill'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index 32f1fb7f7c43bf..e24d246d60e583 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -21,29 +21,17 @@ import { getDefaultConfigs } from '../configurations/default_configs'; export const ReportTypes: Record> = { synthetics: [ - { id: 'upd', label: 'Monitor duration' }, - { id: 'upp', label: 'Pings histogram' }, + { id: 'kpi', label: 'KPI over time' }, + { id: 'dist', label: 'Performance distribution' }, ], ux: [ - { id: 'pld', label: 'Performance distribution' }, { id: 'kpi', label: 'KPI over time' }, + { id: 'dist', label: 'Performance distribution' }, { id: 'cwv', label: 'Core Web Vitals' }, ], - apm: [ - { id: 'svl', label: 'Latency' }, - { id: 'tpt', label: 'Throughput' }, - ], - infra_logs: [ - { - id: 'logs', - label: 'Logs Frequency', - }, - ], - infra_metrics: [ - { id: 'cpu', label: 'CPU usage' }, - { id: 'mem', label: 'Memory usage' }, - { id: 'nwk', label: 'Network activity' }, - ], + apm: [], + infra_logs: [], + infra_metrics: [], }; export function SeriesBuilder({ @@ -72,7 +60,7 @@ export function SeriesBuilder({ const getDataViewSeries = () => { return getDefaultConfigs({ - seriesId, + dataType, indexPattern, reportType: reportType!, }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index 0edc4330ef97aa..2b46bb9a8cd623 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -17,7 +17,7 @@ describe('SeriesDatePicker', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'upp' as const, + reportType: 'dist' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, @@ -32,7 +32,7 @@ describe('SeriesDatePicker', function () { const initSeries = { data: { 'uptime-pings-histogram': { - reportType: 'upp' as const, + reportType: 'kpi' as const, dataType: 'synthetics' as const, breakdown: 'monitor.status', }, @@ -46,7 +46,7 @@ describe('SeriesDatePicker', function () { expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { breakdown: 'monitor.status', dataType: 'synthetics' as const, - reportType: 'upp' as const, + reportType: 'kpi' as const, time: DEFAULT_TIME, }); }); @@ -56,7 +56,7 @@ describe('SeriesDatePicker', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'upp' as const, + reportType: 'kpi' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, @@ -79,7 +79,7 @@ describe('SeriesDatePicker', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'monitor.status', dataType: 'synthetics', - reportType: 'upp', + reportType: 'kpi', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx index 4bef3e8f71821c..a0d2fd86482a5e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx @@ -23,7 +23,7 @@ export function ChartEditOptions({ series, seriesId, breakdowns }: Props) { - + ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 0ce9db73f92b14..1d552486921e1f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -9,15 +9,14 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; import { mockIndexPattern, render } from '../../rtl_helpers'; -import { NEW_SERIES_KEY } from '../../hooks/use_series_storage'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Breakdowns', function () { const dataViewSeries = getDefaultConfigs({ - reportType: 'pld', + reportType: 'dist', indexPattern: mockIndexPattern, - seriesId: NEW_SERIES_KEY, + dataType: 'ux', }); it('should render properly', async function () { @@ -53,7 +52,7 @@ describe('Breakdowns', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'user_agent.name', dataType: 'ux', - reportType: 'pld', + reportType: 'dist', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx index 975817a8417dea..08664ac75eb8da 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx @@ -13,17 +13,18 @@ import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types interface Props { series: DataSeries; + seriesId: string; } -export function ChartOptions({ series }: Props) { +export function ChartOptions({ series, seriesId }: Props) { return ( - + {series.hasOperationType && ( - + )} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 5374fc33093a11..086a1d4341bbc6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -10,7 +10,6 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RemoveSeries } from './remove_series'; import { NEW_SERIES_KEY, useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportToDataTypeMap } from '../../configurations/constants'; interface Props { seriesId: string; @@ -21,7 +20,7 @@ export function SeriesActions({ seriesId }: Props) { const onEdit = () => { removeSeries(seriesId); - setSeries(NEW_SERIES_KEY, { ...series, dataType: ReportToDataTypeMap[series.reportType] }); + setSeries(NEW_SERIES_KEY, { ...series }); }; return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 2919714ac0cd41..8363b6b0eadfdb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -10,16 +10,15 @@ import { screen, waitFor } from '@testing-library/react'; import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { NEW_SERIES_KEY } from '../hooks/use_series_storage'; import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; describe('SelectedFilters', function () { mockAppIndexPattern(); const dataViewSeries = getDefaultConfigs({ - reportType: 'pld', + reportType: 'dist', indexPattern: mockIndexPattern, - seriesId: NEW_SERIES_KEY, + dataType: 'ux', }); it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 6e513fcd2fec90..79218aa111f16c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -18,6 +18,11 @@ import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { SeriesActions } from './columns/series_actions'; import { ChartEditOptions } from './chart_edit_options'; +interface EditItem { + seriesConfig: DataSeries; + id: string; +} + export function SeriesEditor() { const { allSeries, firstSeriesId } = useSeriesStorage(); @@ -43,8 +48,8 @@ export function SeriesEditor() { }), field: 'defaultFilters', width: '15%', - render: (defaultFilters: string[], series: DataSeries) => ( - + render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => ( + ), }, { @@ -53,8 +58,8 @@ export function SeriesEditor() { }), field: 'breakdowns', width: '25%', - render: (val: string[], item: DataSeries) => ( - + render: (val: string[], item: EditItem) => ( + ), }, { @@ -69,7 +74,7 @@ export function SeriesEditor() { width: '20%', field: 'id', align: 'right' as const, - render: (val: string, item: DataSeries) => , + render: (val: string, item: EditItem) => , }, { name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { @@ -78,7 +83,7 @@ export function SeriesEditor() { align: 'center' as const, width: '10%', field: 'id', - render: (val: string, item: DataSeries) => , + render: (val: string, item: EditItem) => , }, ] : []), @@ -86,20 +91,21 @@ export function SeriesEditor() { const allSeriesKeys = Object.keys(allSeries); - const items: DataSeries[] = []; + const items: EditItem[] = []; const { indexPattern } = useAppIndexPatternContext(); allSeriesKeys.forEach((seriesKey) => { const series = allSeries[seriesKey]; if (series.reportType && indexPattern) { - items.push( - getDefaultConfigs({ + items.push({ + id: seriesKey, + seriesConfig: getDefaultConfigs({ indexPattern, reportType: series.reportType, - seriesId: seriesKey, - }) - ); + dataType: series.dataType, + }), + }); } }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 87772532f410d6..98605dfdb4ca3d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -20,17 +20,9 @@ import { IIndexPattern } from '../../../../../../../src/plugins/data/common/inde import { ExistsFilter } from '../../../../../../../src/plugins/data/common/es_query/filters'; export const ReportViewTypes = { - pld: 'page-load-dist', - kpi: 'kpi-trends', + dist: 'data-distribution', + kpi: 'kpi-over-time', cwv: 'core-web-vitals', - upd: 'uptime-duration', - upp: 'uptime-pings', - svl: 'service-latency', - tpt: 'service-throughput', - logs: 'logs-frequency', - cpu: 'cpu-usage', - mem: 'memory-usage', - nwk: 'network-activity', } as const; type ValueOf = T[keyof T]; @@ -60,7 +52,6 @@ export interface ReportDefinition { export interface DataSeries { reportType: ReportViewType; - id: string; xAxisColumn: Partial | Partial; yAxisColumns: Array>; @@ -100,7 +91,6 @@ export interface UrlFilter { } export interface ConfigProps { - seriesId: string; indexPattern: IIndexPattern; } diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index c864462c769152..35161561a23fe2 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -191,7 +191,7 @@ export const PingHistogramComponent: React.FC = ({ { 'pings-over-time': { dataType: 'synthetics', - reportType: 'upp', + reportType: 'kpi', time: { from: dateRangeStart, to: dateRangeEnd }, ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), }, diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 6cfe8f61a1843c..377d7a8fa35d44 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -56,7 +56,7 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { [`monitor-duration`]: { - reportType: 'upd', + reportType: 'kpi', time: { from: dateRangeStart, to: dateRangeEnd }, reportDefinitions: { 'monitor.id': [monitorId] as string[], From dbe6bfd073f024477263d2adad91d1f98d69b8ea Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 14 Jun 2021 16:39:42 +0200 Subject: [PATCH 63/99] [Index Patterns] Cover field editor with a11y tests (#101888) --- test/accessibility/apps/management.ts | 22 ++++++++++++++ test/functional/page_objects/settings_page.ts | 29 ++++++++++++++----- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index 692b140ade7eec..e71f6bb3ebfeea 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); describe('Management', () => { before(async () => { @@ -43,6 +44,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); + it('Index pattern field editor - initial view', async () => { + await PageObjects.settings.clickAddField(); + await a11y.testAppSnapshot(); + }); + + it('Index pattern field editor - all options shown', async () => { + await PageObjects.settings.setFieldName('test'); + await PageObjects.settings.setFieldType('Keyword'); + await PageObjects.settings.setFieldScript("emit('hello world')"); + await PageObjects.settings.toggleRow('formatRow'); + await PageObjects.settings.setFieldFormat('string'); + await PageObjects.settings.toggleRow('customLabelRow'); + await PageObjects.settings.setCustomLabel('custom label'); + await testSubjects.click('toggleAdvancedSetting'); + + await a11y.testAppSnapshot(); + + await testSubjects.click('euiFlyoutCloseButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); + }); + it('Open create index pattern wizard', async () => { await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickAddNewIndexPatternButton(); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 7d7da79b4a3974..88951bb04c956a 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -563,11 +563,8 @@ export class SettingsPageObject extends FtrService { async setFieldScript(script: string) { this.log.debug('set script = ' + script); - const formatRow = await this.testSubjects.find('valueRow'); - const formatRowToggle = (await formatRow.findAllByCssSelector('[data-test-subj="toggle"]'))[0]; - - await formatRowToggle.click(); - const getMonacoTextArea = async () => (await formatRow.findAllByCssSelector('textarea'))[0]; + const valueRow = await this.toggleRow('valueRow'); + const getMonacoTextArea = async () => (await valueRow.findAllByCssSelector('textarea'))[0]; this.retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea())); const monacoTextArea = await getMonacoTextArea(); await monacoTextArea.focus(); @@ -576,8 +573,8 @@ export class SettingsPageObject extends FtrService { async changeFieldScript(script: string) { this.log.debug('set script = ' + script); - const formatRow = await this.testSubjects.find('valueRow'); - const getMonacoTextArea = async () => (await formatRow.findAllByCssSelector('textarea'))[0]; + const valueRow = await this.testSubjects.find('valueRow'); + const getMonacoTextArea = async () => (await valueRow.findAllByCssSelector('textarea'))[0]; this.retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea())); const monacoTextArea = await getMonacoTextArea(); await monacoTextArea.focus(); @@ -622,6 +619,24 @@ export class SettingsPageObject extends FtrService { ); } + async toggleRow(rowTestSubj: string) { + this.log.debug('toggling tow = ' + rowTestSubj); + const row = await this.testSubjects.find(rowTestSubj); + const rowToggle = (await row.findAllByCssSelector('[data-test-subj="toggle"]'))[0]; + await rowToggle.click(); + return row; + } + + async setCustomLabel(label: string) { + this.log.debug('set custom label = ' + label); + await ( + await this.testSubjects.findDescendant( + 'input', + await this.testSubjects.find('customLabelRow') + ) + ).type(label); + } + async setScriptedFieldUrlType(type: string) { this.log.debug('set scripted field Url type = ' + type); await this.find.clickByCssSelector( From b430a985c2e392201cc4afd1bdb14e111320231e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 07:43:24 -0700 Subject: [PATCH 64/99] Update dependency @elastic/charts to v30.1.0 (master) (#101683) --- package.json | 2 +- .../common/charts/__snapshots__/donut_chart.test.tsx.snap | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b4f9109503261c..513352db3f81bb 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "30.0.0", + "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.13.0", diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index b689ca7ff56f0e..1403e6c3e52b12 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -137,7 +137,7 @@ exports[`DonutChart component passes correct props without errors for valid prop "opacity": 1, }, "rectBorder": Object { - "strokeWidth": 0, + "strokeWidth": 1, "visible": false, }, }, diff --git a/yarn.lock b/yarn.lock index 9543f209d6849c..0e2b953389c10a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,10 +1359,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@30.0.0": - version "30.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-30.0.0.tgz#e19ad8b94928aa9bac5d7facc488fa69b683ff1e" - integrity sha512-r22T2dlW3drEmrIx6JNlOOzSp0JCkI/qbIfmvzdMBu8E8hITkJTaXJaLsNN4mz9EvR9jEM8XQQPQXOFKJhWixw== +"@elastic/charts@30.1.0": + version "30.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-30.1.0.tgz#eb9b3348c149ce13f74876738a9d2899b6b10067" + integrity sha512-aUfXRQYQopm+6O48tEO0v/w6fETYORGiSPBRtqlq5xPncZGhGnQbgXVNQsPngYqapnKpOupXAqzjopF+RJ4QWg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 8424a925332d5bd7d93148fbab72d1f518933485 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 14 Jun 2021 10:50:20 -0400 Subject: [PATCH 65/99] [ILM] Migrate to new page layout (#101927) --- .../__snapshots__/policy_table.test.tsx.snap | 98 +++-- .../edit_policy/edit_policy.container.tsx | 70 ++-- .../sections/edit_policy/edit_policy.tsx | 351 +++++++++--------- .../policy_table/policy_table.container.tsx | 70 ++-- .../sections/policy_table/policy_table.tsx | 101 +++-- 5 files changed, 339 insertions(+), 351 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index 8d839e196916b6..556ac35d0565e8 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -46,71 +46,67 @@ Array [ `; exports[`policy table should show empty state when there are not any policies 1`] = ` -
    +
    + - -
    - -

    - Create your first index lifecycle policy -

    -
    -
    -

    - An index lifecycle policy helps you manage your indices as they age. -

    -
    - + Create your first index lifecycle policy +
    -
    + +
    +
    + -
    + +
    -
    + `; exports[`policy table should sort when linked indices header is clicked 1`] = ` diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index fe82b200930724..07c2228863b810 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -7,7 +7,7 @@ import React, { useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; @@ -52,43 +52,47 @@ export const EditPolicy: React.FunctionComponent} - body={ - - } - /> + + } + body={ + + } + /> + ); } if (error || !policies) { const { statusCode, message } = error ? error : { statusCode: '', message: '' }; return ( - - - - } - body={ -

    - {message} ({statusCode}) -

    - } - actions={ - - - - } - /> + + + + + } + body={ +

    + {message} ({statusCode}) +

    + } + actions={ + + + + } + /> +
    ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index d7368249d76e8c..172e8259b87af3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -19,15 +19,10 @@ import { EuiFlexItem, EuiFormRow, EuiHorizontalRule, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, EuiSpacer, EuiSwitch, EuiText, - EuiTitle, + EuiPageHeader, } from '@elastic/eui'; import { TextField, useForm, useFormData, useKibana } from '../../../shared_imports'; @@ -153,201 +148,199 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { }; return ( - - - - - - -

    - {isNewPolicy - ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { - defaultMessage: 'Create policy', - }) - : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { - defaultMessage: 'Edit policy {originalPolicyName}', - values: { originalPolicyName }, - })} -

    -
    -
    - - + <> + + {isNewPolicy + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { + defaultMessage: 'Create policy', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { + defaultMessage: 'Edit policy {originalPolicyName}', + values: { originalPolicyName }, + })} + + } + bottomBorder + rightSideItems={[ + + + , + ]} + /> + + + +
    + {isNewPolicy ? null : ( + + +

    + + + + .{' '} - - - - - {isNewPolicy ? null : ( - - -

    - - - - .{' '} - -

    -
    - - - - { - setSaveAsNew(e.target.checked); - }} - label={ - - - - } - /> - -
    - )} - - {saveAsNew || isNewPolicy ? ( - - path={policyNamePath} - config={{ - label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', { - defaultMessage: 'Policy name', - }), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage', - { - defaultMessage: - 'A policy name cannot start with an underscore and cannot contain a comma or a space.', - } - ), - validations: policyNameValidations, - }} - component={TextField} - componentProps={{ - fullWidth: false, - euiFieldProps: { - 'data-test-subj': 'policyNameField', - }, + /> +

    + + + + + { + setSaveAsNew(e.target.checked); }} + label={ + + + + } /> - ) : null} - - - - - - - -
    - - - - - + + + )} + + {saveAsNew || isNewPolicy ? ( + + path={policyNamePath} + config={{ + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', { + defaultMessage: 'Policy name', + }), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage', + { + defaultMessage: + 'A policy name cannot start with an underscore and cannot contain a comma or a space.', + } + ), + validations: policyNameValidations, + }} + component={TextField} + componentProps={{ + fullWidth: false, + euiFieldProps: { + 'data-test-subj': 'policyNameField', + }, + }} + /> + ) : null} + + + + + + + +
    + + + + + + + + + {isAllowedByLicense && ( + <> - + + + )} - {isAllowedByLicense && ( - <> - - - - )} - - {/* We can't add the here as it breaks the layout + {/* We can't add the here as it breaks the layout and makes the connecting line go further that it needs to. There is an issue in EUI to fix this (https://github.com/elastic/eui/issues/4492) */} - -
    + +
    - + - - - - - - - - {saveAsNew ? ( - - ) : ( - - )} - - - - - - - - - - + + + + - - {isShowingPolicyJsonFlyout ? ( + + {saveAsNew ? ( ) : ( )} + + + + + + + - {isShowingPolicyJsonFlyout ? ( - setIsShowingPolicyJsonFlyout(false)} - /> - ) : null} - -
    -
    -
    + + + {isShowingPolicyJsonFlyout ? ( + + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx index 7cf50de0ee9997..deac2bb239d30a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx @@ -8,7 +8,7 @@ import React, { useEffect } from 'react'; import { ApplicationStart } from 'kibana/public'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { PolicyTable as PresentationComponent } from './policy_table'; import { useKibana } from '../../../shared_imports'; @@ -33,43 +33,47 @@ export const PolicyTable: React.FunctionComponent = if (isLoading) { return ( - } - body={ - - } - /> + + } + body={ + + } + /> + ); } if (error) { const { statusCode, message } = error ? error : { statusCode: '', message: '' }; return ( - - - - } - body={ -

    - {message} ({statusCode}) -

    - } - actions={ - - - - } - /> + + + + + } + body={ +

    + {message} ({statusCode}) +

    + } + actions={ + + + + } + /> +
    ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx index 3b0f815800ba3d..ba89d6c895d93e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx @@ -16,9 +16,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiTitle, - EuiText, - EuiPageBody, + EuiPageHeader, EuiPageContent, } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; @@ -119,67 +117,60 @@ export const PolicyTable: React.FunctionComponent = ({ ); } else { return ( - - - + + + + + } + body={ + +

    - - } - body={ - -

    - -

    -
    - } - actions={createPolicyButton} - /> -
    -
    +

    + + } + actions={createPolicyButton} + /> + ); } return ( - - - {confirmModal} + <> + {confirmModal} - - - -

    - -

    -
    -
    - {createPolicyButton} -
    - - -

    + -

    -
    + + } + description={ + + } + bottomBorder + rightSideItems={[createPolicyButton]} + /> - - {content} -
    -
    + + + {content} + ); }; From 277212df0b919b57d3dadd40bf059eb908b44fd0 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 14 Jun 2021 11:04:27 -0400 Subject: [PATCH 66/99] Update building_a_plugin.mdx (#101921) * Update building_a_plugin.mdx * put back the numbers on the same line * Add info about requiredBundles * Fix numbers again * Update dev_docs/tutorials/building_a_plugin.mdx Co-authored-by: Mikhail Shustov * Update dev_docs/tutorials/building_a_plugin.mdx Co-authored-by: Mikhail Shustov Co-authored-by: Mikhail Shustov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/tutorials/building_a_plugin.mdx | 79 ++++++++++++++++++------ 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/dev_docs/tutorials/building_a_plugin.mdx b/dev_docs/tutorials/building_a_plugin.mdx index cee5a9a399de5b..e751ce7d01b165 100644 --- a/dev_docs/tutorials/building_a_plugin.mdx +++ b/dev_docs/tutorials/building_a_plugin.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/tutorials/build-a-plugin title: Kibana plugin tutorial summary: Anatomy of a Kibana plugin and how to build one date: 2021-02-05 -tags: ['kibana','onboarding', 'dev', 'tutorials'] +tags: ['kibana', 'onboarding', 'dev', 'tutorials'] --- Prereading material: @@ -14,7 +14,7 @@ Prereading material: ## The anatomy of a plugin Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, -or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, +or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, and you interact with Core and other plugins in the same way. The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be: @@ -33,7 +33,7 @@ plugins/ index.ts [6] ``` -### [1] kibana.json +### [1] kibana.json `kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: @@ -42,14 +42,33 @@ plugins/ "id": "demo", "version": "kibana", "server": true, - "ui": true + "ui": true, + "owner": { [1] + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "This plugin extends Kibana by doing xyz, and allows other plugins to extend Kibana by offering abc functionality. It also exposes some helper utilities that do efg", [2] + "requiredPlugins": ["data"], [3] + "optionalPlugins": ["alerting"] [4] + "requiredBundles": ["anotherPlugin"] [5] } ``` +[1], [2]: Every internal plugin should fill in the owner and description properties. + +[3], [4]: Any plugin that you have a dependency on should be listed in `requiredPlugins` or `optionalPlugins`. Doing this will ensure that you have access to that plugin's start and setup contract inside your own plugin's start and setup lifecycle methods. If a plugin you optionally depend on is not installed or disabled, it will be undefined if you try to access it. If a plugin you require is not installed or disabled, kibana will fail to build. + +[5]: Don't worry too much about getting 5 right. The build optimizer will complain if any of these values are incorrect. + + + + You don't need to declare a dependency on a plugin if you only wish to access its types. + + ### [2] public/index.ts -`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of - core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. +`public/index.ts` is the entry point into the client-side code of this plugin. Everything exported from this file will be a part of the plugins . If the plugin only exists to export static utilities, consider using a package. Otherwise, this file must export a function named plugin, which will receive a standard set of +core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. ``` import type { PluginInitializerContext } from 'kibana/server'; @@ -60,13 +79,32 @@ export function plugin(initializerContext: PluginInitializerContext) { } ``` + + +1. When possible, use + +``` +export type { AType } from '...'` +``` + +instead of + +``` +export { AType } from '...'`. +``` + +Using the non-`type` variation will increase the bundle size unnecessarily and may unwillingly provide access to the implementation of `AType` class. + +2. Don't use `export *` in these top level index.ts files + + + ### [3] public/plugin.ts `public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry - point, but all plugins at Elastic should be consistent in this way. +point, but all plugins at Elastic should be consistent in this way. - - ```ts +```ts import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; export class DemoPlugin implements Plugin { @@ -84,10 +122,9 @@ export class DemoPlugin implements Plugin { // called when plugin is torn down during Kibana's shutdown sequence } } - ``` - +``` -### [4] server/index.ts +### [4] server/index.ts `server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: @@ -115,7 +152,7 @@ export class DemoPlugin implements Plugin { } ``` -Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain +Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. ### [6] common/index.ts @@ -124,8 +161,8 @@ considerations related to how plugins integrate with core APIs and APIs exposed ## How plugin's interact with each other, and Core -The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. -For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, +The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. +For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, a plugin just accesses it off of the first argument: ```ts @@ -135,14 +172,16 @@ export class DemoPlugin { public setup(core: CoreSetup) { const router = core.http.createRouter(); // handler is called when '/path' resource is requested with `GET` method - router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + router.get({ path: '/path', validate: false }, (context, req, res) => + res.ok({ content: 'ok' }) + ); } } ``` Unlike core, capabilities exposed by plugins are not automatically injected into all plugins. Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a - dependency in it’s kibana.json manifest file. +dependency in it’s kibana.json manifest file. ** foobar plugin.ts: ** @@ -174,8 +213,8 @@ export class MyPlugin implements Plugin { } } ``` -[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. +[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. ** demo kibana.json** @@ -194,7 +233,7 @@ With that specified in the plugin manifest, the appropriate interfaces are then import type { CoreSetup, CoreStart } from 'kibana/server'; import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; -interface DemoSetupPlugins { [1] +interface DemoSetupPlugins { [1] foobar: FoobarPluginSetup; } @@ -218,7 +257,7 @@ export class DemoPlugin { public stop() {} } ``` - + [1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. [2] These manually constructed types should then be used to specify the type of the second argument to the plugin. From 0e7d4fed93b98d4b746f30141535a852c8b42f69 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:14:02 +0200 Subject: [PATCH 67/99] Fix delayed status API updates in alerting and task_manager (#101778) --- test/functional/apps/bundles/index.js | 9 ++++++ .../server_integration/http/platform/cache.ts | 11 +++++++ .../alerting/server/health/get_state.test.ts | 20 +++++------- .../alerting/server/health/get_state.ts | 13 +++++++- x-pack/plugins/alerting/server/plugin.ts | 31 ++++++++++++------- x-pack/plugins/task_manager/server/plugin.ts | 14 ++++----- 6 files changed, 65 insertions(+), 33 deletions(-) diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index d13e74dd4eed95..577035a8c343cc 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -18,6 +18,15 @@ export default function ({ getService }) { let buildNum; before(async () => { + // Wait for status to become green + let status; + const start = Date.now(); + do { + const resp = await supertest.get('/api/status'); + status = resp.status; + // Stop polling once status stabilizes OR once 40s has passed + } while (status !== 200 && Date.now() - start < 40_000); + const resp = await supertest.get('/api/status').expect(200); buildNum = resp.body.version.build_number; }); diff --git a/test/server_integration/http/platform/cache.ts b/test/server_integration/http/platform/cache.ts index 2c1aa90e963e24..a33f916cf1c4b6 100644 --- a/test/server_integration/http/platform/cache.ts +++ b/test/server_integration/http/platform/cache.ts @@ -12,6 +12,17 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('kibana server cache-control', () => { + before(async () => { + // Wait for status to become green + let status; + const start = Date.now(); + do { + const resp = await supertest.get('/api/status'); + status = resp.status; + // Stop polling once status stabilizes OR once 40s has passed + } while (status !== 200 && Date.now() - start < 40_000); + }); + it('properly marks responses as private, with directives to disable caching', async () => { await supertest .get('/api/status') diff --git a/x-pack/plugins/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts index 96627e10fb3bdf..2dddf81e3b7667 100644 --- a/x-pack/plugins/alerting/server/health/get_state.test.ts +++ b/x-pack/plugins/alerting/server/health/get_state.test.ts @@ -58,7 +58,6 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { const mockTaskManager = taskManagerMock.createStart(); mockTaskManager.get.mockResolvedValue(getHealthCheckTask()); const pollInterval = 100; - const halfInterval = Math.floor(pollInterval / 2); getHealthStatusStream( mockTaskManager, @@ -77,16 +76,15 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { pollInterval ).subscribe(); - // shouldn't fire before poll interval passes + // should fire before poll interval passes // should fire once each poll interval - jest.advanceTimersByTime(halfInterval); - expect(mockTaskManager.get).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(halfInterval); expect(mockTaskManager.get).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(pollInterval); expect(mockTaskManager.get).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(pollInterval); expect(mockTaskManager.get).toHaveBeenCalledTimes(3); + jest.advanceTimersByTime(pollInterval); + expect(mockTaskManager.get).toHaveBeenCalledTimes(4); }); it('should retry on error', async () => { @@ -94,7 +92,6 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { mockTaskManager.get.mockRejectedValue(new Error('Failure')); const retryDelay = 10; const pollInterval = 100; - const halfInterval = Math.floor(pollInterval / 2); getHealthStatusStream( mockTaskManager, @@ -114,28 +111,27 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { retryDelay ).subscribe(); - jest.advanceTimersByTime(halfInterval); - expect(mockTaskManager.get).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(halfInterval); expect(mockTaskManager.get).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(pollInterval); + expect(mockTaskManager.get).toHaveBeenCalledTimes(2); // Retry on failure let numTimesCalled = 1; for (let i = 0; i < MAX_RETRY_ATTEMPTS; i++) { await tick(); jest.advanceTimersByTime(retryDelay); - expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled++ + 1); + expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled++ + 2); } // Once we've exceeded max retries, should not try again await tick(); jest.advanceTimersByTime(retryDelay); - expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled); + expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 1); // Once another poll interval passes, should call fn again await tick(); jest.advanceTimersByTime(pollInterval - MAX_RETRY_ATTEMPTS * retryDelay); - expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 1); + expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 2); }); it('should return healthy status when health status is "ok"', async () => { diff --git a/x-pack/plugins/alerting/server/health/get_state.ts b/x-pack/plugins/alerting/server/health/get_state.ts index 30099614ea42b3..255037d7015a2b 100644 --- a/x-pack/plugins/alerting/server/health/get_state.ts +++ b/x-pack/plugins/alerting/server/health/get_state.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { defer, of, interval, Observable, throwError, timer } from 'rxjs'; -import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; +import { catchError, mergeMap, retryWhen, startWith, switchMap } from 'rxjs/operators'; import { Logger, SavedObjectsServiceStart, @@ -121,6 +121,17 @@ export const getHealthStatusStream = ( retryDelay?: number ): Observable> => interval(healthStatusInterval ?? HEALTH_STATUS_INTERVAL).pipe( + // Emit an initial check + startWith( + getHealthServiceStatusWithRetryAndErrorHandling( + taskManager, + logger, + savedObjects, + config, + retryDelay + ) + ), + // On each interval do a new check switchMap(() => getHealthServiceStatusWithRetryAndErrorHandling( taskManager, diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 769243b8feaf6a..41ae9f15d9af90 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -7,7 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { first, map, share } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { combineLatest } from 'rxjs'; import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; @@ -34,6 +34,7 @@ import { StatusServiceSetup, ServiceStatus, SavedObjectsBulkGetObject, + ServiceStatusLevels, } from '../../../../src/core/server'; import type { AlertingRequestHandlerContext } from './types'; import { defineRoutes } from './routes'; @@ -226,17 +227,23 @@ export class AlertingPlugin { this.config ); + const serviceStatus$ = new BehaviorSubject({ + level: ServiceStatusLevels.unavailable, + summary: 'Alerting is initializing', + }); + core.status.set(serviceStatus$); + core.getStartServices().then(async ([coreStart, startPlugins]) => { - core.status.set( - combineLatest([ - core.status.derivedStatus$, - getHealthStatusStream( - startPlugins.taskManager, - this.logger, - coreStart.savedObjects, - this.config - ), - ]).pipe( + combineLatest([ + core.status.derivedStatus$, + getHealthStatusStream( + startPlugins.taskManager, + this.logger, + coreStart.savedObjects, + this.config + ), + ]) + .pipe( map(([derivedStatus, healthStatus]) => { if (healthStatus.level > derivedStatus.level) { return healthStatus as ServiceStatus; @@ -246,7 +253,7 @@ export class AlertingPlugin { }), share() ) - ); + .subscribe(serviceStatus$); }); initializeAlertingHealth(this.logger, plugins.taskManager, core.getStartServices()); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 51199da26ee7da..d3e251b751ef8c 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -87,15 +87,13 @@ export class TaskManagerPlugin this.config! ); - core.getStartServices().then(async () => { - core.status.set( - combineLatest([core.status.derivedStatus$, serviceStatus$]).pipe( - map(([derivedStatus, serviceStatus]) => - serviceStatus.level > derivedStatus.level ? serviceStatus : derivedStatus - ) + core.status.set( + combineLatest([core.status.derivedStatus$, serviceStatus$]).pipe( + map(([derivedStatus, serviceStatus]) => + serviceStatus.level > derivedStatus.level ? serviceStatus : derivedStatus ) - ); - }); + ) + ); return { index: this.config.index, From 970e9a037bcb098ec84d9a41c5b1e680ecc05f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 14 Jun 2021 17:14:15 +0200 Subject: [PATCH 68/99] [APM] Add AWS and Azure icons for additional services (#101901) --- .../shared/span_icon/get_span_icon.ts | 17 ++++- .../shared/span_icon/span_icon.stories.tsx | 73 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts index bebfcba1a93b40..e2e1391a2f842d 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts @@ -6,7 +6,6 @@ */ import { maybe } from '../../../../common/utils/maybe'; -import awsIcon from './icons/aws.svg'; import cassandraIcon from './icons/cassandra.svg'; import databaseIcon from './icons/database.svg'; import defaultIcon from './icons/default.svg'; @@ -33,12 +32,14 @@ const defaultTypeIcons: { [key: string]: string } = { resource: globeIcon, }; -const typeIcons: { [key: string]: { [key: string]: string } } = { +export const typeIcons: { [type: string]: { [subType: string]: string } } = { aws: { - servicename: awsIcon, + servicename: 'logoAWS', }, db: { cassandra: cassandraIcon, + cosmosdb: 'logoAzure', + dynamodb: 'logoAWS', elasticsearch: elasticsearchIcon, mongodb: mongodbIcon, mysql: mysqlIcon, @@ -51,8 +52,18 @@ const typeIcons: { [key: string]: { [key: string]: string } } = { websocket: websocketIcon, }, messaging: { + azurequeue: 'logoAzure', + azureservicebus: 'logoAzure', jms: javaIcon, kafka: kafkaIcon, + sns: 'logoAWS', + sqs: 'logoAWS', + }, + storage: { + azureblob: 'logoAzure', + azurefile: 'logoAzure', + azuretable: 'logoAzure', + s3: 'logoAWS', }, template: { handlebars: handlebarsIcon, diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx new file mode 100644 index 00000000000000..951d3e61f18460 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGrid, + EuiFlexItem, + EuiCopy, + EuiPanel, + EuiSpacer, + EuiCodeBlock, +} from '@elastic/eui'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { SpanIcon } from './index'; +import { typeIcons } from './get_span_icon'; + +const types = Object.keys(typeIcons); + +storiesOf('shared/span_icon/span_icon', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Span icon', + () => { + return ( + <> + + {''} + + + + + {types.map((type) => { + const subTypes = Object.keys(typeIcons[type]); + return ( + <> + {subTypes.map((subType) => { + const id = `${type}.${subType}`; + return ( + + + {(copy) => ( + +  {' '} + {id} + + )} + + + ); + })} + + ); + })} + + + ); + }, + {} + ); From adda72edd28abf21b201477aaddc55b51e197596 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 14 Jun 2021 12:23:28 -0400 Subject: [PATCH 69/99] Document platform security plugins in kibana.json (#101965) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/security_oss/kibana.json | 5 +++++ src/plugins/spaces_oss/kibana.json | 5 +++++ x-pack/plugins/encrypted_saved_objects/kibana.json | 5 +++++ x-pack/plugins/security/kibana.json | 5 +++++ x-pack/plugins/spaces/kibana.json | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/src/plugins/security_oss/kibana.json b/src/plugins/security_oss/kibana.json index 70e37d586f1db3..c93b5c3b60714d 100644 --- a/src/plugins/security_oss/kibana.json +++ b/src/plugins/security_oss/kibana.json @@ -1,5 +1,10 @@ { "id": "securityOss", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin exposes a limited set of security functionality to OSS plugins.", "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["security"], diff --git a/src/plugins/spaces_oss/kibana.json b/src/plugins/spaces_oss/kibana.json index e048fb7ffb79c6..10127634618f1a 100644 --- a/src/plugins/spaces_oss/kibana.json +++ b/src/plugins/spaces_oss/kibana.json @@ -1,5 +1,10 @@ { "id": "spacesOss", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin exposes a limited set of spaces functionality to OSS plugins.", "version": "kibana", "server": false, "ui": true, diff --git a/x-pack/plugins/encrypted_saved_objects/kibana.json b/x-pack/plugins/encrypted_saved_objects/kibana.json index 74f797ba36a11f..4812afe8c20725 100644 --- a/x-pack/plugins/encrypted_saved_objects/kibana.json +++ b/x-pack/plugins/encrypted_saved_objects/kibana.json @@ -1,5 +1,10 @@ { "id": "encryptedSavedObjects", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin provides encryption and decryption utilities for saved objects containing sensitive information.", "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "encryptedSavedObjects"], diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index f6e7b8bf46a394..a29c01b0f31ccd 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -1,5 +1,10 @@ { "id": "security", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user.", "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index e67931d4a3b8d9..673055b24e8b61 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -1,5 +1,10 @@ { "id": "spaces", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories.", "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "spaces"], From 91b804f505da9614dc6b85cd64ae64f6d06bea0f Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 14 Jun 2021 12:33:46 -0400 Subject: [PATCH 70/99] [Fleet] Display NOTICE.txt from package if it exists (#101663) --- .../plugins/fleet/common/types/models/epm.ts | 8 +- .../applications/integrations/constants.tsx | 6 +- .../integrations/sections/epm/constants.tsx | 7 +- .../epm/screens/detail/overview/details.tsx | 57 +++++++++---- .../screens/detail/overview/notice_modal.tsx | 79 +++++++++++++++++++ .../server/services/epm/archive/index.ts | 7 ++ .../fleet/server/services/epm/packages/get.ts | 1 + .../server/services/epm/registry/index.ts | 12 +++ 8 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 0ef9f8b7ace36a..f19684b0445e2a 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -48,7 +48,12 @@ export type EpmPackageInstallStatus = 'installed' | 'installing'; export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; -export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf; +export type DocAssetType = 'doc' | 'notice'; +export type AssetType = + | KibanaAssetType + | ElasticsearchAssetType + | ValueOf + | DocAssetType; /* Enum mapping of a saved object asset type to how it would appear in a package file path (snake cased) @@ -344,6 +349,7 @@ export interface EpmPackageAdditions { latestVersion: string; assets: AssetsGroupedByServiceByType; removable?: boolean; + notice?: string; } type Merge = Omit> & diff --git a/x-pack/plugins/fleet/public/applications/integrations/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/constants.tsx index 403a47f4b94b2f..08197e18fec026 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/constants.tsx @@ -7,7 +7,7 @@ import type { IconType } from '@elastic/eui'; -import type { AssetType, ServiceName } from '../../types'; +import type { ServiceName } from '../../types'; import { ElasticsearchAssetType, KibanaAssetType } from '../../types'; export * from '../../constants'; @@ -20,8 +20,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = { kibana: Object.values(KibanaAssetType), elasticsearch: Object.values(ElasticsearchAssetType), }; +export type DisplayedAssetType = KibanaAssetType | ElasticsearchAssetType; -export const AssetTitleMap: Record = { +export const AssetTitleMap: Record = { dashboard: 'Dashboard', ilm_policy: 'ILM Policy', ingest_pipeline: 'Ingest Pipeline', @@ -31,7 +32,6 @@ export const AssetTitleMap: Record = { component_template: 'Component Template', search: 'Saved Search', visualization: 'Visualization', - input: 'Agent input', map: 'Map', data_stream_ilm_policy: 'Data Stream ILM Policy', lens: 'Lens', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx index 6ddff968bd3f3b..41db09b0538b91 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx @@ -7,7 +7,7 @@ import type { IconType } from '@elastic/eui'; -import type { AssetType, ServiceName } from '../../types'; +import type { ServiceName } from '../../types'; import { ElasticsearchAssetType, KibanaAssetType } from '../../types'; // only allow Kibana assets for the kibana key, ES asssets for elasticsearch, etc @@ -19,7 +19,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = { elasticsearch: Object.values(ElasticsearchAssetType), }; -export const AssetTitleMap: Record = { +export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType; + +export const AssetTitleMap: Record = { dashboard: 'Dashboard', ilm_policy: 'ILM Policy', ingest_pipeline: 'Ingest Pipeline', @@ -29,7 +31,6 @@ export const AssetTitleMap: Record = { component_template: 'Component Template', search: 'Saved Search', visualization: 'Visualization', - input: 'Agent input', map: 'Map', data_stream_ilm_policy: 'Data Stream ILM Policy', lens: 'Lens', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx index 487df179803452..0a601d2128bbae 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -13,6 +13,8 @@ import { EuiTextColor, EuiDescriptionList, EuiNotificationBadge, + EuiLink, + EuiPortal, } from '@elastic/eui'; import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; @@ -26,6 +28,8 @@ import { entries } from '../../../../../types'; import { useGetCategories } from '../../../../../hooks'; import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants'; +import { NoticeModal } from './notice_modal'; + interface Props { packageInfo: PackageInfo; } @@ -41,6 +45,11 @@ export const Details: React.FC = memo(({ packageInfo }) => { return []; }, [categoriesData, isLoadingCategories, packageInfo.categories]); + const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false); + const toggleNoticeModal = useCallback(() => { + setIsNoticeModalOpen(!isNoticeModalOpen); + }, [isNoticeModalOpen]); + const listItems = useMemo(() => { // Base details: version and categories const items: EuiDescriptionListProps['listItems'] = [ @@ -123,14 +132,23 @@ export const Details: React.FC = memo(({ packageInfo }) => { } // License details - if (packageInfo.license) { + if (packageInfo.license || packageInfo.notice) { items.push({ title: ( ), - description: packageInfo.license, + description: ( + <> +

    {packageInfo.license}

    + {packageInfo.notice && ( +

    + NOTICE.txt +

    + )} + + ), }); } @@ -140,21 +158,30 @@ export const Details: React.FC = memo(({ packageInfo }) => { packageInfo.assets, packageInfo.data_streams, packageInfo.license, + packageInfo.notice, packageInfo.version, + toggleNoticeModal, ]); return ( - - - -

    - -

    -
    -
    - - - -
    + <> + + {isNoticeModalOpen && packageInfo.notice && ( + + )} + + + + +

    + +

    +
    +
    + + + +
    + ); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx new file mode 100644 index 00000000000000..239bd133d3c919 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/notice_modal.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiCodeBlock, + EuiLoadingContent, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalFooter, + EuiModalHeaderTitle, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { sendGetFileByPath, useStartServices } from '../../../../../hooks'; + +interface Props { + noticePath: string; + onClose: () => void; +} + +export const NoticeModal: React.FunctionComponent = ({ noticePath, onClose }) => { + const { notifications } = useStartServices(); + const [notice, setNotice] = useState(undefined); + + useEffect(() => { + async function fetchData() { + try { + const { data } = await sendGetFileByPath(noticePath); + setNotice(data || ''); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.epm.errorLoadingNotice', { + defaultMessage: 'Error loading NOTICE.txt', + }), + }); + } + } + fetchData(); + }, [noticePath, notifications]); + return ( + + + +

    NOTICE.txt

    +
    +
    + + + {notice ? ( + notice + ) : ( + // Simulate a long notice while loading + <> +

    + +

    +

    + +

    + + )} +
    +
    + + + + + +
    + ); +}; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 809684df0592c9..b08ec815a394d0 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -114,6 +114,13 @@ export function getPathParts(path: string): AssetParts { [pkgkey, service, type, file] = path.replace(`data_stream/${dataset}/`, '').split('/'); } + // To support the NOTICE asset at the root level + if (service === 'NOTICE.txt') { + file = service; + type = 'notice'; + service = ''; + } + // This is to cover for the fields.yml files inside the "fields" directory if (file === undefined) { file = type; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index e4e4f9c55fd2be..404431816d10c5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -126,6 +126,7 @@ export async function getPackageInfo(options: { title: packageInfo.title || nameAsTitle(packageInfo.name), assets: Registry.groupPathsByService(paths || []), removable: !isRequiredPackage(pkgName), + notice: Registry.getNoticePath(paths || []), }; const updated = { ...packageInfo, ...additions }; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 5ee7e198555c59..011a0e74e8c184 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -255,3 +255,15 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy elasticsearch: assets.elasticsearch, }; } + +export function getNoticePath(paths: string[]): string | undefined { + for (const path of paths) { + const parts = getPathParts(path.replace(/^\/package\//, '')); + if (parts.type === 'notice') { + const { pkgName, pkgVersion } = splitPkgKey(parts.pkgkey); + return `/package/${pkgName}/${pkgVersion}/${parts.file}`; + } + } + + return undefined; +} From 571524005ee9913f50eaac73a8689c0e33439e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Mon, 14 Jun 2021 18:34:52 +0200 Subject: [PATCH 71/99] [APM] Change impact indicator size (#102060) --- .../shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap | 2 +- x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap index 8e89939f585aae..87b5b68e260267 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap @@ -4,7 +4,7 @@ exports[`ImpactBar component should render with default values 1`] = ` `; diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx index 92f488b8ba0ee3..87b3c669e993c5 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx @@ -18,7 +18,7 @@ export interface ImpactBarProps extends Record { export function ImpactBar({ value, - size = 'l', + size = 'm', max = 100, color = 'primary', ...rest From 666bce392383a57f983617bc25b901a556ec20b0 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 14 Jun 2021 18:44:29 +0200 Subject: [PATCH 72/99] [APM] Enforce span creation/naming for ES searches (#101856) --- packages/kbn-apm-utils/src/index.ts | 51 ++- .../chart_preview/get_transaction_duration.ts | 120 +++--- .../get_transaction_error_count.ts | 72 ++-- .../get_transaction_error_rate.ts | 5 +- ...et_correlations_for_failed_transactions.ts | 207 +++++----- .../errors/get_overall_error_timeseries.ts | 58 +-- .../get_correlations_for_slow_transactions.ts | 89 ++--- .../latency/get_duration_for_percentile.ts | 41 +- .../latency/get_latency_distribution.ts | 116 +++--- .../correlations/latency/get_max_latency.ts | 61 ++- .../get_overall_latency_distribution.ts | 5 +- .../lib/environments/get_all_environments.ts | 88 +++-- .../lib/environments/get_environments.ts | 83 ++-- .../__snapshots__/get_buckets.test.ts.snap | 1 + .../errors/distribution/get_buckets.test.ts | 2 +- .../lib/errors/distribution/get_buckets.ts | 90 ++--- .../lib/errors/get_error_group_sample.ts | 77 ++-- .../apm/server/lib/errors/get_error_groups.ts | 139 ++++--- .../helpers/aggregated_transactions/index.ts | 18 +- .../create_es_client/call_async_with_debug.ts | 21 +- .../create_apm_event_client/index.test.ts | 2 +- .../create_apm_event_client/index.ts | 19 +- .../create_internal_es_client/index.ts | 44 ++- .../server/lib/helpers/setup_request.test.ts | 32 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 4 +- .../by_agent/java/gc/get_gc_rate_chart.ts | 22 +- .../by_agent/java/gc/get_gc_time_chart.ts | 22 +- .../by_agent/java/heap_memory/index.ts | 34 +- .../by_agent/java/non_heap_memory/index.ts | 38 +- .../by_agent/java/thread_count/index.ts | 30 +- .../lib/metrics/by_agent/shared/cpu/index.ts | 32 +- .../metrics/by_agent/shared/memory/index.ts | 70 ++-- .../metrics/fetch_and_transform_metrics.ts | 4 +- .../get_service_count.ts | 50 +-- .../get_transactions_per_minute.ts | 113 +++--- .../lib/observability_overview/has_data.ts | 48 +-- .../lib/rum_client/get_client_metrics.ts | 2 +- .../server/lib/rum_client/get_js_errors.ts | 2 +- .../lib/rum_client/get_long_task_metrics.ts | 2 +- .../rum_client/get_page_load_distribution.ts | 7 +- .../lib/rum_client/get_page_view_trends.ts | 2 +- .../lib/rum_client/get_pl_dist_breakdown.ts | 5 +- .../server/lib/rum_client/get_rum_services.ts | 2 +- .../server/lib/rum_client/get_url_search.ts | 2 +- .../lib/rum_client/get_visitor_breakdown.ts | 2 +- .../lib/rum_client/get_web_core_vitals.ts | 2 +- .../apm/server/lib/rum_client/has_rum_data.ts | 2 +- .../ui_filters/local_ui_filters/index.ts | 55 +-- .../fetch_service_paths_from_trace_ids.ts | 120 +++--- .../server/lib/service_map/get_service_map.ts | 101 ++--- .../get_service_map_service_node_info.ts | 119 +++--- .../lib/service_map/get_trace_sample_ids.ts | 195 +++++----- .../apm/server/lib/service_nodes/index.ts | 101 +++-- .../__snapshots__/queries.test.ts.snap | 1 + .../get_derived_service_annotations.ts | 142 ++++--- .../lib/services/get_service_agent_name.ts | 68 ++-- .../get_destination_map.ts | 137 ++++--- .../get_service_dependencies/get_metrics.ts | 94 ++--- ...service_error_group_detailed_statistics.ts | 111 +++--- ...get_service_error_group_main_statistics.ts | 45 ++- .../get_service_error_groups/index.ts | 76 ++-- .../get_service_instance_metadata_details.ts | 56 +-- ...vice_instances_system_metric_statistics.ts | 259 +++++++------ ...ervice_instances_transaction_statistics.ts | 221 ++++++----- .../services/get_service_metadata_details.ts | 172 ++++----- .../services/get_service_metadata_icons.ts | 92 ++--- .../lib/services/get_service_node_metadata.ts | 69 ++-- ...e_transaction_group_detailed_statistics.ts | 191 +++++----- .../get_service_transaction_groups.ts | 86 ++--- .../services/get_service_transaction_types.ts | 68 ++-- .../get_services/get_legacy_data_status.ts | 44 +-- .../get_service_transaction_stats.ts | 142 +++---- .../get_services_from_metric_documents.ts | 36 +- .../get_services/has_historical_agent_data.ts | 35 +- .../apm/server/lib/services/get_throughput.ts | 25 +- .../get_service_profiling_statistics.ts | 125 +++---- .../get_service_profiling_timeline.ts | 50 +-- .../apm/server/lib/services/queries.test.ts | 2 +- .../create_or_update_configuration.ts | 43 +-- .../delete_configuration.ts | 17 +- .../find_exact_configuration.ts | 56 ++- .../get_agent_name_by_service.ts | 54 +-- .../get_existing_environments_for_service.ts | 54 +-- .../agent_configuration/get_service_names.ts | 66 ++-- .../list_configurations.ts | 6 +- .../mark_applied_by_agent.ts | 5 +- .../search_configurations.ts | 98 +++-- .../create_or_update_custom_link.test.ts | 52 +-- .../create_or_update_custom_link.ts | 31 +- .../custom_link/delete_custom_link.ts | 17 +- .../settings/custom_link/get_transaction.ts | 56 +-- .../settings/custom_link/list_custom_links.ts | 78 ++-- .../apm/server/lib/traces/get_trace_items.ts | 166 ++++---- .../lib/transaction_groups/get_error_rate.ts | 132 +++---- .../get_transaction_group_stats.ts | 187 +++++----- .../lib/transaction_groups/queries.test.ts | 4 +- .../lib/transactions/breakdown/index.ts | 353 +++++++++--------- .../distribution/get_buckets/index.ts | 128 ++++--- .../distribution/get_distribution_max.ts | 70 ++-- .../transactions/get_latency_charts/index.ts | 69 ++-- .../get_throughput_charts/index.ts | 41 +- .../lib/transactions/get_transaction/index.ts | 39 +- .../get_transaction_by_trace/index.ts | 59 +-- .../plugins/apm/server/utils/test_helpers.tsx | 2 +- .../plugins/apm/server/utils/with_apm_span.ts | 2 +- 105 files changed, 3410 insertions(+), 3451 deletions(-) diff --git a/packages/kbn-apm-utils/src/index.ts b/packages/kbn-apm-utils/src/index.ts index 384b6683199e5b..09a6989091f609 100644 --- a/packages/kbn-apm-utils/src/index.ts +++ b/packages/kbn-apm-utils/src/index.ts @@ -14,6 +14,7 @@ export interface SpanOptions { type?: string; subtype?: string; labels?: Record; + intercept?: boolean; } type Span = Exclude; @@ -36,23 +37,27 @@ export async function withSpan( ): Promise { const options = parseSpanOptions(optionsOrName); - const { name, type, subtype, labels } = options; + const { name, type, subtype, labels, intercept } = options; if (!agent.isStarted()) { return cb(); } + let createdSpan: Span | undefined; + // When a span starts, it's marked as the active span in its context. // When it ends, it's not untracked, which means that if a span // starts directly after this one ends, the newly started span is a // child of this span, even though it should be a sibling. // To mitigate this, we queue a microtask by awaiting a promise. - await Promise.resolve(); + if (!intercept) { + await Promise.resolve(); - const span = agent.startSpan(name); + createdSpan = agent.startSpan(name) ?? undefined; - if (!span) { - return cb(); + if (!createdSpan) { + return cb(); + } } // If a span is created in the same context as the span that we just @@ -61,33 +66,51 @@ export async function withSpan( // mitigate this we create a new context. return runInNewContext(() => { + const promise = cb(createdSpan); + + let span: Span | undefined = createdSpan; + + if (intercept) { + span = agent.currentSpan ?? undefined; + } + + if (!span) { + return promise; + } + + const targetedSpan = span; + + if (name) { + targetedSpan.name = name; + } + // @ts-ignore if (type) { - span.type = type; + targetedSpan.type = type; } if (subtype) { - span.subtype = subtype; + targetedSpan.subtype = subtype; } if (labels) { - span.addLabels(labels); + targetedSpan.addLabels(labels); } - return cb(span) + return promise .then((res) => { - if (!span.outcome || span.outcome === 'unknown') { - span.outcome = 'success'; + if (!targetedSpan.outcome || targetedSpan.outcome === 'unknown') { + targetedSpan.outcome = 'success'; } return res; }) .catch((err) => { - if (!span.outcome || span.outcome === 'unknown') { - span.outcome = 'failure'; + if (!targetedSpan.outcome || targetedSpan.outcome === 'unknown') { + targetedSpan.outcome = 'failure'; } throw err; }) .finally(() => { - span.end(); + targetedSpan.end(); }); }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts index 091982598d6a3e..6ce175fcb83626 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -15,82 +15,82 @@ import { import { ProcessorEvent } from '../../../../common/processor_event'; import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -export function getTransactionDurationChartPreview({ +export async function getTransactionDurationChartPreview({ alertParams, setup, }: { alertParams: AlertParams; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_transaction_duration_chart_preview', async () => { - const { apmEventClient, start, end } = setup; - const { - aggregationType, - environment, - serviceName, - transactionType, - } = alertParams; + const { apmEventClient, start, end } = setup; + const { + aggregationType, + environment, + serviceName, + transactionType, + } = alertParams; - const query = { - bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ] as QueryDslQueryContainer[], - }, - }; + const query = { + bool: { + filter: [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...(transactionType + ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] + : []), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ] as QueryDslQueryContainer[], + }, + }; - const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); - const aggs = { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - }, - aggs: { - agg: - aggregationType === 'avg' - ? { avg: { field: TRANSACTION_DURATION } } - : { - percentiles: { - field: TRANSACTION_DURATION, - percents: [aggregationType === '95th' ? 95 : 99], - }, + const aggs = { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + }, + aggs: { + agg: + aggregationType === 'avg' + ? { avg: { field: TRANSACTION_DURATION } } + : { + percentiles: { + field: TRANSACTION_DURATION, + percents: [aggregationType === '95th' ? 95 : 99], }, - }, + }, }, - }; - const params = { - apm: { events: [ProcessorEvent.transaction] }, - body: { size: 0, query, aggs }, - }; - const resp = await apmEventClient.search(params); + }, + }; + const params = { + apm: { events: [ProcessorEvent.transaction] }, + body: { size: 0, query, aggs }, + }; + const resp = await apmEventClient.search( + 'get_transaction_duration_chart_preview', + params + ); - if (!resp.aggregations) { - return []; - } + if (!resp.aggregations) { + return []; + } - return resp.aggregations.timeseries.buckets.map((bucket) => { - const percentilesKey = aggregationType === '95th' ? '95.0' : '99.0'; - const x = bucket.key; - const y = - aggregationType === 'avg' - ? (bucket.agg as { value: number | null }).value - : (bucket.agg as { values: Record }).values[ - percentilesKey - ]; + return resp.aggregations.timeseries.buckets.map((bucket) => { + const percentilesKey = aggregationType === '95th' ? '95.0' : '99.0'; + const x = bucket.key; + const y = + aggregationType === 'avg' + ? (bucket.agg as { value: number | null }).value + : (bucket.agg as { values: Record }).values[ + percentilesKey + ]; - return { x, y }; - }); + return { x, y }; }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts index 2cf1317dc44b0d..3d64c63cb2041c 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts @@ -9,58 +9,58 @@ import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { AlertParams } from '../../../routes/alerts/chart_preview'; import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -export function getTransactionErrorCountChartPreview({ +export async function getTransactionErrorCountChartPreview({ setup, alertParams, }: { setup: Setup & SetupTimeRange; alertParams: AlertParams; }) { - return withApmSpan('get_transaction_error_count_chart_preview', async () => { - const { apmEventClient, start, end } = setup; - const { serviceName, environment } = alertParams; + const { apmEventClient, start, end } = setup; + const { serviceName, environment } = alertParams; - const query = { - bool: { - filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ], - }, - }; + const query = { + bool: { + filter: [ + ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ], + }, + }; - const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); - const aggs = { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - }, + const aggs = { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, }, - }; + }, + }; - const params = { - apm: { events: [ProcessorEvent.error] }, - body: { size: 0, query, aggs }, - }; + const params = { + apm: { events: [ProcessorEvent.error] }, + body: { size: 0, query, aggs }, + }; - const resp = await apmEventClient.search(params); + const resp = await apmEventClient.search( + 'get_transaction_error_count_chart_preview', + params + ); - if (!resp.aggregations) { - return []; - } + if (!resp.aggregations) { + return []; + } - return resp.aggregations.timeseries.buckets.map((bucket) => { - return { - x: bucket.key, - y: bucket.doc_count, - }; - }); + return resp.aggregations.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + y: bucket.doc_count, + }; }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index f0c8d23e0e8fa4..0a6a25ad9c5332 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -64,7 +64,10 @@ export async function getTransactionErrorRateChartPreview({ body: { size: 0, query, aggs }, }; - const resp = await apmEventClient.search(params); + const resp = await apmEventClient.search( + 'get_transaction_error_rate_chart_preview', + params + ); if (!resp.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index 8ee469c9a93c77..11e9f99ddb356b 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -21,68 +21,68 @@ import { getTimeseriesAggregation, getTransactionErrorRateTimeSeries, } from '../../helpers/transaction_error_rate'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; interface Options extends CorrelationsOptions { fieldNames: string[]; } export async function getCorrelationsForFailedTransactions(options: Options) { - return withApmSpan('get_correlations_for_failed_transactions', async () => { - const { fieldNames, setup } = options; - const { apmEventClient } = setup; - const filters = getCorrelationsFilters(options); - - const params = { - apm: { events: [ProcessorEvent.transaction] }, - track_total_hits: true, - body: { - size: 0, - query: { - bool: { filter: filters }, - }, - aggs: { - failed_transactions: { - filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, - - // significant term aggs - aggs: fieldNames.reduce((acc, fieldName) => { - return { - ...acc, - [fieldName]: { - significant_terms: { - size: 10, - field: fieldName, - background_filter: { - bool: { - filter: filters, - must_not: { - term: { [EVENT_OUTCOME]: EventOutcome.failure }, - }, + const { fieldNames, setup } = options; + const { apmEventClient } = setup; + const filters = getCorrelationsFilters(options); + + const params = { + apm: { events: [ProcessorEvent.transaction] }, + track_total_hits: true, + body: { + size: 0, + query: { + bool: { filter: filters }, + }, + aggs: { + failed_transactions: { + filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + + // significant term aggs + aggs: fieldNames.reduce((acc, fieldName) => { + return { + ...acc, + [fieldName]: { + significant_terms: { + size: 10, + field: fieldName, + background_filter: { + bool: { + filter: filters, + must_not: { + term: { [EVENT_OUTCOME]: EventOutcome.failure }, }, }, }, }, - }; - }, {} as Record), - }, + }, + }; + }, {} as Record), }, }, - }; - - const response = await apmEventClient.search(params); - if (!response.aggregations) { - return { significantTerms: [] }; - } - - const sigTermAggs = omit( - response.aggregations?.failed_transactions, - 'doc_count' - ); - - const topSigTerms = processSignificantTermAggs({ sigTermAggs }); - return getErrorRateTimeSeries({ setup, filters, topSigTerms }); - }); + }, + }; + + const response = await apmEventClient.search( + 'get_correlations_for_failed_transactions', + params + ); + if (!response.aggregations) { + return { significantTerms: [] }; + } + + const sigTermAggs = omit( + response.aggregations?.failed_transactions, + 'doc_count' + ); + + const topSigTerms = processSignificantTermAggs({ sigTermAggs }); + return getErrorRateTimeSeries({ setup, filters, topSigTerms }); } export async function getErrorRateTimeSeries({ @@ -94,58 +94,59 @@ export async function getErrorRateTimeSeries({ filters: ESFilter[]; topSigTerms: TopSigTerm[]; }) { - return withApmSpan('get_error_rate_timeseries', async () => { - const { start, end, apmEventClient } = setup; - const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); - - if (isEmpty(topSigTerms)) { - return { significantTerms: [] }; + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); + + if (isEmpty(topSigTerms)) { + return { significantTerms: [] }; + } + + const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString); + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { timeseries: timeseriesAgg }, + }; + return acc; + }, + {} as { + [key: string]: { + filter: AggregationOptionsByType['filter']; + aggs: { timeseries: typeof timeseriesAgg }; + }; } - - const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString); - - const perTermAggs = topSigTerms.reduce( - (acc, term, index) => { - acc[`term_${index}`] = { - filter: { term: { [term.fieldName]: term.fieldValue } }, - aggs: { timeseries: timeseriesAgg }, - }; - return acc; - }, - {} as { - [key: string]: { - filter: AggregationOptionsByType['filter']; - aggs: { timeseries: typeof timeseriesAgg }; - }; - } - ); - - const params = { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { bool: { filter: filters } }, - aggs: perTermAggs, - }, - }; - - const response = await apmEventClient.search(params); - const { aggregations } = response; - - if (!aggregations) { - return { significantTerms: [] }; - } - - return { - significantTerms: topSigTerms.map((topSig, index) => { - const agg = aggregations[`term_${index}`]!; - - return { - ...topSig, - timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets), - }; - }), - }; - }); + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: perTermAggs, + }, + }; + + const response = await apmEventClient.search( + 'get_error_rate_timeseries', + params + ); + const { aggregations } = response; + + if (!aggregations) { + return { significantTerms: [] }; + } + + return { + significantTerms: topSigTerms.map((topSig, index) => { + const agg = aggregations[`term_${index}`]!; + + return { + ...topSig, + timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets), + }; + }), + }; } diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts index 9387e64a51e01c..f3477273806b6f 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts @@ -11,41 +11,41 @@ import { getTimeseriesAggregation, getTransactionErrorRateTimeSeries, } from '../../helpers/transaction_error_rate'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; export async function getOverallErrorTimeseries(options: CorrelationsOptions) { - return withApmSpan('get_error_rate_timeseries', async () => { - const { setup } = options; - const filters = getCorrelationsFilters(options); - const { start, end, apmEventClient } = setup; - const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); + const { setup } = options; + const filters = getCorrelationsFilters(options); + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); - const params = { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { bool: { filter: filters } }, - aggs: { - timeseries: getTimeseriesAggregation(start, end, intervalString), - }, + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseries: getTimeseriesAggregation(start, end, intervalString), }, - }; + }, + }; - const response = await apmEventClient.search(params); - const { aggregations } = response; + const response = await apmEventClient.search( + 'get_error_rate_timeseries', + params + ); + const { aggregations } = response; - if (!aggregations) { - return { overall: null }; - } + if (!aggregations) { + return { overall: null }; + } - return { - overall: { - timeseries: getTransactionErrorRateTimeSeries( - aggregations.timeseries.buckets - ), - }, - }; - }); + return { + overall: { + timeseries: getTransactionErrorRateTimeSeries( + aggregations.timeseries.buckets + ), + }, + }; } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts index 0f93d1411a001c..c37b3e3ab82426 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts @@ -41,60 +41,63 @@ export async function getCorrelationsForSlowTransactions(options: Options) { return { significantTerms: [] }; } - const response = await withApmSpan('get_significant_terms', () => { - const params = { - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { - bool: { - // foreground filters - filter: filters, - must: { - function_score: { - query: { - range: { - [TRANSACTION_DURATION]: { gte: durationForPercentile }, - }, + const params = { + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { + bool: { + // foreground filters + filter: filters, + must: { + function_score: { + query: { + range: { + [TRANSACTION_DURATION]: { gte: durationForPercentile }, }, - script_score: { - script: { - source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`, - }, + }, + script_score: { + script: { + source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`, }, }, }, }, }, - aggs: fieldNames.reduce((acc, fieldName) => { - return { - ...acc, - [fieldName]: { - significant_terms: { - size: 10, - field: fieldName, - background_filter: { - bool: { - filter: [ - ...filters, - { - range: { - [TRANSACTION_DURATION]: { - lt: durationForPercentile, - }, + }, + aggs: fieldNames.reduce((acc, fieldName) => { + return { + ...acc, + [fieldName]: { + significant_terms: { + size: 10, + field: fieldName, + background_filter: { + bool: { + filter: [ + ...filters, + { + range: { + [TRANSACTION_DURATION]: { + lt: durationForPercentile, }, }, - ], - }, + }, + ], }, }, }, - }; - }, {} as Record), - }, - }; - return apmEventClient.search(params); - }); + }, + }; + }, {} as Record), + }, + }; + + const response = await apmEventClient.search( + 'get_significant_terms', + params + ); + if (!response.aggregations) { return { significantTerms: [] }; } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts index 43c261743861d8..a686980700d83a 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts @@ -8,7 +8,6 @@ import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getDurationForPercentile({ @@ -20,31 +19,27 @@ export async function getDurationForPercentile({ filters: ESFilter[]; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_duration_for_percentiles', async () => { - const { apmEventClient } = setup; - const res = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.transaction], + const { apmEventClient } = setup; + const res = await apmEventClient.search('get_duration_for_percentiles', { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + size: 0, + query: { + bool: { filter: filters }, }, - body: { - size: 0, - query: { - bool: { filter: filters }, - }, - aggs: { - percentile: { - percentiles: { - field: TRANSACTION_DURATION, - percents: [durationPercentile], - }, + aggs: { + percentile: { + percentiles: { + field: TRANSACTION_DURATION, + percents: [durationPercentile], }, }, }, - }); - - const duration = Object.values( - res.aggregations?.percentile.values || {} - )[0]; - return duration || 0; + }, }); + + const duration = Object.values(res.aggregations?.percentile.values || {})[0]; + return duration || 0; } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts index 6d42b26b22e42d..be1bb631378cff 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts @@ -10,7 +10,7 @@ import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { TopSigTerm } from '../process_significant_term_aggs'; -import { withApmSpan } from '../../../utils/with_apm_span'; + import { getDistributionAggregation, trimBuckets, @@ -29,70 +29,70 @@ export async function getLatencyDistribution({ maxLatency: number; distributionInterval: number; }) { - return withApmSpan('get_latency_distribution', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const distributionAgg = getDistributionAggregation( - maxLatency, - distributionInterval - ); + const distributionAgg = getDistributionAggregation( + maxLatency, + distributionInterval + ); - const perTermAggs = topSigTerms.reduce( - (acc, term, index) => { - acc[`term_${index}`] = { - filter: { term: { [term.fieldName]: term.fieldValue } }, - aggs: { - distribution: distributionAgg, - }, + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { + distribution: distributionAgg, + }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { + distribution: typeof distributionAgg; }; - return acc; - }, - {} as Record< - string, - { - filter: AggregationOptionsByType['filter']; - aggs: { - distribution: typeof distributionAgg; - }; - } - > - ); + } + > + ); - const params = { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { bool: { filter: filters } }, - aggs: perTermAggs, - }, - }; + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: perTermAggs, + }, + }; - const response = await withApmSpan('get_terms_distribution', () => - apmEventClient.search(params) - ); - type Agg = NonNullable; + const response = await apmEventClient.search( + 'get_latency_distribution', + params + ); - if (!response.aggregations) { - return []; - } + type Agg = NonNullable; - return topSigTerms.map((topSig, index) => { - // ignore the typescript error since existence of response.aggregations is already checked: - // @ts-expect-error - const agg = response.aggregations[`term_${index}`] as Agg[string]; - const total = agg.distribution.doc_count; - const buckets = trimBuckets( - agg.distribution.dist_filtered_by_latency.buckets - ); + if (!response.aggregations) { + return []; + } - return { - ...topSig, - distribution: buckets.map((bucket) => ({ - x: bucket.key, - y: (bucket.doc_count / total) * 100, - })), - }; - }); + return topSigTerms.map((topSig, index) => { + // ignore the typescript error since existence of response.aggregations is already checked: + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg[string]; + const total = agg.distribution.doc_count; + const buckets = trimBuckets( + agg.distribution.dist_filtered_by_latency.buckets + ); + + return { + ...topSig, + distribution: buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })), + }; }); } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts index 8b415bf0d80a7b..f2762086614b49 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts @@ -8,7 +8,6 @@ import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { TopSigTerm } from '../process_significant_term_aggs'; @@ -21,41 +20,39 @@ export async function getMaxLatency({ filters: ESFilter[]; topSigTerms?: TopSigTerm[]; }) { - return withApmSpan('get_max_latency', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const params = { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { - bool: { - filter: filters, + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { + bool: { + filter: filters, - ...(topSigTerms.length - ? { - // only include docs containing the significant terms - should: topSigTerms.map((term) => ({ - term: { [term.fieldName]: term.fieldValue }, - })), - minimum_should_match: 1, - } - : null), - }, + ...(topSigTerms.length + ? { + // only include docs containing the significant terms + should: topSigTerms.map((term) => ({ + term: { [term.fieldName]: term.fieldValue }, + })), + minimum_should_match: 1, + } + : null), }, - aggs: { - // TODO: add support for metrics - // max_latency: { max: { field: TRANSACTION_DURATION } }, - max_latency: { - percentiles: { field: TRANSACTION_DURATION, percents: [99] }, - }, + }, + aggs: { + // TODO: add support for metrics + // max_latency: { max: { field: TRANSACTION_DURATION } }, + max_latency: { + percentiles: { field: TRANSACTION_DURATION, percents: [99] }, }, }, - }; + }, + }; - const response = await apmEventClient.search(params); - // return response.aggregations?.max_latency.value; - return Object.values(response.aggregations?.max_latency.values ?? {})[0]; - }); + const response = await apmEventClient.search('get_max_latency', params); + // return response.aggregations?.max_latency.value; + return Object.values(response.aggregations?.max_latency.values ?? {})[0]; } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts index c5d4def51ea54a..b0e0f22c703663 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts @@ -71,8 +71,9 @@ export async function getOverallLatencyDistribution( }, }; - const response = await withApmSpan('get_terms_distribution', () => - apmEventClient.search(params) + const response = await apmEventClient.search( + 'get_terms_distribution', + params ); if (!response.aggregations) { diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 1bf01c24776fb0..f6a19879748532 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -13,7 +13,6 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { withApmSpan } from '../../utils/with_apm_span'; /** * This is used for getting *all* environments, and does not filter by range. @@ -30,59 +29,56 @@ export async function getAllEnvironments({ searchAggregatedTransactions: boolean; includeMissing?: boolean; }) { - const spanName = serviceName + const operationName = serviceName ? 'get_all_environments_for_service' : 'get_all_environments_for_all_services'; - return withApmSpan(spanName, async () => { - const { apmEventClient, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - // omit filter for service.name if "All" option is selected - const serviceNameFilter = serviceName - ? [{ term: { [SERVICE_NAME]: serviceName } }] - : []; + const { apmEventClient, config } = setup; + const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - // use timeout + min_doc_count to return as early as possible - // if filter is not defined to prevent timeouts - ...(!serviceName ? { timeout: '1ms' } : {}), - size: 0, - query: { - bool: { - filter: [...serviceNameFilter], - }, + // omit filter for service.name if "All" option is selected + const serviceNameFilter = serviceName + ? [{ term: { [SERVICE_NAME]: serviceName } }] + : []; + + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + // use timeout + min_doc_count to return as early as possible + // if filter is not defined to prevent timeouts + ...(!serviceName ? { timeout: '1ms' } : {}), + size: 0, + query: { + bool: { + filter: [...serviceNameFilter], }, - aggs: { - environments: { - terms: { - field: SERVICE_ENVIRONMENT, - size: maxServiceEnvironments, - ...(!serviceName ? { min_doc_count: 0 } : {}), - missing: includeMissing - ? ENVIRONMENT_NOT_DEFINED.value - : undefined, - }, + }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + size: maxServiceEnvironments, + ...(!serviceName ? { min_doc_count: 0 } : {}), + missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined, }, }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); + const resp = await apmEventClient.search(operationName, params); - const environments = - resp.aggregations?.environments.buckets.map( - (bucket) => bucket.key as string - ) || []; - return environments; - }); + const environments = + resp.aggregations?.environments.buckets.map( + (bucket) => bucket.key as string + ) || []; + return environments; } diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts index 509e4cdcd67ac0..c0b267f180010a 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -12,7 +12,6 @@ import { import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeQuery } from '../../../server/utils/queries'; -import { withApmSpan } from '../../utils/with_apm_span'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -29,60 +28,58 @@ export async function getEnvironments({ serviceName?: string; searchAggregatedTransactions: boolean; }) { - const spanName = serviceName + const operationName = serviceName ? 'get_environments_for_service' : 'get_environments'; - return withApmSpan(spanName, async () => { - const { start, end, apmEventClient, config } = setup; + const { start, end, apmEventClient, config } = setup; - const filter = rangeQuery(start, end); + const filter = rangeQuery(start, end); - if (serviceName) { - filter.push({ - term: { [SERVICE_NAME]: serviceName }, - }); - } + if (serviceName) { + filter.push({ + term: { [SERVICE_NAME]: serviceName }, + }); + } - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; + const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.metric, - ProcessorEvent.error, - ], - }, - body: { - size: 0, - query: { - bool: { - filter, - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter, }, - aggs: { - environments: { - terms: { - field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED.value, - size: maxServiceEnvironments, - }, + }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + missing: ENVIRONMENT_NOT_DEFINED.value, + size: maxServiceEnvironments, }, }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); - const aggs = resp.aggregations; - const environmentsBuckets = aggs?.environments.buckets || []; + const resp = await apmEventClient.search(operationName, params); + const aggs = resp.aggregations; + const environmentsBuckets = aggs?.environments.buckets || []; - const environments = environmentsBuckets.map( - (environmentBucket) => environmentBucket.key as string - ); + const environments = environmentsBuckets.map( + (environmentBucket) => environmentBucket.key as string + ); - return environments; - }); + return environments; } diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap index 43fe4dfe752e65..2c0330f17320de 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap @@ -3,6 +3,7 @@ exports[`get buckets should make the correct query 1`] = ` Array [ Array [ + "get_error_distribution_buckets", Object { "apm": Object { "events": Array [ diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts index b1260d653f3de8..712343d445d44a 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts @@ -65,7 +65,7 @@ describe('get buckets', () => { }); it('should limit query results to error documents', () => { - const query = clientSpy.mock.calls[0][0]; + const query = clientSpy.mock.calls[0][1]; expect(query.apm.events).toEqual([ProcessorEvent.error]); }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 462c9bcdc43101..a51464764f2b44 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -16,7 +16,6 @@ import { rangeQuery, kqlQuery, } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getBuckets({ @@ -34,58 +33,59 @@ export async function getBuckets({ bucketSize: number; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_error_distribution_buckets', async () => { - const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; + const { start, end, apmEventClient } = setup; + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; - if (groupId) { - filter.push({ term: { [ERROR_GROUP_ID]: groupId } }); - } + if (groupId) { + filter.push({ term: { [ERROR_GROUP_ID]: groupId } }); + } - const params = { - apm: { - events: [ProcessorEvent.error], - }, - body: { - size: 0, - query: { - bool: { - filter, - }, + const params = { + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter, }, - aggs: { - distribution: { - histogram: { - field: '@timestamp', - min_doc_count: 0, - interval: bucketSize, - extended_bounds: { - min: start, - max: end, - }, + }, + aggs: { + distribution: { + histogram: { + field: '@timestamp', + min_doc_count: 0, + interval: bucketSize, + extended_bounds: { + min: start, + max: end, }, }, }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); + const resp = await apmEventClient.search( + 'get_error_distribution_buckets', + params + ); - const buckets = (resp.aggregations?.distribution.buckets || []).map( - (bucket) => ({ - key: bucket.key, - count: bucket.doc_count, - }) - ); + const buckets = (resp.aggregations?.distribution.buckets || []).map( + (bucket) => ({ + key: bucket.key, + count: bucket.doc_count, + }) + ); - return { - noHits: resp.hits.total.value === 0, - buckets: resp.hits.total.value > 0 ? buckets : [], - }; - }); + return { + noHits: resp.hits.total.value === 0, + buckets: resp.hits.total.value > 0 ? buckets : [], + }; } diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts index 57fb4861809932..a915a4fb033051 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts @@ -17,11 +17,10 @@ import { rangeQuery, kqlQuery, } from '../../../server/utils/queries'; -import { withApmSpan } from '../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; -export function getErrorGroupSample({ +export async function getErrorGroupSample({ environment, kuery, serviceName, @@ -34,48 +33,46 @@ export function getErrorGroupSample({ groupId: string; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_error_group_sample', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const params = { - apm: { - events: [ProcessorEvent.error as const], - }, - body: { - size: 1, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [ERROR_GROUP_ID]: groupId } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - should: [{ term: { [TRANSACTION_SAMPLED]: true } }], - }, + const params = { + apm: { + events: [ProcessorEvent.error as const], + }, + body: { + size: 1, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [ERROR_GROUP_ID]: groupId } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + should: [{ term: { [TRANSACTION_SAMPLED]: true } }], }, - sort: asMutableArray([ - { _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top - { '@timestamp': { order: 'desc' } }, // sort by timestamp to get the most recent error - ] as const), }, - }; + sort: asMutableArray([ + { _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top + { '@timestamp': { order: 'desc' } }, // sort by timestamp to get the most recent error + ] as const), + }, + }; - const resp = await apmEventClient.search(params); - const error = resp.hits.hits[0]?._source; - const transactionId = error?.transaction?.id; - const traceId = error?.trace?.id; + const resp = await apmEventClient.search('get_error_group_sample', params); + const error = resp.hits.hits[0]?._source; + const transactionId = error?.transaction?.id; + const traceId = error?.trace?.id; - let transaction; - if (transactionId && traceId) { - transaction = await getTransaction({ transactionId, traceId, setup }); - } + let transaction; + if (transactionId && traceId) { + transaction = await getTransaction({ transactionId, traceId, setup }); + } - return { - transaction, - error, - occurrencesCount: resp.hits.total.value, - }; - }); + return { + transaction, + error, + occurrencesCount: resp.hits.total.value, + }; } diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index f5b22e5349756c..d705a2eb5a00c8 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -15,11 +15,10 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { withApmSpan } from '../../utils/with_apm_span'; import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -export function getErrorGroups({ +export async function getErrorGroups({ environment, kuery, serviceName, @@ -34,87 +33,83 @@ export function getErrorGroups({ sortDirection?: 'asc' | 'desc'; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_error_groups', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - // sort buckets by last occurrence of error - const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; + // sort buckets by last occurrence of error + const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; - const projection = getErrorGroupsProjection({ - environment, - kuery, - setup, - serviceName, - }); + const projection = getErrorGroupsProjection({ + environment, + kuery, + setup, + serviceName, + }); - const order = sortByLatestOccurrence - ? { - max_timestamp: sortDirection, - } - : { _count: sortDirection }; + const order = sortByLatestOccurrence + ? { + max_timestamp: sortDirection, + } + : { _count: sortDirection }; - const params = mergeProjection(projection, { - body: { - size: 0, - aggs: { - error_groups: { - terms: { - ...projection.body.aggs.error_groups.terms, - size: 500, - order, - }, - aggs: { - sample: { - top_hits: { - _source: [ - ERROR_LOG_MESSAGE, - ERROR_EXC_MESSAGE, - ERROR_EXC_HANDLED, - ERROR_EXC_TYPE, - ERROR_CULPRIT, - ERROR_GROUP_ID, - '@timestamp', - ], - sort: [{ '@timestamp': 'desc' as const }], - size: 1, - }, + const params = mergeProjection(projection, { + body: { + size: 0, + aggs: { + error_groups: { + terms: { + ...projection.body.aggs.error_groups.terms, + size: 500, + order, + }, + aggs: { + sample: { + top_hits: { + _source: [ + ERROR_LOG_MESSAGE, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + ERROR_CULPRIT, + ERROR_GROUP_ID, + '@timestamp', + ], + sort: [{ '@timestamp': 'desc' as const }], + size: 1, }, - ...(sortByLatestOccurrence - ? { - max_timestamp: { - max: { - field: '@timestamp', - }, - }, - } - : {}), }, + ...(sortByLatestOccurrence + ? { + max_timestamp: { + max: { + field: '@timestamp', + }, + }, + } + : {}), }, }, }, - }); + }, + }); - const resp = await apmEventClient.search(params); + const resp = await apmEventClient.search('get_error_groups', params); - // aggregations can be undefined when no matching indices are found. - // this is an exception rather than the rule so the ES type does not account for this. - const hits = (resp.aggregations?.error_groups.buckets || []).map( - (bucket) => { - const source = bucket.sample.hits.hits[0]._source; - const message = getErrorName(source); + // aggregations can be undefined when no matching indices are found. + // this is an exception rather than the rule so the ES type does not account for this. + const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => { + const source = bucket.sample.hits.hits[0]._source; + const message = getErrorName(source); - return { - message, - occurrenceCount: bucket.doc_count, - culprit: source.error.culprit, - groupId: source.error.grouping_key, - latestOccurrenceAt: source['@timestamp'], - handled: source.error.exception?.[0].handled, - type: source.error.exception?.[0].type, - }; - } - ); - - return hits; + return { + message, + occurrenceCount: bucket.doc_count, + culprit: source.error.culprit, + groupId: source.error.grouping_key, + latestOccurrenceAt: source['@timestamp'], + handled: source.error.exception?.[0].handled, + type: source.error.exception?.[0].type, + }; }); + + return hits; } diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts index 394cf6b988f120..8bfb137c1689cf 100644 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts @@ -14,7 +14,6 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { APMConfig } from '../../..'; import { APMEventClient } from '../create_es_client/create_apm_event_client'; -import { withApmSpan } from '../../../utils/with_apm_span'; export async function getHasAggregatedTransactions({ start, @@ -25,8 +24,9 @@ export async function getHasAggregatedTransactions({ end?: number; apmEventClient: APMEventClient; }) { - return withApmSpan('get_has_aggregated_transactions', async () => { - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_has_aggregated_transactions', + { apm: { events: [ProcessorEvent.metric], }, @@ -41,14 +41,14 @@ export async function getHasAggregatedTransactions({ }, }, terminateAfter: 1, - }); - - if (response.hits.total.value > 0) { - return true; } + ); + + if (response.hits.total.value > 0) { + return true; + } - return false; - }); + return false; } export async function getSearchAggregatedTransactions({ diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 989297544c78fe..39018e26f371ce 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -81,17 +81,26 @@ export async function callAsyncWithDebug({ return res; } -export const getDebugBody = ( - params: Record, - requestType: string -) => { +export const getDebugBody = ({ + params, + requestType, + operationName, +}: { + params: Record; + requestType: string; + operationName: string; +}) => { + const operationLine = `${operationName}\n`; + if (requestType === 'search') { - return `GET ${params.index}/_search\n${formatObj(params.body)}`; + return `${operationLine}GET ${params.index}/_search\n${formatObj( + params.body + )}`; } return `${chalk.bold('ES operation:')} ${requestType}\n${chalk.bold( 'ES query:' - )}\n${formatObj(params)}`; + )}\n${operationLine}${formatObj(params)}`; }; export const getDebugTitle = (request: KibanaRequest) => diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts index addd7391d782d9..8e82a189d75f35 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -47,7 +47,7 @@ describe('createApmEventClient', () => { }, }); - await eventClient.search({ + await eventClient.search('foo', { apm: { events: [], }, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index b8a14253a229a2..916a6981f286a9 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -6,6 +6,7 @@ */ import { ValuesType } from 'utility-types'; +import { withApmSpan } from '../../../../utils/with_apm_span'; import { Profile } from '../../../../../typings/es_schemas/ui/profile'; import { ElasticsearchClient, @@ -34,6 +35,7 @@ import { unpackProcessorEvents } from './unpack_processor_events'; export type APMEventESSearchRequest = Omit & { apm: { events: ProcessorEvent[]; + includeLegacyData?: boolean; }; }; @@ -78,11 +80,13 @@ export function createApmEventClient({ }) { return { async search( - params: TParams, - { includeLegacyData = false } = {} + operationName: string, + params: TParams ): Promise> { const withProcessorEventFilter = unpackProcessorEvents(params, indices); + const { includeLegacyData = false } = params.apm; + const withPossibleLegacyDataFilter = !includeLegacyData ? addFilterToExcludeLegacyData(withProcessorEventFilter) : withProcessorEventFilter; @@ -98,15 +102,18 @@ export function createApmEventClient({ return callAsyncWithDebug({ cb: () => { - const searchPromise = cancelEsRequestOnAbort( - esClient.search(searchParams), - request + const searchPromise = withApmSpan(operationName, () => + cancelEsRequestOnAbort(esClient.search(searchParams), request) ); return unwrapEsResponse(searchPromise); }, getDebugMessage: () => ({ - body: getDebugBody(searchParams, requestType), + body: getDebugBody({ + params: searchParams, + requestType, + operationName, + }), title: getDebugTitle(request), }), isCalledWithInternalUser: false, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 1544538de74a65..e6b61a709ae353 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -31,20 +31,23 @@ export function createInternalESClient({ }: Pick & { debug: boolean }) { const { asInternalUser } = context.core.elasticsearch.client; - function callEs({ - cb, - requestType, - params, - }: { - requestType: string; - cb: () => TransportRequestPromise; - params: Record; - }) { + function callEs( + operationName: string, + { + cb, + requestType, + params, + }: { + requestType: string; + cb: () => TransportRequestPromise; + params: Record; + } + ) { return callAsyncWithDebug({ cb: () => unwrapEsResponse(cancelEsRequestOnAbort(cb(), request)), getDebugMessage: () => ({ title: getDebugTitle(request), - body: getDebugBody(params, requestType), + body: getDebugBody({ params, requestType, operationName }), }), debug, isCalledWithInternalUser: true, @@ -59,30 +62,37 @@ export function createInternalESClient({ TDocument = unknown, TSearchRequest extends ESSearchRequest = ESSearchRequest >( + operationName: string, params: TSearchRequest ): Promise> => { - return callEs({ + return callEs(operationName, { requestType: 'search', cb: () => asInternalUser.search(params), params, }); }, - index: (params: APMIndexDocumentParams) => { - return callEs({ + index: (operationName: string, params: APMIndexDocumentParams) => { + return callEs(operationName, { requestType: 'index', cb: () => asInternalUser.index(params), params, }); }, - delete: (params: estypes.DeleteRequest): Promise<{ result: string }> => { - return callEs({ + delete: ( + operationName: string, + params: estypes.DeleteRequest + ): Promise<{ result: string }> => { + return callEs(operationName, { requestType: 'delete', cb: () => asInternalUser.delete(params), params, }); }, - indicesCreate: (params: estypes.IndicesCreateRequest) => { - return callEs({ + indicesCreate: ( + operationName: string, + params: estypes.IndicesCreateRequest + ) => { + return callEs(operationName, { requestType: 'indices.create', cb: () => asInternalUser.indices.create(params), params, diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index c0ff0cab88f47f..66b3c91fc6f2d1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -109,7 +109,7 @@ describe('setupRequest', () => { it('calls callWithRequest', async () => { const mockResources = getMockResources(); const { apmEventClient } = await setupRequest(mockResources); - await apmEventClient.search({ + await apmEventClient.search('foo', { apm: { events: [ProcessorEvent.transaction] }, body: { foo: 'bar' }, }); @@ -137,7 +137,7 @@ describe('setupRequest', () => { it('calls callWithInternalUser', async () => { const mockResources = getMockResources(); const { internalClient } = await setupRequest(mockResources); - await internalClient.search({ + await internalClient.search('foo', { index: ['apm-*'], body: { foo: 'bar' }, } as any); @@ -156,7 +156,7 @@ describe('setupRequest', () => { it('adds a range filter for `observer.version_major` to the existing filter', async () => { const mockResources = getMockResources(); const { apmEventClient } = await setupRequest(mockResources); - await apmEventClient.search({ + await apmEventClient.search('foo', { apm: { events: [ProcessorEvent.transaction], }, @@ -183,19 +183,15 @@ describe('setupRequest', () => { it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { const mockResources = getMockResources(); const { apmEventClient } = await setupRequest(mockResources); - await apmEventClient.search( - { - apm: { - events: [ProcessorEvent.error], - }, - body: { - query: { bool: { filter: [{ term: { field: 'someTerm' } }] } }, - }, - }, - { + await apmEventClient.search('foo', { + apm: { + events: [ProcessorEvent.error], includeLegacyData: true, - } - ); + }, + body: { + query: { bool: { filter: [{ term: { field: 'someTerm' } }] } }, + }, + }); const params = mockResources.context.core.elasticsearch.client.asCurrentUser.search .mock.calls[0][0]; @@ -221,7 +217,7 @@ describe('without a bool filter', () => { it('adds a range filter for `observer.version_major`', async () => { const mockResources = getMockResources(); const { apmEventClient } = await setupRequest(mockResources); - await apmEventClient.search({ + await apmEventClient.search('foo', { apm: { events: [ProcessorEvent.error], }, @@ -251,7 +247,7 @@ describe('with includeFrozen=false', () => { const { apmEventClient } = await setupRequest(mockResources); - await apmEventClient.search({ + await apmEventClient.search('foo', { apm: { events: [], }, @@ -273,7 +269,7 @@ describe('with includeFrozen=true', () => { const { apmEventClient } = await setupRequest(mockResources); - await apmEventClient.search({ + await apmEventClient.search('foo', { apm: { events: [] }, }); diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 3b3ef8b9c4bcf5..d1040b49dcd8b7 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -30,6 +30,7 @@ export async function fetchAndTransformGcMetrics({ serviceNodeName, chartBase, fieldName, + operationName, }: { environment?: string; kuery?: string; @@ -38,6 +39,7 @@ export async function fetchAndTransformGcMetrics({ serviceNodeName?: string; chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; + operationName: string; }) { const { start, end, apmEventClient, config } = setup; @@ -108,7 +110,7 @@ export async function fetchAndTransformGcMetrics({ }, }); - const response = await apmEventClient.search(params); + const response = await apmEventClient.search(operationName, params); const { aggregations } = response; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts index 388331f3bbf179..3ec40d51716941 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { withApmSpan } from '../../../../../utils/with_apm_span'; import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../../../../helpers/setup_request'; import { fetchAndTransformGcMetrics } from './fetch_and_transform_gc_metrics'; @@ -45,17 +44,16 @@ function getGcRateChart({ serviceName: string; serviceNodeName?: string; }) { - return withApmSpan('get_gc_rate_charts', () => - fetchAndTransformGcMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - fieldName: METRIC_JAVA_GC_COUNT, - }) - ); + return fetchAndTransformGcMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + fieldName: METRIC_JAVA_GC_COUNT, + operationName: 'get_gc_rate_charts', + }); } export { getGcRateChart }; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts index e6f80190d1daa3..8e4416d94fb908 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { withApmSpan } from '../../../../../utils/with_apm_span'; import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../../../../helpers/setup_request'; import { fetchAndTransformGcMetrics } from './fetch_and_transform_gc_metrics'; @@ -45,17 +44,16 @@ function getGcTimeChart({ serviceName: string; serviceNodeName?: string; }) { - return withApmSpan('get_gc_time_charts', () => - fetchAndTransformGcMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - fieldName: METRIC_JAVA_GC_TIME, - }) - ); + return fetchAndTransformGcMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + fieldName: METRIC_JAVA_GC_TIME, + operationName: 'get_gc_time_charts', + }); } export { getGcTimeChart }; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index 7630827a3cb389..6a23213e94537d 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { withApmSpan } from '../../../../../utils/with_apm_span'; import { METRIC_JAVA_HEAP_MEMORY_MAX, METRIC_JAVA_HEAP_MEMORY_COMMITTED, @@ -65,22 +64,21 @@ export function getHeapMemoryChart({ serviceName: string; serviceNodeName?: string; }) { - return withApmSpan('get_heap_memory_charts', () => - fetchAndTransformMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - aggs: { - heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } }, - heapMemoryCommitted: { - avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED }, - }, - heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } }, + return fetchAndTransformMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } }, + heapMemoryCommitted: { + avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED }, }, - additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }], - }) - ); + heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } }, + }, + additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }], + operationName: 'get_heap_memory_charts', + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index cd11e5e5383b63..1ceb42b7db4799 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { withApmSpan } from '../../../../../utils/with_apm_span'; import { METRIC_JAVA_NON_HEAP_MEMORY_MAX, METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED, @@ -62,24 +61,23 @@ export async function getNonHeapMemoryChart({ serviceName: string; serviceNodeName?: string; }) { - return withApmSpan('get_non_heap_memory_charts', () => - fetchAndTransformMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - aggs: { - nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } }, - nonHeapMemoryCommitted: { - avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED }, - }, - nonHeapMemoryUsed: { - avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED }, - }, + return fetchAndTransformMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } }, + nonHeapMemoryCommitted: { + avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED }, }, - additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }], - }) - ); + nonHeapMemoryUsed: { + avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED }, + }, + }, + additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }], + operationName: 'get_non_heap_memory_charts', + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index 8d4c079197d191..700c5e08d4defa 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { withApmSpan } from '../../../../../utils/with_apm_span'; import { METRIC_JAVA_THREAD_COUNT, AGENT_NAME, @@ -54,19 +53,18 @@ export async function getThreadCountChart({ serviceName: string; serviceNodeName?: string; }) { - return withApmSpan('get_thread_count_charts', () => - fetchAndTransformMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - aggs: { - threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } }, - threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } }, - }, - additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }], - }) - ); + return fetchAndTransformMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } }, + threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } }, + }, + additionalFilters: [{ term: { [AGENT_NAME]: 'java' } }], + operationName: 'get_thread_count_charts', + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index 37bef191ae876e..a568d58bdd4389 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -7,7 +7,6 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { withApmSpan } from '../../../../../utils/with_apm_span'; import { METRIC_SYSTEM_CPU_PERCENT, METRIC_PROCESS_CPU_PERCENT, @@ -66,20 +65,19 @@ export function getCPUChartData({ serviceName: string; serviceNodeName?: string; }) { - return withApmSpan('get_cpu_metric_charts', () => - fetchAndTransformMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - aggs: { - systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, - systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, - processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, - processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } }, - }, - }) - ); + return fetchAndTransformMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, + systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, + processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, + processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } }, + }, + operationName: 'get_cpu_metric_charts', + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 0ec2f2c2fcfb25..1f7860d567b037 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -84,45 +84,41 @@ export async function getMemoryChartData({ serviceNodeName?: string; }) { return withApmSpan('get_memory_metrics_charts', async () => { - const cgroupResponse = await withApmSpan( - 'get_cgroup_memory_metrics_charts', - () => - fetchAndTransformMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - aggs: { - memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } }, - memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } }, - }, - additionalFilters: [ - { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, - ], - }) - ); + const cgroupResponse = await fetchAndTransformMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } }, + }, + additionalFilters: [ + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, + ], + operationName: 'get_cgroup_memory_metrics_charts', + }); if (cgroupResponse.noHits) { - return await withApmSpan('get_system_memory_metrics_charts', () => - fetchAndTransformMetrics({ - environment, - kuery, - setup, - serviceName, - serviceNodeName, - chartBase, - aggs: { - memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } }, - memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } }, - }, - additionalFilters: [ - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ], - }) - ); + return await fetchAndTransformMetrics({ + environment, + kuery, + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } }, + }, + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + operationName: 'get_system_memory_metrics_charts', + }); } return cgroupResponse; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 30234447821ec9..df9e33e6f4b409 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -56,6 +56,7 @@ export async function fetchAndTransformMetrics({ chartBase, aggs, additionalFilters = [], + operationName, }: { environment?: string; kuery?: string; @@ -65,6 +66,7 @@ export async function fetchAndTransformMetrics({ chartBase: ChartBase; aggs: T; additionalFilters?: Filter[]; + operationName: string; }) { const { start, end, apmEventClient, config } = setup; @@ -98,7 +100,7 @@ export async function fetchAndTransformMetrics({ }, }); - const response = await apmEventClient.search(params); + const response = await apmEventClient.search(operationName, params); return transformDataToMetricsChart(response, chartBase); } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 2ccbe318862f1f..086516371387e6 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -10,40 +10,40 @@ import { rangeQuery } from '../../../server/utils/queries'; import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getServiceCount({ +export async function getServiceCount({ setup, searchAggregatedTransactions, }: { setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { - return withApmSpan('observability_overview_get_service_count', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), }, - aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, }, - }; + aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, + }, + }; - const { aggregations } = await apmEventClient.search(params); - return aggregations?.serviceCount.value || 0; - }); + const { aggregations } = await apmEventClient.search( + 'observability_overview_get_service_count', + params + ); + return aggregations?.serviceCount.value || 0; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts index da8ac7c50b5947..016cb50566da0d 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -14,9 +14,8 @@ import { rangeQuery } from '../../../server/utils/queries'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { calculateThroughput } from '../helpers/calculate_throughput'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getTransactionsPerMinute({ +export async function getTransactionsPerMinute({ setup, bucketSize, searchAggregatedTransactions, @@ -25,71 +24,69 @@ export function getTransactionsPerMinute({ bucketSize: string; searchAggregatedTransactions: boolean; }) { - return withApmSpan( - 'observability_overview_get_transactions_per_minute', - async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const { aggregations } = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], + const { aggregations } = await apmEventClient.search( + 'observability_overview_get_transactions_per_minute', + { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), + }, }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, }, - }, - aggs: { - transactionType: { - terms: { - field: TRANSACTION_TYPE, - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - min_doc_count: 0, - }, - aggs: { - throughput: { rate: { unit: 'minute' as const } }, - }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, }, }, }, }, }, - }); + }, + } + ); - if (!aggregations || !aggregations.transactionType.buckets) { - return { value: undefined, timeseries: [] }; - } + if (!aggregations || !aggregations.transactionType.buckets) { + return { value: undefined, timeseries: [] }; + } - const topTransactionTypeBucket = - aggregations.transactionType.buckets.find( - ({ key: transactionType }) => - transactionType === TRANSACTION_REQUEST || - transactionType === TRANSACTION_PAGE_LOAD - ) || aggregations.transactionType.buckets[0]; + const topTransactionTypeBucket = + aggregations.transactionType.buckets.find( + ({ key: transactionType }) => + transactionType === TRANSACTION_REQUEST || + transactionType === TRANSACTION_PAGE_LOAD + ) || aggregations.transactionType.buckets[0]; - return { - value: calculateThroughput({ - start, - end, - value: topTransactionTypeBucket?.doc_count || 0, - }), - timeseries: - topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.throughput.value, - })) || [], - }; - } - ); + return { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket?.doc_count || 0, + }), + timeseries: + topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.throughput.value, + })) || [], + }; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index bbe13874d7d3b4..5c1a33e750e12f 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -6,31 +6,31 @@ */ import { ProcessorEvent } from '../../../common/processor_event'; -import { withApmSpan } from '../../utils/with_apm_span'; import { Setup } from '../helpers/setup_request'; -export function getHasData({ setup }: { setup: Setup }) { - return withApmSpan('observability_overview_has_apm_data', async () => { - const { apmEventClient } = setup; - try { - const params = { - apm: { - events: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - terminateAfter: 1, - body: { - size: 0, - }, - }; +export async function getHasData({ setup }: { setup: Setup }) { + const { apmEventClient } = setup; + try { + const params = { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + terminateAfter: 1, + body: { + size: 0, + }, + }; - const response = await apmEventClient.search(params); - return response.hits.total.value > 0; - } catch (e) { - return false; - } - }); + const response = await apmEventClient.search( + 'observability_overview_has_apm_data', + params + ); + return response.hits.total.value > 0; + } catch (e) { + return false; + } } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index ea1a65020e0cca..e56f234c0633ea 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -63,7 +63,7 @@ export async function getClientMetrics({ }); const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_client_metrics', params); const { hasFetchStartField: { backEnd, totalPageLoadDuration }, } = response.aggregations!; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts index d65399f91bef84..6f734a214501d1 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts @@ -94,7 +94,7 @@ export async function getJSErrors({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_js_errors', params); const { totalErrorGroups, totalErrorPages, errors } = response.aggregations ?? {}; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts index c873b9b4aed829..c4c6f613172d14 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts @@ -64,7 +64,7 @@ export async function getLongTaskMetrics({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_long_task_metrics', params); const pkey = percentile.toFixed(1); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 0f1d7146f84596..73d634e3134d18 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -117,7 +117,7 @@ export async function getPageLoadDistribution({ const { aggregations, hits: { total }, - } = await apmEventClient.search(params); + } = await apmEventClient.search('get_page_load_distribution', params); if (total.value === 0) { return null; @@ -210,7 +210,10 @@ const getPercentilesDistribution = async ({ const { apmEventClient } = setup; - const { aggregations } = await apmEventClient.search(params); + const { aggregations } = await apmEventClient.search( + 'get_page_load_distribution', + params + ); return aggregations?.loadDistribution.values ?? []; }; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 13046e6c5a8738..41af2ae166aaf6 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -69,7 +69,7 @@ export async function getPageViewTrends({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_page_view_trends', params); const { topBreakdowns } = response.aggregations ?? {}; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 6a6caab9537333..e63d834307a5f0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -92,7 +92,10 @@ export const getPageLoadDistBreakdown = async ({ const { apmEventClient } = setup; - const { aggregations } = await apmEventClient.search(params); + const { aggregations } = await apmEventClient.search( + 'get_page_load_dist_breakdown', + params + ); const pageDistBreakdowns = aggregations?.breakdowns.buckets; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index ffe9225f1ab99a..a2e6b55738d3a1 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -38,7 +38,7 @@ export async function getRumServices({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_rum_services', params); const result = response.aggregations?.services.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts index 7cf9066fb4d4da..ae65cdbd121ea4 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts @@ -56,7 +56,7 @@ export async function getUrlSearch({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_url_search', params); const { urls, totalUrls } = response.aggregations ?? {}; const pkey = percentile.toFixed(1); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 247b808896e41a..9c7a64d7c6481f 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -51,7 +51,7 @@ export async function getVisitorBreakdown({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_visitor_breakdown', params); const { browsers, os } = response.aggregations!; const totalItems = response.hits.total.value; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 9bde701df56721..bbb301e22aa8de 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -103,7 +103,7 @@ export async function getWebCoreVitals({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_web_core_vitals', params); const { lcp, cls, diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 87136fc0538a6b..fc5da4ec1d0fa6 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -51,7 +51,7 @@ export async function hasRumData({ const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('has_rum_data', params); return { indices: setup.indices['apm_oss.transactionIndices']!, hasData: response.hits.total.value > 0, diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts index 8fdeb77171862b..e0e9bb2ca002f4 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts @@ -38,37 +38,38 @@ export function getLocalUIFilters({ delete projectionWithoutAggs.body.aggs; return Promise.all( - localFilterNames.map(async (name) => - withApmSpan('get_ui_filter_options_for_field', async () => { - const query = getLocalFilterQuery({ - uiFilters, - projection, - localUIFilterName: name, - }); + localFilterNames.map(async (name) => { + const query = getLocalFilterQuery({ + uiFilters, + projection, + localUIFilterName: name, + }); - const response = await apmEventClient.search(query); + const response = await apmEventClient.search( + 'get_ui_filter_options_for_field', + query + ); - const filter = localUIFilters[name]; + const filter = localUIFilters[name]; - const buckets = response?.aggregations?.by_terms?.buckets ?? []; + const buckets = response?.aggregations?.by_terms?.buckets ?? []; - return { - ...filter, - options: orderBy( - buckets.map((bucket) => { - return { - name: bucket.key as string, - count: bucket.bucket_count - ? bucket.bucket_count.value - : bucket.doc_count, - }; - }), - 'count', - 'desc' - ), - }; - }) - ) + return { + ...filter, + options: orderBy( + buckets.map((bucket) => { + return { + name: bucket.key as string, + count: bucket.bucket_count + ? bucket.bucket_count.value + : bucket.doc_count, + }; + }), + 'count', + 'desc' + ), + }; + }) ); }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 6047b97651e6ad..6ecfe425dc8c5a 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -14,44 +14,42 @@ import { ServiceConnectionNode, } from '../../../common/service_map'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { withApmSpan } from '../../utils/with_apm_span'; export async function fetchServicePathsFromTraceIds( setup: Setup & SetupTimeRange, traceIds: string[] ) { - return withApmSpan('get_service_paths_from_trace_ids', async () => { - const { apmEventClient } = setup; - - // make sure there's a range so ES can skip shards - const dayInMs = 24 * 60 * 60 * 1000; - const start = setup.start - dayInMs; - const end = setup.end + dayInMs; - - const serviceMapParams = { - apm: { - events: [ProcessorEvent.span, ProcessorEvent.transaction], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { - terms: { - [TRACE_ID]: traceIds, - }, + const { apmEventClient } = setup; + + // make sure there's a range so ES can skip shards + const dayInMs = 24 * 60 * 60 * 1000; + const start = setup.start - dayInMs; + const end = setup.end + dayInMs; + + const serviceMapParams = { + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + [TRACE_ID]: traceIds, }, - ...rangeQuery(start, end), - ], - }, + }, + ...rangeQuery(start, end), + ], }, - aggs: { - service_map: { - scripted_metric: { - init_script: { - lang: 'painless', - source: `state.eventsById = new HashMap(); + }, + aggs: { + service_map: { + scripted_metric: { + init_script: { + lang: 'painless', + source: `state.eventsById = new HashMap(); String[] fieldsToCopy = new String[] { 'parent.id', @@ -65,10 +63,10 @@ export async function fetchServicePathsFromTraceIds( 'agent.name' }; state.fieldsToCopy = fieldsToCopy;`, - }, - map_script: { - lang: 'painless', - source: `def id; + }, + map_script: { + lang: 'painless', + source: `def id; if (!doc['span.id'].empty) { id = doc['span.id'].value; } else { @@ -85,14 +83,14 @@ export async function fetchServicePathsFromTraceIds( } state.eventsById[id] = copy`, - }, - combine_script: { - lang: 'painless', - source: `return state.eventsById;`, - }, - reduce_script: { - lang: 'painless', - source: ` + }, + combine_script: { + lang: 'painless', + source: `return state.eventsById;`, + }, + reduce_script: { + lang: 'painless', + source: ` def getDestination ( def event ) { def destination = new HashMap(); destination['span.destination.service.resource'] = event['span.destination.service.resource']; @@ -208,29 +206,29 @@ export async function fetchServicePathsFromTraceIds( response.discoveredServices = discoveredServices; return response;`, - }, }, }, } as const, }, - }; - - const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( - serviceMapParams - ); - - return serviceMapFromTraceIdsScriptResponse as { - aggregations?: { - service_map: { - value: { - paths: ConnectionNode[][]; - discoveredServices: Array<{ - from: ExternalConnectionNode; - to: ServiceConnectionNode; - }>; - }; + }, + }; + + const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( + 'get_service_paths_from_trace_ids', + serviceMapParams + ); + + return serviceMapFromTraceIdsScriptResponse as { + aggregations?: { + service_map: { + value: { + paths: ConnectionNode[][]; + discoveredServices: Array<{ + from: ExternalConnectionNode; + to: ServiceConnectionNode; + }>; }; }; }; - }); + }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index e5b0b72b8784ae..6d50023d3fd0e7 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -87,69 +87,70 @@ async function getConnectionData({ } async function getServicesData(options: IEnvOptions) { - return withApmSpan('get_service_stats_for_service_map', async () => { - const { environment, setup, searchAggregatedTransactions } = options; + const { environment, setup, searchAggregatedTransactions } = options; - const projection = getServicesProjection({ - setup, - searchAggregatedTransactions, - }); + const projection = getServicesProjection({ + setup, + searchAggregatedTransactions, + }); - let filter = [ - ...projection.body.query.bool.filter, - ...environmentQuery(environment), - ]; + let filter = [ + ...projection.body.query.bool.filter, + ...environmentQuery(environment), + ]; - if (options.serviceName) { - filter = filter.concat({ - term: { - [SERVICE_NAME]: options.serviceName, + if (options.serviceName) { + filter = filter.concat({ + term: { + [SERVICE_NAME]: options.serviceName, + }, + }); + } + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: { + ...projection.body.query.bool, + filter, }, - }); - } - - const params = mergeProjection(projection, { - body: { - size: 0, - query: { - bool: { - ...projection.body.query.bool, - filter, + }, + aggs: { + services: { + terms: { + field: projection.body.aggs.services.terms.field, + size: 500, }, - }, - aggs: { - services: { - terms: { - field: projection.body.aggs.services.terms.field, - size: 500, - }, - aggs: { - agent_name: { - terms: { - field: AGENT_NAME, - }, + aggs: { + agent_name: { + terms: { + field: AGENT_NAME, }, }, }, }, }, - }); + }, + }); - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search( + 'get_service_stats_for_service_map', + params + ); - return ( - response.aggregations?.services.buckets.map((bucket) => { - return { - [SERVICE_NAME]: bucket.key as string, - [AGENT_NAME]: - (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - [SERVICE_ENVIRONMENT]: options.environment || null, - }; - }) || [] - ); - }); + return ( + response.aggregations?.services.buckets.map((bucket) => { + return { + [SERVICE_NAME]: bucket.key as string, + [AGENT_NAME]: + (bucket.agent_name.buckets[0]?.key as string | undefined) || '', + [SERVICE_ENVIRONMENT]: options.environment || null, + }; + }) || [] + ); } export type ConnectionsResponse = PromiseReturnType; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 9850c36c573dd7..2709fb640d8ce3 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -120,7 +120,7 @@ async function getErrorStats({ }); } -function getTransactionStats({ +async function getTransactionStats({ setup, filter, minutes, @@ -129,68 +129,70 @@ function getTransactionStats({ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }> { - return withApmSpan('get_transaction_stats_for_service_map_node', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...filter, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - { - terms: { - [TRANSACTION_TYPE]: [ - TRANSACTION_REQUEST, - TRANSACTION_PAGE_LOAD, - ], - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + { + terms: { + [TRANSACTION_TYPE]: [ + TRANSACTION_REQUEST, + TRANSACTION_PAGE_LOAD, + ], }, - ], - }, - }, - track_total_hits: true, - aggs: { - duration: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), }, + ], + }, + }, + track_total_hits: true, + aggs: { + duration: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }, - }; - const response = await apmEventClient.search(params); + }, + }; + const response = await apmEventClient.search( + 'get_transaction_stats_for_service_map_node', + params + ); - const totalRequests = response.hits.total.value; + const totalRequests = response.hits.total.value; - return { - avgTransactionDuration: response.aggregations?.duration.value ?? null, - avgRequestsPerMinute: totalRequests > 0 ? totalRequests / minutes : null, - }; - }); + return { + avgTransactionDuration: response.aggregations?.duration.value ?? null, + avgRequestsPerMinute: totalRequests > 0 ? totalRequests / minutes : null, + }; } -function getCpuStats({ +async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { - return withApmSpan('get_avg_cpu_usage_for_service_map_node', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_avg_cpu_usage_for_service_map_node', + { apm: { events: [ProcessorEvent.metric], }, @@ -206,10 +208,10 @@ function getCpuStats({ }, aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, }, - }); + } + ); - return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null }; - }); + return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null }; } function getMemoryStats({ @@ -219,7 +221,7 @@ function getMemoryStats({ return withApmSpan('get_memory_stats_for_service_map_node', async () => { const { apmEventClient } = setup; - const getAvgMemoryUsage = ({ + const getAvgMemoryUsage = async ({ additionalFilters, script, }: { @@ -228,8 +230,9 @@ function getMemoryStats({ | typeof percentCgroupMemoryUsedScript | typeof percentSystemMemoryUsedScript; }) => { - return withApmSpan('get_avg_memory_for_service_map_node', async () => { - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_avg_memory_for_service_map_node', + { apm: { events: [ProcessorEvent.metric], }, @@ -244,9 +247,9 @@ function getMemoryStats({ avgMemoryUsage: { avg: { script } }, }, }, - }); - return response.aggregations?.avgMemoryUsage.value ?? null; - }); + } + ); + return response.aggregations?.avgMemoryUsage.value ?? null; }; let avgMemoryUsage = await getAvgMemoryUsage({ diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index fa04b963388b20..7894a95cf4d7e4 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -18,12 +18,11 @@ import { import { ProcessorEvent } from '../../../common/processor_event'; import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; import { environmentQuery, rangeQuery } from '../../../server/utils/queries'; -import { withApmSpan } from '../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; const MAX_TRACES_TO_INSPECT = 1000; -export function getTraceSampleIds({ +export async function getTraceSampleIds({ serviceName, environment, setup, @@ -32,90 +31,88 @@ export function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_trace_sample_ids', async () => { - const { start, end, apmEventClient, config } = setup; + const { start, end, apmEventClient, config } = setup; - const query = { - bool: { - filter: [ - { - exists: { - field: SPAN_DESTINATION_SERVICE_RESOURCE, - }, + const query = { + bool: { + filter: [ + { + exists: { + field: SPAN_DESTINATION_SERVICE_RESOURCE, }, - ...rangeQuery(start, end), - ] as ESFilter[], - }, - } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } }; + }, + ...rangeQuery(start, end), + ] as ESFilter[], + }, + } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } }; - if (serviceName) { - query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } }); - } + if (serviceName) { + query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } }); + } - query.bool.filter.push(...environmentQuery(environment)); + query.bool.filter.push(...environmentQuery(environment)); - const fingerprintBucketSize = serviceName - ? config['xpack.apm.serviceMapFingerprintBucketSize'] - : config['xpack.apm.serviceMapFingerprintGlobalBucketSize']; + const fingerprintBucketSize = serviceName + ? config['xpack.apm.serviceMapFingerprintBucketSize'] + : config['xpack.apm.serviceMapFingerprintGlobalBucketSize']; - const traceIdBucketSize = serviceName - ? config['xpack.apm.serviceMapTraceIdBucketSize'] - : config['xpack.apm.serviceMapTraceIdGlobalBucketSize']; + const traceIdBucketSize = serviceName + ? config['xpack.apm.serviceMapTraceIdBucketSize'] + : config['xpack.apm.serviceMapTraceIdGlobalBucketSize']; - const samplerShardSize = traceIdBucketSize * 10; + const samplerShardSize = traceIdBucketSize * 10; - const params = { - apm: { - events: [ProcessorEvent.span], - }, - body: { - size: 0, - query, - aggs: { - connections: { - composite: { - sources: asMutableArray([ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: { - terms: { - field: SPAN_DESTINATION_SERVICE_RESOURCE, - }, + const params = { + apm: { + events: [ProcessorEvent.span], + }, + body: { + size: 0, + query, + aggs: { + connections: { + composite: { + sources: asMutableArray([ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: { + terms: { + field: SPAN_DESTINATION_SERVICE_RESOURCE, }, }, - { - [SERVICE_NAME]: { - terms: { - field: SERVICE_NAME, - }, + }, + { + [SERVICE_NAME]: { + terms: { + field: SERVICE_NAME, }, }, - { - [SERVICE_ENVIRONMENT]: { - terms: { - field: SERVICE_ENVIRONMENT, - missing_bucket: true, - }, + }, + { + [SERVICE_ENVIRONMENT]: { + terms: { + field: SERVICE_ENVIRONMENT, + missing_bucket: true, }, }, - ] as const), - size: fingerprintBucketSize, - }, - aggs: { - sample: { - sampler: { - shard_size: samplerShardSize, - }, - aggs: { - trace_ids: { - terms: { - field: TRACE_ID, - size: traceIdBucketSize, - execution_hint: 'map' as const, - // remove bias towards large traces by sorting on trace.id - // which will be random-esque - order: { - _key: 'desc' as const, - }, + }, + ] as const), + size: fingerprintBucketSize, + }, + aggs: { + sample: { + sampler: { + shard_size: samplerShardSize, + }, + aggs: { + trace_ids: { + terms: { + field: TRACE_ID, + size: traceIdBucketSize, + execution_hint: 'map' as const, + // remove bias towards large traces by sorting on trace.id + // which will be random-esque + order: { + _key: 'desc' as const, }, }, }, @@ -124,34 +121,36 @@ export function getTraceSampleIds({ }, }, }, - }; + }, + }; - try { - const tracesSampleResponse = await apmEventClient.search(params); - // make sure at least one trace per composite/connection bucket - // is queried - const traceIdsWithPriority = - tracesSampleResponse.aggregations?.connections.buckets.flatMap( - (bucket) => - bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({ - traceId: sampleDocBucket.key as string, - priority: index, - })) - ) || []; + try { + const tracesSampleResponse = await apmEventClient.search( + 'get_trace_sample_ids', + params + ); + // make sure at least one trace per composite/connection bucket + // is queried + const traceIdsWithPriority = + tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) => + bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({ + traceId: sampleDocBucket.key as string, + priority: index, + })) + ) || []; - const traceIds = take( - uniq( - sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId) - ), - MAX_TRACES_TO_INSPECT - ); + const traceIds = take( + uniq( + sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId) + ), + MAX_TRACES_TO_INSPECT + ); - return { traceIds }; - } catch (error) { - if ('displayName' in error && error.displayName === 'RequestTimeout') { - throw Boom.internal(SERVICE_MAP_TIMEOUT_ERROR); - } - throw error; + return { traceIds }; + } catch (error) { + if ('displayName' in error && error.displayName === 'RequestTimeout') { + throw Boom.internal(SERVICE_MAP_TIMEOUT_ERROR); } - }); + throw error; + } } diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index 07b7e532d80558..97c553f344205a 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -14,10 +14,9 @@ import { import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { getServiceNodesProjection } from '../../projections/service_nodes'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { withApmSpan } from '../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -const getServiceNodes = ({ +const getServiceNodes = async ({ kuery, setup, serviceName, @@ -26,69 +25,67 @@ const getServiceNodes = ({ setup: Setup & SetupTimeRange; serviceName: string; }) => { - return withApmSpan('get_service_nodes', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const projection = getServiceNodesProjection({ kuery, setup, serviceName }); + const projection = getServiceNodesProjection({ kuery, setup, serviceName }); - const params = mergeProjection(projection, { - body: { - aggs: { - nodes: { - terms: { - ...projection.body.aggs.nodes.terms, - size: 10000, - missing: SERVICE_NODE_NAME_MISSING, - }, - aggs: { - cpu: { - avg: { - field: METRIC_PROCESS_CPU_PERCENT, - }, + const params = mergeProjection(projection, { + body: { + aggs: { + nodes: { + terms: { + ...projection.body.aggs.nodes.terms, + size: 10000, + missing: SERVICE_NODE_NAME_MISSING, + }, + aggs: { + cpu: { + avg: { + field: METRIC_PROCESS_CPU_PERCENT, }, - heapMemory: { - avg: { - field: METRIC_JAVA_HEAP_MEMORY_USED, - }, + }, + heapMemory: { + avg: { + field: METRIC_JAVA_HEAP_MEMORY_USED, }, - nonHeapMemory: { - avg: { - field: METRIC_JAVA_NON_HEAP_MEMORY_USED, - }, + }, + nonHeapMemory: { + avg: { + field: METRIC_JAVA_NON_HEAP_MEMORY_USED, }, - threadCount: { - max: { - field: METRIC_JAVA_THREAD_COUNT, - }, + }, + threadCount: { + max: { + field: METRIC_JAVA_THREAD_COUNT, }, }, }, }, }, - }); + }, + }); - const response = await apmEventClient.search(params); + const response = await apmEventClient.search('get_service_nodes', params); - if (!response.aggregations) { - return []; - } + if (!response.aggregations) { + return []; + } - return response.aggregations.nodes.buckets - .map((bucket) => ({ - name: bucket.key as string, - cpu: bucket.cpu.value, - heapMemory: bucket.heapMemory.value, - nonHeapMemory: bucket.nonHeapMemory.value, - threadCount: bucket.threadCount.value, - })) - .filter( - (item) => - item.cpu !== null || - item.heapMemory !== null || - item.nonHeapMemory !== null || - item.threadCount != null - ); - }); + return response.aggregations.nodes.buckets + .map((bucket) => ({ + name: bucket.key as string, + cpu: bucket.cpu.value, + heapMemory: bucket.heapMemory.value, + nonHeapMemory: bucket.nonHeapMemory.value, + threadCount: bucket.threadCount.value, + })) + .filter( + (item) => + item.cpu !== null || + item.heapMemory !== null || + item.nonHeapMemory !== null || + item.threadCount != null + ); }; export { getServiceNodes }; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 9d05369aca840f..2f653e2c4df1db 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -22,6 +22,7 @@ Object { "events": Array [ "transaction", ], + "includeLegacyData": true, }, "body": Object { "query": Object { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 611f9b18a0b1a2..202b5075d2ea74 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -13,7 +13,6 @@ import { SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -31,20 +30,52 @@ export async function getDerivedServiceAnnotations({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { - return withApmSpan('get_derived_service_annotations', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ...environmentQuery(environment), - ]; + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ...environmentQuery(environment), + ]; - const versions = - ( - await apmEventClient.search({ + const versions = + ( + await apmEventClient.search('get_derived_service_annotations', { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [...filter, ...rangeQuery(start, end)], + }, + }, + aggs: { + versions: { + terms: { + field: SERVICE_VERSION, + }, + }, + }, + }, + }) + ).aggregations?.versions.buckets.map((bucket) => bucket.key) ?? []; + + if (versions.length <= 1) { + return []; + } + const annotations = await Promise.all( + versions.map(async (version) => { + const response = await apmEventClient.search( + 'get_first_seen_of_version', + { apm: { events: [ getProcessorEventForAggregatedTransactions( @@ -53,73 +84,40 @@ export async function getDerivedServiceAnnotations({ ], }, body: { - size: 0, + size: 1, query: { bool: { - filter: [...filter, ...rangeQuery(start, end)], + filter: [...filter, { term: { [SERVICE_VERSION]: version } }], }, }, - aggs: { - versions: { - terms: { - field: SERVICE_VERSION, - }, - }, + sort: { + '@timestamp': 'asc', }, }, - }) - ).aggregations?.versions.buckets.map((bucket) => bucket.key) ?? []; + } + ); - if (versions.length <= 1) { - return []; - } - const annotations = await Promise.all( - versions.map(async (version) => { - return withApmSpan('get_first_seen_of_version', async () => { - const response = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 1, - query: { - bool: { - filter: [...filter, { term: { [SERVICE_VERSION]: version } }], - }, - }, - sort: { - '@timestamp': 'asc', - }, - }, - }); - - const firstSeen = new Date( - response.hits.hits[0]._source['@timestamp'] - ).getTime(); + const firstSeen = new Date( + response.hits.hits[0]._source['@timestamp'] + ).getTime(); - if (!isFiniteNumber(firstSeen)) { - throw new Error( - 'First seen for version was unexpectedly undefined or null.' - ); - } + if (!isFiniteNumber(firstSeen)) { + throw new Error( + 'First seen for version was unexpectedly undefined or null.' + ); + } - if (firstSeen < start || firstSeen > end) { - return null; - } + if (firstSeen < start || firstSeen > end) { + return null; + } - return { - type: AnnotationType.VERSION, - id: version, - '@timestamp': firstSeen, - text: version, - }; - }); - }) - ); - return annotations.filter(Boolean) as Annotation[]; - }); + return { + type: AnnotationType.VERSION, + id: version, + '@timestamp': firstSeen, + text: version, + }; + }) + ); + return annotations.filter(Boolean) as Annotation[]; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index a81c0b2fc2c443..82147d7c94236e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -13,9 +13,8 @@ import { import { rangeQuery } from '../../../server/utils/queries'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getServiceAgentName({ +export async function getServiceAgentName({ serviceName, setup, searchAggregatedTransactions, @@ -24,42 +23,41 @@ export function getServiceAgentName({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { - return withApmSpan('get_service_agent_name', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const params = { - terminateAfter: 1, - apm: { - events: [ - ProcessorEvent.error, - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.metric, - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ], - }, + const params = { + terminateAfter: 1, + apm: { + events: [ + ProcessorEvent.error, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.metric, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ], }, - aggs: { - agents: { - terms: { field: AGENT_NAME, size: 1 }, - }, + }, + aggs: { + agents: { + terms: { field: AGENT_NAME, size: 1 }, }, }, - }; + }, + }; - const { aggregations } = await apmEventClient.search(params); - const agentName = aggregations?.agents.buckets[0]?.key as - | string - | undefined; - return { agentName }; - }); + const { aggregations } = await apmEventClient.search( + 'get_service_agent_name', + params + ); + const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; + return { agentName }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts index db491012c986b1..4993484f5b2400 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts @@ -38,56 +38,54 @@ export const getDestinationMap = ({ return withApmSpan('get_service_destination_map', async () => { const { start, end, apmEventClient } = setup; - const response = await withApmSpan('get_exit_span_samples', async () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.span], + const response = await apmEventClient.search('get_exit_span_samples', { + apm: { + events: [ProcessorEvent.span], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ], + }, }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ], + aggs: { + connections: { + composite: { + size: 1000, + sources: asMutableArray([ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: { + terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, + }, + }, + // make sure we get samples for both successful + // and failed calls + { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, + ] as const), }, - }, - aggs: { - connections: { - composite: { - size: 1000, - sources: asMutableArray([ - { - [SPAN_DESTINATION_SERVICE_RESOURCE]: { - terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], + sort: [ + { + '@timestamp': 'desc' as const, }, - }, - // make sure we get samples for both successful - // and failed calls - { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, - ] as const), - }, - aggs: { - sample: { - top_hits: { - size: 1, - _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], - sort: [ - { - '@timestamp': 'desc' as const, - }, - ], - }, + ], }, }, }, }, }, - }) - ); + }, + }); const outgoingConnections = response.aggregations?.connections.buckets.map((bucket) => { @@ -103,38 +101,37 @@ export const getDestinationMap = ({ }; }) ?? []; - const transactionResponse = await withApmSpan( + const transactionResponse = await apmEventClient.search( 'get_transactions_for_exit_spans', - () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - { - terms: { - [PARENT_ID]: outgoingConnections.map( - (connection) => connection[SPAN_ID] - ), - }, + { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + { + terms: { + [PARENT_ID]: outgoingConnections.map( + (connection) => connection[SPAN_ID] + ), }, - ...rangeQuery(start, end), - ], - }, + }, + ...rangeQuery(start, end), + ], }, - size: outgoingConnections.length, - docvalue_fields: asMutableArray([ - SERVICE_NAME, - SERVICE_ENVIRONMENT, - AGENT_NAME, - PARENT_ID, - ] as const), - _source: false, }, - }) + size: outgoingConnections.length, + docvalue_fields: asMutableArray([ + SERVICE_NAME, + SERVICE_ENVIRONMENT, + AGENT_NAME, + PARENT_ID, + ] as const), + _source: false, + }, + } ); const incomingConnections = transactionResponse.hits.hits.map((hit) => ({ diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts index c8642c6272b5f4..1d815dd7180e3f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts @@ -18,9 +18,8 @@ import { environmentQuery, rangeQuery } from '../../../../server/utils/queries'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { EventOutcome } from '../../../../common/event_outcome'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export const getMetrics = ({ +export const getMetrics = async ({ setup, serviceName, environment, @@ -31,10 +30,11 @@ export const getMetrics = ({ environment?: string; numBuckets: number; }) => { - return withApmSpan('get_service_destination_metrics', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_service_destination_metrics', + { apm: { events: [ProcessorEvent.metric], }, @@ -46,7 +46,9 @@ export const getMetrics = ({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, { - exists: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, + exists: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, }, ...rangeQuery(start, end), ...environmentQuery(environment), @@ -99,47 +101,47 @@ export const getMetrics = ({ }, }, }, - }); + } + ); - return ( - response.aggregations?.connections.buckets.map((bucket) => ({ - span: { - destination: { - service: { - resource: String(bucket.key), - }, + return ( + response.aggregations?.connections.buckets.map((bucket) => ({ + span: { + destination: { + service: { + resource: String(bucket.key), }, }, - value: { - count: sum( - bucket.timeseries.buckets.map( - (dateBucket) => dateBucket.count.value ?? 0 - ) - ), - latency_sum: sum( - bucket.timeseries.buckets.map( - (dateBucket) => dateBucket.latency_sum.value ?? 0 - ) - ), - error_count: sum( - bucket.timeseries.buckets.flatMap( - (dateBucket) => - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0 - ) - ), - }, - timeseries: bucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - count: dateBucket.count.value ?? 0, - latency_sum: dateBucket.latency_sum.value ?? 0, - error_count: - dateBucket[EVENT_OUTCOME].buckets.find( - (outcomeBucket) => outcomeBucket.key === EventOutcome.failure - )?.count.value ?? 0, - })), - })) ?? [] - ); - }); + }, + value: { + count: sum( + bucket.timeseries.buckets.map( + (dateBucket) => dateBucket.count.value ?? 0 + ) + ), + latency_sum: sum( + bucket.timeseries.buckets.map( + (dateBucket) => dateBucket.latency_sum.value ?? 0 + ) + ), + error_count: sum( + bucket.timeseries.buckets.flatMap( + (dateBucket) => + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0 + ) + ), + }, + timeseries: bucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + count: dateBucket.count.value ?? 0, + latency_sum: dateBucket.latency_sum.value ?? 0, + error_count: + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0, + })), + })) ?? [] + ); }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts index e45864de2fc1e5..bd69bfc53db71f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts @@ -18,7 +18,6 @@ import { rangeQuery, kqlQuery, } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -43,75 +42,71 @@ export async function getServiceErrorGroupDetailedStatistics({ start: number; end: number; }): Promise> { - return withApmSpan( - 'get_service_error_group_detailed_statistics', - async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const { intervalString } = getBucketSize({ start, end, numBuckets }); + const { intervalString } = getBucketSize({ start, end, numBuckets }); - const timeseriesResponse = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.error], + const timeseriesResponse = await apmEventClient.search( + 'get_service_error_group_detailed_statistics', + { + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: groupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, }, - body: { - size: 0, - query: { - bool: { - filter: [ - { terms: { [ERROR_GROUP_ID]: groupIds } }, - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, }, - }, - aggs: { - error_groups: { - terms: { - field: ERROR_GROUP_ID, - size: 500, - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, - }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, }, }, }, }, }, }, - }); - - if (!timeseriesResponse.aggregations) { - return []; - } - - return timeseriesResponse.aggregations.error_groups.buckets.map( - (bucket) => { - const groupId = bucket.key as string; - return { - groupId, - timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => { - return { - x: timeseriesBucket.key, - y: timeseriesBucket.doc_count, - }; - }), - }; - } - ); + }, } ); + + if (!timeseriesResponse.aggregations) { + return []; + } + + return timeseriesResponse.aggregations.error_groups.buckets.map((bucket) => { + const groupId = bucket.key as string; + return { + groupId, + timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => { + return { + x: timeseriesBucket.key, + y: timeseriesBucket.doc_count, + }; + }), + }; + }); } export async function getServiceErrorGroupPeriods({ diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts index 361c92244aee06..8168c0d5549aa4 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts @@ -19,11 +19,10 @@ import { rangeQuery, kqlQuery, } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getErrorName } from '../../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -export function getServiceErrorGroupMainStatistics({ +export async function getServiceErrorGroupMainStatistics({ kuery, serviceName, setup, @@ -36,10 +35,11 @@ export function getServiceErrorGroupMainStatistics({ transactionType: string; environment?: string; }) { - return withApmSpan('get_service_error_group_main_statistics', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_service_error_group_main_statistics', + { apm: { events: [ProcessorEvent.error], }, @@ -79,24 +79,23 @@ export function getServiceErrorGroupMainStatistics({ }, }, }, - }); + } + ); - const errorGroups = - response.aggregations?.error_groups.buckets.map((bucket) => ({ - group_id: bucket.key as string, - name: - getErrorName(bucket.sample.hits.hits[0]._source) ?? - NOT_AVAILABLE_LABEL, - last_seen: new Date( - bucket.sample.hits.hits[0]?._source['@timestamp'] - ).getTime(), - occurrences: bucket.doc_count, - })) ?? []; + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: bucket.doc_count, + })) ?? []; - return { - is_aggregation_accurate: - (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, - error_groups: errorGroups, - }; - }); + return { + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: errorGroups, + }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts index 7729822df30ca0..b720c56464c30f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -59,8 +59,9 @@ export async function getServiceErrorGroups({ const { intervalString } = getBucketSize({ start, end, numBuckets }); - const response = await withApmSpan('get_top_service_error_groups', () => - apmEventClient.search({ + const response = await apmEventClient.search( + 'get_top_service_error_groups', + { apm: { events: [ProcessorEvent.error], }, @@ -104,7 +105,7 @@ export async function getServiceErrorGroups({ }, }, }, - }) + } ); const errorGroups = @@ -139,50 +140,49 @@ export async function getServiceErrorGroups({ (group) => group.group_id ); - const timeseriesResponse = await withApmSpan( + const timeseriesResponse = await apmEventClient.search( 'get_service_error_groups_timeseries', - async () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.error], + { + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, }, - body: { - size: 0, - query: { - bool: { - filter: [ - { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size, }, - }, - aggs: { - error_groups: { - terms: { - field: ERROR_GROUP_ID, - size, - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, - }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, }, }, }, }, }, }, - }) + }, + } ); return { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts index 25935bcc37dff5..bdf9530a9c0c7e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instance_metadata_details.ts @@ -11,7 +11,6 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { environmentQuery, kqlQuery, rangeQuery } from '../../utils/queries'; -import { withApmSpan } from '../../utils/with_apm_span'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -37,18 +36,19 @@ export async function getServiceInstanceMetadataDetails({ environment?: string; kuery?: string; }) { - return withApmSpan('get_service_instance_metadata_details', async () => { - const { start, end, apmEventClient } = setup; - const filter = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [SERVICE_NODE_NAME]: serviceNodeName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; + const { start, end, apmEventClient } = setup; + const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [SERVICE_NODE_NAME]: serviceNodeName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_service_instance_metadata_details', + { apm: { events: [ getProcessorEventForAggregatedTransactions( @@ -61,24 +61,24 @@ export async function getServiceInstanceMetadataDetails({ size: 1, query: { bool: { filter } }, }, - }); + } + ); - const sample = response.hits.hits[0]?._source; + const sample = response.hits.hits[0]?._source; - if (!sample) { - return {}; - } + if (!sample) { + return {}; + } - const { agent, service, container, kubernetes, host, cloud } = sample; + const { agent, service, container, kubernetes, host, cloud } = sample; - return { - '@timestamp': sample['@timestamp'], - agent, - service, - container, - kubernetes, - host, - cloud, - }; - }); + return { + '@timestamp': sample['@timestamp'], + agent, + service, + container, + kubernetes, + host, + cloud, + }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts index 1a33e9810dd5e0..526ae19143f130 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts @@ -24,7 +24,6 @@ import { percentCgroupMemoryUsedScript, percentSystemMemoryUsedScript, } from '../../metrics/by_agent/shared/memory'; -import { withApmSpan } from '../../../utils/with_apm_span'; interface ServiceInstanceSystemMetricPrimaryStatistics { serviceNodeName: string; @@ -67,142 +66,140 @@ export async function getServiceInstancesSystemMetricStatistics< size?: number; isComparisonSearch: T; }): Promise>> { - return withApmSpan( - 'get_service_instances_system_metric_statistics', - async () => { - const { apmEventClient } = setup; - - const { intervalString } = getBucketSize({ start, end, numBuckets }); - - const systemMemoryFilter = { - bool: { - filter: [ - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ], - }, - }; - - const cgroupMemoryFilter = { - exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES }, - }; - - const cpuUsageFilter = { exists: { field: METRIC_PROCESS_CPU_PERCENT } }; - - function withTimeseries( - agg: TParams - ) { - return { - ...(isComparisonSearch - ? { - avg: { avg: agg }, - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, - }, - }, - aggs: { avg: { avg: agg } }, + const { apmEventClient } = setup; + + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const systemMemoryFilter = { + bool: { + filter: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + }, + }; + + const cgroupMemoryFilter = { + exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES }, + }; + + const cpuUsageFilter = { exists: { field: METRIC_PROCESS_CPU_PERCENT } }; + + function withTimeseries( + agg: TParams + ) { + return { + ...(isComparisonSearch + ? { + avg: { avg: agg }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, }, - } - : { avg: { avg: agg } }), - }; - } - - const subAggs = { - memory_usage_cgroup: { - filter: cgroupMemoryFilter, - aggs: withTimeseries({ script: percentCgroupMemoryUsedScript }), - }, - memory_usage_system: { - filter: systemMemoryFilter, - aggs: withTimeseries({ script: percentSystemMemoryUsedScript }), - }, - cpu_usage: { - filter: cpuUsageFilter, - aggs: withTimeseries({ field: METRIC_PROCESS_CPU_PERCENT }), - }, - }; - - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.metric], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ...(isComparisonSearch && serviceNodeIds - ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] - : []), - ], - should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter], - minimum_should_match: 1, + }, + aggs: { avg: { avg: agg } }, }, + } + : { avg: { avg: agg } }), + }; + } + + const subAggs = { + memory_usage_cgroup: { + filter: cgroupMemoryFilter, + aggs: withTimeseries({ script: percentCgroupMemoryUsedScript }), + }, + memory_usage_system: { + filter: systemMemoryFilter, + aggs: withTimeseries({ script: percentSystemMemoryUsedScript }), + }, + cpu_usage: { + filter: cpuUsageFilter, + aggs: withTimeseries({ field: METRIC_PROCESS_CPU_PERCENT }), + }, + }; + + const response = await apmEventClient.search( + 'get_service_instances_system_metric_statistics', + { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...(isComparisonSearch && serviceNodeIds + ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] + : []), + ], + should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter], + minimum_should_match: 1, }, - aggs: { - [SERVICE_NODE_NAME]: { - terms: { - field: SERVICE_NODE_NAME, - missing: SERVICE_NODE_NAME_MISSING, - ...(size ? { size } : {}), - ...(isComparisonSearch ? { include: serviceNodeIds } : {}), - }, - aggs: subAggs, + }, + aggs: { + [SERVICE_NODE_NAME]: { + terms: { + field: SERVICE_NODE_NAME, + missing: SERVICE_NODE_NAME_MISSING, + ...(size ? { size } : {}), + ...(isComparisonSearch ? { include: serviceNodeIds } : {}), }, + aggs: subAggs, }, }, - }); - - return ( - (response.aggregations?.[SERVICE_NODE_NAME].buckets.map( - (serviceNodeBucket) => { - const serviceNodeName = String(serviceNodeBucket.key); - const hasCGroupData = - serviceNodeBucket.memory_usage_cgroup.avg.value !== null; - - const memoryMetricsKey = hasCGroupData - ? 'memory_usage_cgroup' - : 'memory_usage_system'; - - const cpuUsage = - // Timeseries is available when isComparisonSearch is true - 'timeseries' in serviceNodeBucket.cpu_usage - ? serviceNodeBucket.cpu_usage.timeseries.buckets.map( - (dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.avg.value, - }) - ) - : serviceNodeBucket.cpu_usage.avg.value; - - const memoryUsageValue = serviceNodeBucket[memoryMetricsKey]; - const memoryUsage = - // Timeseries is available when isComparisonSearch is true - 'timeseries' in memoryUsageValue - ? memoryUsageValue.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.avg.value, - })) - : serviceNodeBucket[memoryMetricsKey].avg.value; - - return { - serviceNodeName, - cpuUsage, - memoryUsage, - }; - } - ) as Array>) || [] - ); + }, } ); + + return ( + (response.aggregations?.[SERVICE_NODE_NAME].buckets.map( + (serviceNodeBucket) => { + const serviceNodeName = String(serviceNodeBucket.key); + const hasCGroupData = + serviceNodeBucket.memory_usage_cgroup.avg.value !== null; + + const memoryMetricsKey = hasCGroupData + ? 'memory_usage_cgroup' + : 'memory_usage_system'; + + const cpuUsage = + // Timeseries is available when isComparisonSearch is true + 'timeseries' in serviceNodeBucket.cpu_usage + ? serviceNodeBucket.cpu_usage.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg.value, + }) + ) + : serviceNodeBucket.cpu_usage.avg.value; + + const memoryUsageValue = serviceNodeBucket[memoryMetricsKey]; + const memoryUsage = + // Timeseries is available when isComparisonSearch is true + 'timeseries' in memoryUsageValue + ? memoryUsageValue.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg.value, + })) + : serviceNodeBucket[memoryMetricsKey].avg.value; + + return { + serviceNodeName, + cpuUsage, + memoryUsage, + }; + } + ) as Array>) || [] + ); } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts index ad54a231b52ef2..7d9dca9b2a7061 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts @@ -26,7 +26,6 @@ import { getLatencyValue, } from '../../helpers/latency_aggregation_type'; import { Setup } from '../../helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; interface ServiceInstanceTransactionPrimaryStatistics { serviceNodeName: string; @@ -77,126 +76,124 @@ export async function getServiceInstancesTransactionStatistics< size?: number; numBuckets?: number; }): Promise>> { - return withApmSpan( - 'get_service_instances_transaction_statistics', - async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const { intervalString, bucketSize } = getBucketSize({ - start, - end, - numBuckets, - }); + const { intervalString, bucketSize } = getBucketSize({ + start, + end, + numBuckets, + }); - const field = getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ); + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); - const subAggs = { - ...getLatencyAggregation(latencyAggregationType, field), - failures: { - filter: { - term: { - [EVENT_OUTCOME]: EventOutcome.failure, - }, - }, + const subAggs = { + ...getLatencyAggregation(latencyAggregationType, field), + failures: { + filter: { + term: { + [EVENT_OUTCOME]: EventOutcome.failure, }, - }; + }, + }, + }; - const query = { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ...(isComparisonSearch && serviceNodeIds - ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] - : []), - ], - }, - }; + const query = { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...(isComparisonSearch && serviceNodeIds + ? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }] + : []), + ], + }, + }; - const aggs = { - [SERVICE_NODE_NAME]: { - terms: { - field: SERVICE_NODE_NAME, - missing: SERVICE_NODE_NAME_MISSING, - ...(size ? { size } : {}), - ...(isComparisonSearch ? { include: serviceNodeIds } : {}), - }, - aggs: isComparisonSearch - ? { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, - aggs: subAggs, - }, - } - : subAggs, - }, - }; + const aggs = { + [SERVICE_NODE_NAME]: { + terms: { + field: SERVICE_NODE_NAME, + missing: SERVICE_NODE_NAME_MISSING, + ...(size ? { size } : {}), + ...(isComparisonSearch ? { include: serviceNodeIds } : {}), + }, + aggs: isComparisonSearch + ? { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: subAggs, + }, + } + : subAggs, + }, + }; - const response = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { size: 0, query, aggs }, - }); + const response = await apmEventClient.search( + 'get_service_instances_transaction_statistics', + { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { size: 0, query, aggs }, + } + ); - const bucketSizeInMinutes = bucketSize / 60; + const bucketSizeInMinutes = bucketSize / 60; - return ( - (response.aggregations?.[SERVICE_NODE_NAME].buckets.map( - (serviceNodeBucket) => { - const { doc_count: count, key } = serviceNodeBucket; - const serviceNodeName = String(key); + return ( + (response.aggregations?.[SERVICE_NODE_NAME].buckets.map( + (serviceNodeBucket) => { + const { doc_count: count, key } = serviceNodeBucket; + const serviceNodeName = String(key); - // Timeseries is returned when isComparisonSearch is true - if ('timeseries' in serviceNodeBucket) { - const { timeseries } = serviceNodeBucket; - return { - serviceNodeName, - errorRate: timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.failures.doc_count / dateBucket.doc_count, - })), - throughput: timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.doc_count / bucketSizeInMinutes, - })), - latency: timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: getLatencyValue({ - aggregation: dateBucket.latency, - latencyAggregationType, - }), - })), - }; - } else { - const { failures, latency } = serviceNodeBucket; - return { - serviceNodeName, - errorRate: failures.doc_count / count, - latency: getLatencyValue({ - aggregation: latency, - latencyAggregationType, - }), - throughput: calculateThroughput({ start, end, value: count }), - }; - } - } - ) as Array>) || [] - ); - } + // Timeseries is returned when isComparisonSearch is true + if ('timeseries' in serviceNodeBucket) { + const { timeseries } = serviceNodeBucket; + return { + serviceNodeName, + errorRate: timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.failures.doc_count / dateBucket.doc_count, + })), + throughput: timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count / bucketSizeInMinutes, + })), + latency: timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: getLatencyValue({ + aggregation: dateBucket.latency, + latencyAggregationType, + }), + })), + }; + } else { + const { failures, latency } = serviceNodeBucket; + return { + serviceNodeName, + errorRate: failures.doc_count / count, + latency: getLatencyValue({ + aggregation: latency, + latencyAggregationType, + }), + throughput: calculateThroughput({ start, end, value: count }), + }; + } + } + ) as Array>) || [] ); } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts index e2341b306a8781..910725b0054113 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts @@ -25,7 +25,6 @@ import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw' import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { should } from './get_service_metadata_icons'; -import { withApmSpan } from '../../utils/with_apm_span'; type ServiceMetadataDetailsRaw = Pick< TransactionRaw, @@ -59,7 +58,7 @@ export interface ServiceMetadataDetails { }; } -export function getServiceMetadataDetails({ +export async function getServiceMetadataDetails({ serviceName, setup, searchAggregatedTransactions, @@ -68,105 +67,106 @@ export function getServiceMetadataDetails({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }): Promise { - return withApmSpan('get_service_metadata_details', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const filter = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ]; + const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ]; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - size: 1, - _source: [SERVICE, AGENT, HOST, CONTAINER_ID, KUBERNETES, CLOUD], - query: { bool: { filter, should } }, - aggs: { - serviceVersions: { - terms: { - field: SERVICE_VERSION, - size: 10, - order: { _key: 'desc' as const }, - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + size: 1, + _source: [SERVICE, AGENT, HOST, CONTAINER_ID, KUBERNETES, CLOUD], + query: { bool: { filter, should } }, + aggs: { + serviceVersions: { + terms: { + field: SERVICE_VERSION, + size: 10, + order: { _key: 'desc' as const }, }, - availabilityZones: { - terms: { - field: CLOUD_AVAILABILITY_ZONE, - size: 10, - }, + }, + availabilityZones: { + terms: { + field: CLOUD_AVAILABILITY_ZONE, + size: 10, }, - machineTypes: { - terms: { - field: CLOUD_MACHINE_TYPE, - size: 10, - }, + }, + machineTypes: { + terms: { + field: CLOUD_MACHINE_TYPE, + size: 10, }, - totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } }, }, + totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } }, }, - }; - - const response = await apmEventClient.search(params); - - if (response.hits.total.value === 0) { - return { - service: undefined, - container: undefined, - cloud: undefined, - }; - } + }, + }; - const { service, agent, host, kubernetes, container, cloud } = response.hits - .hits[0]._source as ServiceMetadataDetailsRaw; + const response = await apmEventClient.search( + 'get_service_metadata_details', + params + ); - const serviceMetadataDetails = { - versions: response.aggregations?.serviceVersions.buckets.map( - (bucket) => bucket.key as string - ), - runtime: service.runtime, - framework: service.framework?.name, - agent, + if (response.hits.total.value === 0) { + return { + service: undefined, + container: undefined, + cloud: undefined, }; + } + + const { service, agent, host, kubernetes, container, cloud } = response.hits + .hits[0]._source as ServiceMetadataDetailsRaw; - const totalNumberInstances = - response.aggregations?.totalNumberInstances.value; + const serviceMetadataDetails = { + versions: response.aggregations?.serviceVersions.buckets.map( + (bucket) => bucket.key as string + ), + runtime: service.runtime, + framework: service.framework?.name, + agent, + }; - const containerDetails = - host || container || totalNumberInstances || kubernetes - ? { - os: host?.os?.platform, - type: (!!kubernetes ? 'Kubernetes' : 'Docker') as ContainerType, - isContainerized: !!container?.id, - totalNumberInstances, - } - : undefined; + const totalNumberInstances = + response.aggregations?.totalNumberInstances.value; - const cloudDetails = cloud + const containerDetails = + host || container || totalNumberInstances || kubernetes ? { - provider: cloud.provider, - projectName: cloud.project?.name, - availabilityZones: response.aggregations?.availabilityZones.buckets.map( - (bucket) => bucket.key as string - ), - machineTypes: response.aggregations?.machineTypes.buckets.map( - (bucket) => bucket.key as string - ), + os: host?.os?.platform, + type: (!!kubernetes ? 'Kubernetes' : 'Docker') as ContainerType, + isContainerized: !!container?.id, + totalNumberInstances, } : undefined; - return { - service: serviceMetadataDetails, - container: containerDetails, - cloud: cloudDetails, - }; - }); + const cloudDetails = cloud + ? { + provider: cloud.provider, + projectName: cloud.project?.name, + availabilityZones: response.aggregations?.availabilityZones.buckets.map( + (bucket) => bucket.key as string + ), + machineTypes: response.aggregations?.machineTypes.buckets.map( + (bucket) => bucket.key as string + ), + } + : undefined; + + return { + service: serviceMetadataDetails, + container: containerDetails, + cloud: cloudDetails, + }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts index 94da6545c5e90e..469c788a6cf17a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts @@ -20,7 +20,6 @@ import { rangeQuery } from '../../../server/utils/queries'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { withApmSpan } from '../../utils/with_apm_span'; type ServiceMetadataIconsRaw = Pick< TransactionRaw, @@ -41,7 +40,7 @@ export const should = [ { exists: { field: AGENT_NAME } }, ]; -export function getServiceMetadataIcons({ +export async function getServiceMetadataIcons({ serviceName, setup, searchAggregatedTransactions, @@ -50,55 +49,56 @@ export function getServiceMetadataIcons({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }): Promise { - return withApmSpan('get_service_metadata_icons', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const filter = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ]; + const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ]; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - size: 1, - _source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME], - query: { bool: { filter, should } }, - }, - }; + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + size: 1, + _source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME], + query: { bool: { filter, should } }, + }, + }; - const response = await apmEventClient.search(params); + const response = await apmEventClient.search( + 'get_service_metadata_icons', + params + ); - if (response.hits.total.value === 0) { - return { - agentName: undefined, - containerType: undefined, - cloudProvider: undefined, - }; - } + if (response.hits.total.value === 0) { + return { + agentName: undefined, + containerType: undefined, + cloudProvider: undefined, + }; + } - const { kubernetes, cloud, container, agent } = response.hits.hits[0] - ._source as ServiceMetadataIconsRaw; + const { kubernetes, cloud, container, agent } = response.hits.hits[0] + ._source as ServiceMetadataIconsRaw; - let containerType: ContainerType; - if (!!kubernetes) { - containerType = 'Kubernetes'; - } else if (!!container) { - containerType = 'Docker'; - } + let containerType: ContainerType; + if (!!kubernetes) { + containerType = 'Kubernetes'; + } else if (!!container) { + containerType = 'Docker'; + } - return { - agentName: agent?.name, - containerType, - cloudProvider: cloud?.provider, - }; - }); + return { + agentName: agent?.name, + containerType, + cloudProvider: cloud?.provider, + }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index 8eaf9e96c7fd97..0f0c1741790526 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -13,9 +13,8 @@ import { import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; import { mergeProjection } from '../../projections/util/merge_projection'; import { getServiceNodesProjection } from '../../projections/service_nodes'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getServiceNodeMetadata({ +export async function getServiceNodeMetadata({ kuery, serviceName, serviceNodeName, @@ -26,44 +25,44 @@ export function getServiceNodeMetadata({ serviceNodeName: string; setup: Setup & SetupTimeRange; }) { - return withApmSpan('get_service_node_metadata', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const query = mergeProjection( - getServiceNodesProjection({ - kuery, - setup, - serviceName, - serviceNodeName, - }), - { - body: { - size: 0, - aggs: { - host: { - terms: { - field: HOST_NAME, - size: 1, - }, + const query = mergeProjection( + getServiceNodesProjection({ + kuery, + setup, + serviceName, + serviceNodeName, + }), + { + body: { + size: 0, + aggs: { + host: { + terms: { + field: HOST_NAME, + size: 1, }, - containerId: { - terms: { - field: CONTAINER_ID, - size: 1, - }, + }, + containerId: { + terms: { + field: CONTAINER_ID, + size: 1, }, }, }, - } - ); + }, + } + ); - const response = await apmEventClient.search(query); + const response = await apmEventClient.search( + 'get_service_node_metadata', + query + ); - return { - host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, - containerId: - response.aggregations?.containerId.buckets[0]?.key || - NOT_AVAILABLE_LABEL, - }; - }); + return { + host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, + containerId: + response.aggregations?.containerId.buckets[0]?.key || NOT_AVAILABLE_LABEL, + }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts index f14dba69bf404b..36d372e322cbc4 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts @@ -21,7 +21,6 @@ import { kqlQuery, } from '../../../server/utils/queries'; import { Coordinate } from '../../../typings/timeseries'; -import { withApmSpan } from '../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -68,74 +67,72 @@ export async function getServiceTransactionGroupDetailedStatistics({ impact: number; }> > { - return withApmSpan( - 'get_service_transaction_group_detailed_statistics', - async () => { - const { apmEventClient } = setup; - const { intervalString } = getBucketSize({ start, end, numBuckets }); + const { apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets }); - const field = getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ); + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); - const response = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], + const response = await apmEventClient.search( + 'get_service_transaction_group_detailed_statistics', + { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], + aggs: { + total_duration: { sum: { field } }, + transaction_groups: { + terms: { + field: TRANSACTION_NAME, + include: transactionNames, + size: transactionNames.length, }, - }, - aggs: { - total_duration: { sum: { field } }, - transaction_groups: { - terms: { - field: TRANSACTION_NAME, - include: transactionNames, - size: transactionNames.length, + aggs: { + transaction_group_total_duration: { + sum: { field }, }, - aggs: { - transaction_group_total_duration: { - sum: { field }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, }, - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, + aggs: { + throughput_rate: { + rate: { + unit: 'minute', }, }, - aggs: { - throughput_rate: { - rate: { - unit: 'minute', - }, - }, - ...getLatencyAggregation(latencyAggregationType, field), - [EVENT_OUTCOME]: { - terms: { - field: EVENT_OUTCOME, - include: [EventOutcome.failure, EventOutcome.success], - }, + ...getLatencyAggregation(latencyAggregationType, field), + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + include: [EventOutcome.failure, EventOutcome.success], }, }, }, @@ -143,46 +140,42 @@ export async function getServiceTransactionGroupDetailedStatistics({ }, }, }, - }); - - const buckets = response.aggregations?.transaction_groups.buckets ?? []; - - const totalDuration = response.aggregations?.total_duration.value; - return buckets.map((bucket) => { - const transactionName = bucket.key as string; - const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({ - x: timeseriesBucket.key, - y: getLatencyValue({ - latencyAggregationType, - aggregation: timeseriesBucket.latency, - }), - })); - const throughput = bucket.timeseries.buckets.map( - (timeseriesBucket) => ({ - x: timeseriesBucket.key, - y: timeseriesBucket.throughput_rate.value, - }) - ); - const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({ - x: timeseriesBucket.key, - y: calculateTransactionErrorPercentage( - timeseriesBucket[EVENT_OUTCOME] - ), - })); - const transactionGroupTotalDuration = - bucket.transaction_group_total_duration.value || 0; - return { - transactionName, - latency, - throughput, - errorRate, - impact: totalDuration - ? (transactionGroupTotalDuration * 100) / totalDuration - : 0, - }; - }); + }, } ); + + const buckets = response.aggregations?.transaction_groups.buckets ?? []; + + const totalDuration = response.aggregations?.total_duration.value; + return buckets.map((bucket) => { + const transactionName = bucket.key as string; + const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: getLatencyValue({ + latencyAggregationType, + aggregation: timeseriesBucket.latency, + }), + })); + const throughput = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: timeseriesBucket.throughput_rate.value, + })); + const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({ + x: timeseriesBucket.key, + y: calculateTransactionErrorPercentage(timeseriesBucket[EVENT_OUTCOME]), + })); + const transactionGroupTotalDuration = + bucket.transaction_group_total_duration.value || 0; + return { + transactionName, + latency, + throughput, + errorRate, + impact: totalDuration + ? (transactionGroupTotalDuration * 100) / totalDuration + : 0, + }; + }); } export async function getServiceTransactionGroupDetailedStatisticsPeriods({ diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts index 28574bab4df213..a4cc27c875d731 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts @@ -18,7 +18,6 @@ import { rangeQuery, kqlQuery, } from '../../../server/utils/queries'; -import { withApmSpan } from '../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -56,14 +55,15 @@ export async function getServiceTransactionGroups({ transactionType: string; latencyAggregationType: LatencyAggregationType; }) { - return withApmSpan('get_service_transaction_groups', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const field = getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ); + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_service_transaction_groups', + { apm: { events: [ getProcessorEventForAggregatedTransactions( @@ -110,45 +110,45 @@ export async function getServiceTransactionGroups({ }, }, }, - }); + } + ); - const totalDuration = response.aggregations?.total_duration.value; + const totalDuration = response.aggregations?.total_duration.value; - const transactionGroups = - response.aggregations?.transaction_groups.buckets.map((bucket) => { - const errorRate = calculateTransactionErrorPercentage( - bucket[EVENT_OUTCOME] - ); + const transactionGroups = + response.aggregations?.transaction_groups.buckets.map((bucket) => { + const errorRate = calculateTransactionErrorPercentage( + bucket[EVENT_OUTCOME] + ); - const transactionGroupTotalDuration = - bucket.transaction_group_total_duration.value || 0; + const transactionGroupTotalDuration = + bucket.transaction_group_total_duration.value || 0; - return { - name: bucket.key as string, - latency: getLatencyValue({ - latencyAggregationType, - aggregation: bucket.latency, - }), - throughput: calculateThroughput({ - start, - end, - value: bucket.doc_count, - }), - errorRate, - impact: totalDuration - ? (transactionGroupTotalDuration * 100) / totalDuration - : 0, - }; - }) ?? []; + return { + name: bucket.key as string, + latency: getLatencyValue({ + latencyAggregationType, + aggregation: bucket.latency, + }), + throughput: calculateThroughput({ + start, + end, + value: bucket.doc_count, + }), + errorRate, + impact: totalDuration + ? (transactionGroupTotalDuration * 100) / totalDuration + : 0, + }; + }) ?? []; - return { - transactionGroups: transactionGroups.map((transactionGroup) => ({ - ...transactionGroup, - transactionType, - })), - isAggregationAccurate: - (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) === - 0, - }; - }); + return { + transactionGroups: transactionGroups.map((transactionGroup) => ({ + ...transactionGroup, + transactionType, + })), + isAggregationAccurate: + (response.aggregations?.transaction_groups.sum_other_doc_count ?? 0) === + 0, + }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index e280ab6db1665d..f38a7fba09d967 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -15,9 +15,8 @@ import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, } from '../helpers/aggregated_transactions'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getServiceTransactionTypes({ +export async function getServiceTransactionTypes({ setup, serviceName, searchAggregatedTransactions, @@ -26,41 +25,42 @@ export function getServiceTransactionTypes({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { - return withApmSpan('get_service_transaction_types', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ], - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ], }, - aggs: { - types: { - terms: { field: TRANSACTION_TYPE, size: 100 }, - }, + }, + aggs: { + types: { + terms: { field: TRANSACTION_TYPE, size: 100 }, }, }, - }; + }, + }; - const { aggregations } = await apmEventClient.search(params); - const transactionTypes = - aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; - return { transactionTypes }; - }); + const { aggregations } = await apmEventClient.search( + 'get_service_transaction_types', + params + ); + const transactionTypes = + aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; + return { transactionTypes }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts index b42fd340bfb422..f33bedb6ef4fb1 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts @@ -9,35 +9,31 @@ import { rangeQuery } from '../../../../server/utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; // returns true if 6.x data is found export async function getLegacyDataStatus(setup: Setup & SetupTimeRange) { - return withApmSpan('get_legacy_data_status', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const params = { - terminateAfter: 1, - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, - ...rangeQuery(start, end), - ], - }, + const params = { + terminateAfter: 1, + apm: { + events: [ProcessorEvent.transaction], + includeLegacyData: true, + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, + ...rangeQuery(start, end), + ], }, }, - }; + }, + }; - const resp = await apmEventClient.search(params, { - includeLegacyData: true, - }); - const hasLegacyData = resp.hits.total.value > 0; - return hasLegacyData; - }); + const resp = await apmEventClient.search('get_legacy_data_status', params); + const hasLegacyData = resp.hits.total.value > 0; + return hasLegacyData; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 1e36df379e9646..019ab8770887ae 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -33,7 +33,6 @@ import { getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; import { ServicesItemsSetup } from './get_services_items'; -import { withApmSpan } from '../../../utils/with_apm_span'; interface AggregationParams { environment?: string; @@ -50,23 +49,24 @@ export async function getServiceTransactionStats({ searchAggregatedTransactions, maxNumServices, }: AggregationParams) { - return withApmSpan('get_service_transaction_stats', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const outcomes = getOutcomeAggregation(); + const outcomes = getOutcomeAggregation(); - const metrics = { - avg_duration: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, + const metrics = { + avg_duration: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, - outcomes, - }; + }, + outcomes, + }; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_service_transaction_stats', + { apm: { events: [ getProcessorEventForAggregatedTransactions( @@ -133,64 +133,64 @@ export async function getServiceTransactionStats({ }, }, }, - }); + } + ); - return ( - response.aggregations?.services.buckets.map((bucket) => { - const topTransactionTypeBucket = - bucket.transactionType.buckets.find( - ({ key }) => - key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD - ) ?? bucket.transactionType.buckets[0]; + return ( + response.aggregations?.services.buckets.map((bucket) => { + const topTransactionTypeBucket = + bucket.transactionType.buckets.find( + ({ key }) => + key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD + ) ?? bucket.transactionType.buckets[0]; - return { - serviceName: bucket.key as string, - transactionType: topTransactionTypeBucket.key as string, - environments: topTransactionTypeBucket.environments.buckets.map( - (environmentBucket) => environmentBucket.key as string + return { + serviceName: bucket.key as string, + transactionType: topTransactionTypeBucket.key as string, + environments: topTransactionTypeBucket.environments.buckets.map( + (environmentBucket) => environmentBucket.key as string + ), + agentName: topTransactionTypeBucket.sample.top[0].metrics[ + AGENT_NAME + ] as AgentName, + avgResponseTime: { + value: topTransactionTypeBucket.avg_duration.value, + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.avg_duration.value, + }) ), - agentName: topTransactionTypeBucket.sample.top[0].metrics[ - AGENT_NAME - ] as AgentName, - avgResponseTime: { - value: topTransactionTypeBucket.avg_duration.value, - timeseries: topTransactionTypeBucket.timeseries.buckets.map( - (dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.avg_duration.value, - }) - ), - }, - transactionErrorRate: { - value: calculateTransactionErrorPercentage( - topTransactionTypeBucket.outcomes - ), - timeseries: topTransactionTypeBucket.timeseries.buckets.map( - (dateBucket) => ({ - x: dateBucket.key, - y: calculateTransactionErrorPercentage(dateBucket.outcomes), - }) - ), - }, - transactionsPerMinute: { - value: calculateThroughput({ - start, - end, - value: topTransactionTypeBucket.doc_count, - }), - timeseries: topTransactionTypeBucket.timeseries.buckets.map( - (dateBucket) => ({ - x: dateBucket.key, - y: calculateThroughput({ - start, - end, - value: dateBucket.doc_count, - }), - }) - ), - }, - }; - }) ?? [] - ); - }); + }, + transactionErrorRate: { + value: calculateTransactionErrorPercentage( + topTransactionTypeBucket.outcomes + ), + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: calculateTransactionErrorPercentage(dateBucket.outcomes), + }) + ), + }, + transactionsPerMinute: { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket.doc_count, + }), + timeseries: topTransactionTypeBucket.timeseries.buckets.map( + (dateBucket) => ({ + x: dateBucket.key, + y: calculateThroughput({ + start, + end, + value: dateBucket.doc_count, + }), + }) + ), + }, + }; + }) ?? [] + ); } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts index 906cc62e64d1a4..4692d1122b16c9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts @@ -14,9 +14,8 @@ import { import { environmentQuery, kqlQuery, rangeQuery } from '../../../utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export function getServicesFromMetricDocuments({ +export async function getServicesFromMetricDocuments({ environment, setup, maxNumServices, @@ -27,10 +26,11 @@ export function getServicesFromMetricDocuments({ maxNumServices: number; kuery?: string; }) { - return withApmSpan('get_services_from_metric_documents', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_services_from_metric_documents', + { apm: { events: [ProcessorEvent.metric], }, @@ -67,18 +67,18 @@ export function getServicesFromMetricDocuments({ }, }, }, - }); + } + ); - return ( - response.aggregations?.services.buckets.map((bucket) => { - return { - serviceName: bucket.key as string, - environments: bucket.environments.buckets.map( - (envBucket) => envBucket.key as string - ), - agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName, - }; - }) ?? [] - ); - }); + return ( + response.aggregations?.services.buckets.map((bucket) => { + return { + serviceName: bucket.key as string, + environments: bucket.environments.buckets.map( + (envBucket) => envBucket.key as string + ), + agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName, + }; + }) ?? [] + ); } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts index 28f6944fd24daf..97b8a8fa5505b0 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts @@ -6,29 +6,26 @@ */ import { ProcessorEvent } from '../../../../common/processor_event'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; // Note: this logic is duplicated in tutorials/apm/envs/on_prem export async function hasHistoricalAgentData(setup: Setup) { - return withApmSpan('has_historical_agent_data', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const params = { - terminateAfter: 1, - apm: { - events: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ], - }, - body: { - size: 0, - }, - }; + const params = { + terminateAfter: 1, + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.transaction, + ], + }, + body: { + size: 0, + }, + }; - const resp = await apmEventClient.search(params); - return resp.hits.total.value > 0; - }); + const resp = await apmEventClient.search('has_historical_agent_data', params); + return resp.hits.total.value > 0; } diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index 5f5008a28c2325..b0cb917d302fc5 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -21,7 +21,6 @@ import { } from '../helpers/aggregated_transactions'; import { getBucketSize } from '../helpers/get_bucket_size'; import { Setup } from '../helpers/setup_request'; -import { withApmSpan } from '../../utils/with_apm_span'; interface Options { environment?: string; @@ -88,20 +87,18 @@ function fetcher({ }, }; - return apmEventClient.search(params); + return apmEventClient.search('get_throughput_for_service', params); } -export function getThroughput(options: Options) { - return withApmSpan('get_throughput_for_service', async () => { - const response = await fetcher(options); +export async function getThroughput(options: Options) { + const response = await fetcher(options); - return ( - response.aggregations?.timeseries.buckets.map((bucket) => { - return { - x: bucket.key, - y: bucket.throughput.value, - }; - }) ?? [] - ); - }); + return ( + response.aggregations?.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + y: bucket.throughput.value, + }; + }) ?? [] + ); } diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 858f36e6e2c13e..bb98abf724db4c 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -41,7 +41,7 @@ const maybeAdd = (to: any[], value: any) => { to.push(value); }; -function getProfilingStats({ +async function getProfilingStats({ apmEventClient, filter, valueTypeField, @@ -50,49 +50,47 @@ function getProfilingStats({ filter: ESFilter[]; valueTypeField: string; }) { - return withApmSpan('get_profiling_stats', async () => { - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.profile], - }, - body: { - size: 0, - query: { - bool: { - filter, - }, + const response = await apmEventClient.search('get_profiling_stats', { + apm: { + events: [ProcessorEvent.profile], + }, + body: { + size: 0, + query: { + bool: { + filter, }, - aggs: { - stacks: { - terms: { - field: PROFILE_TOP_ID, - size: MAX_STACK_IDS, - order: { - value: 'desc', - }, + }, + aggs: { + stacks: { + terms: { + field: PROFILE_TOP_ID, + size: MAX_STACK_IDS, + order: { + value: 'desc', }, - aggs: { - value: { - sum: { - field: valueTypeField, - }, + }, + aggs: { + value: { + sum: { + field: valueTypeField, }, }, }, }, }, - }); + }, + }); - const stacks = - response.aggregations?.stacks.buckets.map((stack) => { - return { - id: stack.key as string, - value: stack.value.value!, - }; - }) ?? []; + const stacks = + response.aggregations?.stacks.buckets.map((stack) => { + return { + id: stack.key as string, + value: stack.value.value!, + }; + }) ?? []; - return stacks; - }); + return stacks; } function getProfilesWithStacks({ @@ -103,8 +101,9 @@ function getProfilesWithStacks({ filter: ESFilter[]; }) { return withApmSpan('get_profiles_with_stacks', async () => { - const cardinalityResponse = await withApmSpan('get_top_cardinality', () => - apmEventClient.search({ + const cardinalityResponse = await apmEventClient.search( + 'get_top_cardinality', + { apm: { events: [ProcessorEvent.profile], }, @@ -121,7 +120,7 @@ function getProfilesWithStacks({ }, }, }, - }) + } ); const cardinality = cardinalityResponse.aggregations?.top.value ?? 0; @@ -140,39 +139,37 @@ function getProfilesWithStacks({ const allResponses = await withApmSpan('get_all_stacks', async () => { return Promise.all( [...new Array(partitions)].map(async (_, num) => { - const response = await withApmSpan('get_partition', () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.profile], - }, - body: { - query: { - bool: { - filter, - }, + const response = await apmEventClient.search('get_partition', { + apm: { + events: [ProcessorEvent.profile], + }, + body: { + query: { + bool: { + filter, }, - aggs: { - top: { - terms: { - field: PROFILE_TOP_ID, - size: Math.max(MAX_STACKS_PER_REQUEST), - include: { - num_partitions: partitions, - partition: num, - }, + }, + aggs: { + top: { + terms: { + field: PROFILE_TOP_ID, + size: Math.max(MAX_STACKS_PER_REQUEST), + include: { + num_partitions: partitions, + partition: num, }, - aggs: { - latest: { - top_hits: { - _source: [PROFILE_TOP_ID, PROFILE_STACK], - }, + }, + aggs: { + latest: { + top_hits: { + _source: [PROFILE_TOP_ID, PROFILE_STACK], }, }, }, }, }, - }) - ); + }, + }); return ( response.aggregations?.top.buckets.flatMap((bucket) => { diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts index 93fa029da8c721..af3cd6596a8c1b 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -17,7 +17,6 @@ import { } from '../../../../common/profiling'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { kqlQuery } from '../../../utils/queries'; const configMap = mapValues( @@ -38,10 +37,11 @@ export async function getServiceProfilingTimeline({ setup: Setup & SetupTimeRange; environment?: string; }) { - return withApmSpan('get_service_profiling_timeline', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient, start, end } = setup; - const response = await apmEventClient.search({ + const response = await apmEventClient.search( + 'get_service_profiling_timeline', + { apm: { events: [ProcessorEvent.profile], }, @@ -96,29 +96,29 @@ export async function getServiceProfilingTimeline({ }, }, }, - }); + } + ); - const { aggregations } = response; + const { aggregations } = response; - if (!aggregations) { - return []; - } + if (!aggregations) { + return []; + } - return aggregations.timeseries.buckets.map((bucket) => { - return { - x: bucket.key, - valueTypes: { - unknown: bucket.value_type.buckets.unknown.num_profiles.value, - // TODO: use enum as object key. not possible right now - // because of https://github.com/microsoft/TypeScript/issues/37888 - ...mapValues(configMap, (_, key) => { - return ( - bucket.value_type.buckets[key as ProfilingValueType]?.num_profiles - .value ?? 0 - ); - }), - }, - }; - }); + return aggregations.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + valueTypes: { + unknown: bucket.value_type.buckets.unknown.num_profiles.value, + // TODO: use enum as object key. not possible right now + // because of https://github.com/microsoft/TypeScript/issues/37888 + ...mapValues(configMap, (_, key) => { + return ( + bucket.value_type.buckets[key as ProfilingValueType]?.num_profiles + .value ?? 0 + ); + }), + }, + }; }); } diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index b167eff65ee0aa..6adaca9c1a93dc 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -55,7 +55,7 @@ describe('services queries', () => { }) ); - const allParams = mock.spy.mock.calls.map((call) => call[0]); + const allParams = mock.spy.mock.calls.map((call) => call[1]); expect(allParams).toMatchSnapshot(); }); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 18853824355622..c112c3be3362b3 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -12,7 +12,6 @@ import { AgentConfigurationIntake, } from '../../../../common/agent_configuration/configuration_types'; import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; -import { withApmSpan } from '../../../utils/with_apm_span'; export function createOrUpdateConfiguration({ configurationId, @@ -23,30 +22,28 @@ export function createOrUpdateConfiguration({ configurationIntake: AgentConfigurationIntake; setup: Setup; }) { - return withApmSpan('create_or_update_configuration', async () => { - const { internalClient, indices } = setup; + const { internalClient, indices } = setup; - const params: APMIndexDocumentParams = { - refresh: true, - index: indices.apmAgentConfigurationIndex, - body: { - agent_name: configurationIntake.agent_name, - service: { - name: configurationIntake.service.name, - environment: configurationIntake.service.environment, - }, - settings: configurationIntake.settings, - '@timestamp': Date.now(), - applied_by_agent: false, - etag: hash(configurationIntake), + const params: APMIndexDocumentParams = { + refresh: true, + index: indices.apmAgentConfigurationIndex, + body: { + agent_name: configurationIntake.agent_name, + service: { + name: configurationIntake.service.name, + environment: configurationIntake.service.environment, }, - }; + settings: configurationIntake.settings, + '@timestamp': Date.now(), + applied_by_agent: false, + etag: hash(configurationIntake), + }, + }; - // by specifying an id elasticsearch will delete the previous doc and insert the updated doc - if (configurationId) { - params.id = configurationId; - } + // by specifying an id elasticsearch will delete the previous doc and insert the updated doc + if (configurationId) { + params.id = configurationId; + } - return internalClient.index(params); - }); + return internalClient.index('create_or_update_agent_configuration', params); } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts index 6ed6f79979889b..125c97730a6fa1 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; export async function deleteConfiguration({ @@ -15,15 +14,13 @@ export async function deleteConfiguration({ configurationId: string; setup: Setup; }) { - return withApmSpan('delete_agent_configuration', async () => { - const { internalClient, indices } = setup; + const { internalClient, indices } = setup; - const params = { - refresh: 'wait_for' as const, - index: indices.apmAgentConfigurationIndex, - id: configurationId, - }; + const params = { + refresh: 'wait_for' as const, + index: indices.apmAgentConfigurationIndex, + id: configurationId, + }; - return internalClient.delete(params); - }); + return internalClient.delete('delete_agent_configuration', params); } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 9fd4849c7640a8..3543d38f7b5d1c 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -11,47 +11,45 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; import { convertConfigSettingsToString } from './convert_settings_to_string'; -export function findExactConfiguration({ +export async function findExactConfiguration({ service, setup, }: { service: AgentConfiguration['service']; setup: Setup; }) { - return withApmSpan('find_exact_agent_configuration', async () => { - const { internalClient, indices } = setup; - - const serviceNameFilter = service.name - ? { term: { [SERVICE_NAME]: service.name } } - : { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }; - - const environmentFilter = service.environment - ? { term: { [SERVICE_ENVIRONMENT]: service.environment } } - : { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } }; - - const params = { - index: indices.apmAgentConfigurationIndex, - body: { - query: { - bool: { filter: [serviceNameFilter, environmentFilter] }, - }, + const { internalClient, indices } = setup; + + const serviceNameFilter = service.name + ? { term: { [SERVICE_NAME]: service.name } } + : { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }; + + const environmentFilter = service.environment + ? { term: { [SERVICE_ENVIRONMENT]: service.environment } } + : { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } }; + + const params = { + index: indices.apmAgentConfigurationIndex, + body: { + query: { + bool: { filter: [serviceNameFilter, environmentFilter] }, }, - }; + }, + }; - const resp = await internalClient.search( - params - ); + const resp = await internalClient.search( + 'find_exact_agent_configuration', + params + ); - const hit = resp.hits.hits[0] as SearchHit | undefined; + const hit = resp.hits.hits[0] as SearchHit | undefined; - if (!hit) { - return; - } + if (!hit) { + return; + } - return convertConfigSettingsToString(hit); - }); + return convertConfigSettingsToString(hit); } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index 379ed12e373895..0b6dd10b42e254 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -9,7 +9,6 @@ import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { withApmSpan } from '../../../utils/with_apm_span'; export async function getAgentNameByService({ serviceName, @@ -18,35 +17,36 @@ export async function getAgentNameByService({ serviceName: string; setup: Setup; }) { - return withApmSpan('get_agent_name_by_service', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const params = { - terminateAfter: 1, - apm: { - events: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [{ term: { [SERVICE_NAME]: serviceName } }], - }, + const params = { + terminateAfter: 1, + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [{ term: { [SERVICE_NAME]: serviceName } }], }, - aggs: { - agent_names: { - terms: { field: AGENT_NAME, size: 1 }, - }, + }, + aggs: { + agent_names: { + terms: { field: AGENT_NAME, size: 1 }, }, }, - }; + }, + }; - const { aggregations } = await apmEventClient.search(params); - const agentName = aggregations?.agent_names.buckets[0]?.key; - return agentName as string | undefined; - }); + const { aggregations } = await apmEventClient.search( + 'get_agent_name_by_service', + params + ); + const agentName = aggregations?.agent_names.buckets[0]?.key; + return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index 4a32b3c3a370bd..124a373d3cf070 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { withApmSpan } from '../../../../utils/with_apm_span'; import { Setup } from '../../../helpers/setup_request'; import { SERVICE_NAME, @@ -20,36 +19,37 @@ export async function getExistingEnvironmentsForService({ serviceName: string | undefined; setup: Setup; }) { - return withApmSpan('get_existing_environments_for_service', async () => { - const { internalClient, indices, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; + const { internalClient, indices, config } = setup; + const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const bool = serviceName - ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } - : { must_not: [{ exists: { field: SERVICE_NAME } }] }; + const bool = serviceName + ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } + : { must_not: [{ exists: { field: SERVICE_NAME } }] }; - const params = { - index: indices.apmAgentConfigurationIndex, - body: { - size: 0, - query: { bool }, - aggs: { - environments: { - terms: { - field: SERVICE_ENVIRONMENT, - missing: ALL_OPTION_VALUE, - size: maxServiceEnvironments, - }, + const params = { + index: indices.apmAgentConfigurationIndex, + body: { + size: 0, + query: { bool }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + missing: ALL_OPTION_VALUE, + size: maxServiceEnvironments, }, }, }, - }; + }, + }; - const resp = await internalClient.search(params); - const existingEnvironments = - resp.aggregations?.environments.buckets.map( - (bucket) => bucket.key as string - ) || []; - return existingEnvironments; - }); + const resp = await internalClient.search( + 'get_existing_environments_for_service', + params + ); + const existingEnvironments = + resp.aggregations?.environments.buckets.map( + (bucket) => bucket.key as string + ) || []; + return existingEnvironments; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 9c56455f45902f..0786bc6bc27714 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -11,52 +11,52 @@ import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; import { getProcessorEventForAggregatedTransactions } from '../../helpers/aggregated_transactions'; -import { withApmSpan } from '../../../utils/with_apm_span'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; -export function getServiceNames({ +export async function getServiceNames({ setup, searchAggregatedTransactions, }: { setup: Setup; searchAggregatedTransactions: boolean; }) { - return withApmSpan('get_service_names_for_agent_config', async () => { - const { apmEventClient, config } = setup; - const maxServiceSelection = config['xpack.apm.maxServiceSelection']; + const { apmEventClient, config } = setup; + const maxServiceSelection = config['xpack.apm.maxServiceSelection']; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - body: { - timeout: '1ms', - size: 0, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - size: maxServiceSelection, - min_doc_count: 0, - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + timeout: '1ms', + size: 0, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: maxServiceSelection, + min_doc_count: 0, }, }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); - const serviceNames = - resp.aggregations?.services.buckets - .map((bucket) => bucket.key as string) - .sort() || []; - return [ALL_OPTION_VALUE, ...serviceNames]; - }); + const resp = await apmEventClient.search( + 'get_service_names_for_agent_config', + params + ); + const serviceNames = + resp.aggregations?.services.buckets + .map((bucket) => bucket.key as string) + .sort() || []; + return [ALL_OPTION_VALUE, ...serviceNames]; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts index adcfe88392dc8b..098888c23ccbc9 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts @@ -8,7 +8,6 @@ import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { convertConfigSettingsToString } from './convert_settings_to_string'; -import { withApmSpan } from '../../../utils/with_apm_span'; export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; @@ -18,8 +17,9 @@ export async function listConfigurations({ setup }: { setup: Setup }) { size: 200, }; - const resp = await withApmSpan('list_agent_configurations', () => - internalClient.search(params) + const resp = await internalClient.search( + 'list_agent_configuration', + params ); return resp.hits.hits diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts index 2026742a936a49..5fa4993921570b 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts @@ -29,5 +29,8 @@ export async function markAppliedByAgent({ }, }; - return internalClient.index(params); + return internalClient.index( + 'mark_configuration_applied_by_agent', + params + ); } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 7454128a741d5c..4e27953b3a315d 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -13,7 +13,6 @@ import { import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { convertConfigSettingsToString } from './convert_settings_to_string'; -import { withApmSpan } from '../../../utils/with_apm_span'; export async function searchConfigurations({ service, @@ -22,65 +21,64 @@ export async function searchConfigurations({ service: AgentConfiguration['service']; setup: Setup; }) { - return withApmSpan('search_agent_configurations', async () => { - const { internalClient, indices } = setup; + const { internalClient, indices } = setup; - // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring). - // Additionally a boost has been added to service.name to ensure it scores higher. - // If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins - const serviceNameFilter = service.name - ? [ - { - constant_score: { - filter: { term: { [SERVICE_NAME]: service.name } }, - boost: 2, - }, + // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring). + // Additionally a boost has been added to service.name to ensure it scores higher. + // If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins + const serviceNameFilter = service.name + ? [ + { + constant_score: { + filter: { term: { [SERVICE_NAME]: service.name } }, + boost: 2, }, - ] - : []; + }, + ] + : []; - const environmentFilter = service.environment - ? [ - { - constant_score: { - filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } }, - boost: 1, - }, + const environmentFilter = service.environment + ? [ + { + constant_score: { + filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } }, + boost: 1, }, - ] - : []; + }, + ] + : []; - const params = { - index: indices.apmAgentConfigurationIndex, - body: { - query: { - bool: { - minimum_should_match: 2, - should: [ - ...serviceNameFilter, - ...environmentFilter, - { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, - { - bool: { - must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }], - }, + const params = { + index: indices.apmAgentConfigurationIndex, + body: { + query: { + bool: { + minimum_should_match: 2, + should: [ + ...serviceNameFilter, + ...environmentFilter, + { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, + { + bool: { + must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }], }, - ], - }, + }, + ], }, }, - }; + }, + }; - const resp = await internalClient.search( - params - ); + const resp = await internalClient.search( + 'search_agent_configurations', + params + ); - const hit = resp.hits.hits[0] as SearchHit | undefined; + const hit = resp.hits.hits[0] as SearchHit | undefined; - if (!hit) { - return; - } + if (!hit) { + return; + } - return convertConfigSettingsToString(hit); - }); + return convertConfigSettingsToString(hit); } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts index 47ee91232ea48e..051b9e2809e494 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts @@ -39,17 +39,20 @@ describe('Create or Update Custom link', () => { it('creates a new custom link', () => { createOrUpdateCustomLink({ customLink, setup: mockedSetup }); - expect(internalClientIndexMock).toHaveBeenCalledWith({ - refresh: true, - index: 'apmCustomLinkIndex', - body: { - '@timestamp': 1570737000000, - label: 'foo', - url: 'http://elastic.com/{{trace.id}}', - 'service.name': ['opbeans-java'], - 'transaction.type': ['Request'], - }, - }); + expect(internalClientIndexMock).toHaveBeenCalledWith( + 'create_or_update_custom_link', + { + refresh: true, + index: 'apmCustomLinkIndex', + body: { + '@timestamp': 1570737000000, + label: 'foo', + url: 'http://elastic.com/{{trace.id}}', + 'service.name': ['opbeans-java'], + 'transaction.type': ['Request'], + }, + } + ); }); it('update a new custom link', () => { createOrUpdateCustomLink({ @@ -57,17 +60,20 @@ describe('Create or Update Custom link', () => { customLink, setup: mockedSetup, }); - expect(internalClientIndexMock).toHaveBeenCalledWith({ - refresh: true, - index: 'apmCustomLinkIndex', - id: 'bar', - body: { - '@timestamp': 1570737000000, - label: 'foo', - url: 'http://elastic.com/{{trace.id}}', - 'service.name': ['opbeans-java'], - 'transaction.type': ['Request'], - }, - }); + expect(internalClientIndexMock).toHaveBeenCalledWith( + 'create_or_update_custom_link', + { + refresh: true, + index: 'apmCustomLinkIndex', + id: 'bar', + body: { + '@timestamp': 1570737000000, + label: 'foo', + url: 'http://elastic.com/{{trace.id}}', + 'service.name': ['opbeans-java'], + 'transaction.type': ['Request'], + }, + } + ); }); }); diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts index 7e546fb5550360..8f14e87fe183bc 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -12,7 +12,6 @@ import { import { Setup } from '../../helpers/setup_request'; import { toESFormat } from './helper'; import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; -import { withApmSpan } from '../../../utils/with_apm_span'; export function createOrUpdateCustomLink({ customLinkId, @@ -23,23 +22,21 @@ export function createOrUpdateCustomLink({ customLink: Omit; setup: Setup; }) { - return withApmSpan('create_or_update_custom_link', () => { - const { internalClient, indices } = setup; + const { internalClient, indices } = setup; - const params: APMIndexDocumentParams = { - refresh: true, - index: indices.apmCustomLinkIndex, - body: { - '@timestamp': Date.now(), - ...toESFormat(customLink), - }, - }; + const params: APMIndexDocumentParams = { + refresh: true, + index: indices.apmCustomLinkIndex, + body: { + '@timestamp': Date.now(), + ...toESFormat(customLink), + }, + }; - // by specifying an id elasticsearch will delete the previous doc and insert the updated doc - if (customLinkId) { - params.id = customLinkId; - } + // by specifying an id elasticsearch will delete the previous doc and insert the updated doc + if (customLinkId) { + params.id = customLinkId; + } - return internalClient.index(params); - }); + return internalClient.index('create_or_update_custom_link', params); } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts index 7c88bcc43cc7f7..bf7cfb33d87ac6 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; export function deleteCustomLink({ @@ -15,15 +14,13 @@ export function deleteCustomLink({ customLinkId: string; setup: Setup; }) { - return withApmSpan('delete_custom_link', () => { - const { internalClient, indices } = setup; + const { internalClient, indices } = setup; - const params = { - refresh: 'wait_for' as const, - index: indices.apmCustomLinkIndex, - id: customLinkId, - }; + const params = { + refresh: 'wait_for' as const, + index: indices.apmCustomLinkIndex, + id: customLinkId, + }; - return internalClient.delete(params); - }); + return internalClient.delete('delete_custom_link', params); } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index d3d9b452853540..91bc8c85bc0143 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -11,43 +11,43 @@ import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; import { splitFilterValueByComma } from './helper'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export function getTransaction({ +export async function getTransaction({ setup, filters = {}, }: { setup: Setup; filters?: t.TypeOf; }) { - return withApmSpan('get_transaction_for_custom_link', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const esFilters = compact( - Object.entries(filters) - // loops through the filters splitting the value by comma and removing white spaces - .map(([key, value]) => { - if (value) { - return { terms: { [key]: splitFilterValueByComma(value) } }; - } - }) - ); + const esFilters = compact( + Object.entries(filters) + // loops through the filters splitting the value by comma and removing white spaces + .map(([key, value]) => { + if (value) { + return { terms: { [key]: splitFilterValueByComma(value) } }; + } + }) + ); - const params = { - terminateAfter: 1, - apm: { - events: [ProcessorEvent.transaction as const], - }, - size: 1, - body: { - query: { - bool: { - filter: esFilters, - }, + const params = { + terminateAfter: 1, + apm: { + events: [ProcessorEvent.transaction as const], + }, + size: 1, + body: { + query: { + bool: { + filter: esFilters, }, }, - }; - const resp = await apmEventClient.search(params); - return resp.hits.hits[0]?._source; - }); + }, + }; + const resp = await apmEventClient.search( + 'get_transaction_for_custom_link', + params + ); + return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts index 0eac2e08d0901e..d477da85e0d9b8 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts @@ -14,54 +14,54 @@ import { import { Setup } from '../../helpers/setup_request'; import { fromESFormat } from './helper'; import { filterOptionsRt } from './custom_link_types'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export function listCustomLinks({ +export async function listCustomLinks({ setup, filters = {}, }: { setup: Setup; filters?: t.TypeOf; }): Promise { - return withApmSpan('list_custom_links', async () => { - const { internalClient, indices } = setup; - const esFilters = Object.entries(filters).map(([key, value]) => { - return { + const { internalClient, indices } = setup; + const esFilters = Object.entries(filters).map(([key, value]) => { + return { + bool: { + minimum_should_match: 1, + should: [ + { term: { [key]: value } }, + { bool: { must_not: [{ exists: { field: key } }] } }, + ] as QueryDslQueryContainer[], + }, + }; + }); + + const params = { + index: indices.apmCustomLinkIndex, + size: 500, + body: { + query: { bool: { - minimum_should_match: 1, - should: [ - { term: { [key]: value } }, - { bool: { must_not: [{ exists: { field: key } }] } }, - ] as QueryDslQueryContainer[], + filter: esFilters, }, - }; - }); - - const params = { - index: indices.apmCustomLinkIndex, - size: 500, - body: { - query: { - bool: { - filter: esFilters, + }, + sort: [ + { + 'label.keyword': { + order: 'asc' as const, }, }, - sort: [ - { - 'label.keyword': { - order: 'asc' as const, - }, - }, - ], - }, - }; - const resp = await internalClient.search(params); - const customLinks = resp.hits.hits.map((item) => - fromESFormat({ - id: item._id, - ...item._source, - }) - ); - return customLinks; - }); + ], + }, + }; + const resp = await internalClient.search( + 'list_custom_links', + params + ); + const customLinks = resp.hits.hits.map((item) => + fromESFormat({ + id: item._id, + ...item._source, + }) + ); + return customLinks; } diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index 157f09978eaec6..68d316ef55df98 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -19,7 +19,6 @@ import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeQuery } from '../../../server/utils/queries'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseValueType } from '../../../typings/common'; -import { withApmSpan } from '../../utils/with_apm_span'; export interface ErrorsPerTransaction { [transactionId: string]: number; @@ -29,103 +28,94 @@ export async function getTraceItems( traceId: string, setup: Setup & SetupTimeRange ) { - return withApmSpan('get_trace_items', async () => { - const { start, end, apmEventClient, config } = setup; - const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; - const excludedLogLevels = ['debug', 'info', 'warning']; + const { start, end, apmEventClient, config } = setup; + const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; + const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = withApmSpan('get_trace_error_items', () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.error], + const errorResponsePromise = apmEventClient.search('get_trace_items', { + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: maxTraceItems, + query: { + bool: { + filter: [ + { term: { [TRACE_ID]: traceId } }, + ...rangeQuery(start, end), + ], + must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, }, - body: { - size: maxTraceItems, - query: { - bool: { - filter: [ - { term: { [TRACE_ID]: traceId } }, - ...rangeQuery(start, end), - ], - must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, - }, - }, - aggs: { - by_transaction_id: { - terms: { - field: TRANSACTION_ID, - size: maxTraceItems, - // high cardinality - execution_hint: 'map' as const, - }, - }, + }, + aggs: { + by_transaction_id: { + terms: { + field: TRANSACTION_ID, + size: maxTraceItems, + // high cardinality + execution_hint: 'map' as const, }, }, - }) - ); + }, + }, + }); - const traceResponsePromise = withApmSpan('get_trace_span_items', () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.span, ProcessorEvent.transaction], - }, - body: { - size: maxTraceItems, - query: { - bool: { - filter: [ - { term: { [TRACE_ID]: traceId } }, - ...rangeQuery(start, end), - ] as QueryDslQueryContainer[], - should: { - exists: { field: PARENT_ID }, - }, - }, + const traceResponsePromise = apmEventClient.search('get_trace_span_items', { + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, + body: { + size: maxTraceItems, + query: { + bool: { + filter: [ + { term: { [TRACE_ID]: traceId } }, + ...rangeQuery(start, end), + ] as QueryDslQueryContainer[], + should: { + exists: { field: PARENT_ID }, }, - sort: [ - { _score: { order: 'asc' as const } }, - { [TRANSACTION_DURATION]: { order: 'desc' as const } }, - { [SPAN_DURATION]: { order: 'desc' as const } }, - ], - track_total_hits: true, }, - }) - ); + }, + sort: [ + { _score: { order: 'asc' as const } }, + { [TRANSACTION_DURATION]: { order: 'desc' as const } }, + { [SPAN_DURATION]: { order: 'desc' as const } }, + ], + track_total_hits: true, + }, + }); - const [errorResponse, traceResponse]: [ - // explicit intermediary types to avoid TS "excessively deep" error - PromiseValueType, - PromiseValueType - ] = (await Promise.all([ - errorResponsePromise, - traceResponsePromise, - ])) as any; + const [errorResponse, traceResponse]: [ + // explicit intermediary types to avoid TS "excessively deep" error + PromiseValueType, + PromiseValueType + ] = (await Promise.all([errorResponsePromise, traceResponsePromise])) as any; - const exceedsMax = traceResponse.hits.total.value > maxTraceItems; + const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const items = traceResponse.hits.hits.map((hit) => hit._source); + const items = traceResponse.hits.hits.map((hit) => hit._source); - const errorFrequencies: { - errorsPerTransaction: ErrorsPerTransaction; - errorDocs: APMError[]; - } = { - errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), - errorsPerTransaction: - errorResponse.aggregations?.by_transaction_id.buckets.reduce( - (acc, current) => { - return { - ...acc, - [current.key]: current.doc_count, - }; - }, - {} as ErrorsPerTransaction - ) ?? {}, - }; + const errorFrequencies: { + errorsPerTransaction: ErrorsPerTransaction; + errorDocs: APMError[]; + } = { + errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), + errorsPerTransaction: + errorResponse.aggregations?.by_transaction_id.buckets.reduce( + (acc, current) => { + return { + ...acc, + [current.key]: current.doc_count, + }; + }, + {} as ErrorsPerTransaction + ) ?? {}, + }; - return { - items, - exceedsMax, - ...errorFrequencies, - }; - }); + return { + items, + exceedsMax, + ...errorFrequencies, + }; } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 71f803a03bf85f..6499e80be93027 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -31,7 +31,6 @@ import { getOutcomeAggregation, getTransactionErrorRateTimeSeries, } from '../helpers/transaction_error_rate'; -import { withApmSpan } from '../../utils/with_apm_span'; export async function getErrorRate({ environment, @@ -58,81 +57,82 @@ export async function getErrorRate({ transactionErrorRate: Coordinate[]; average: number | null; }> { - return withApmSpan('get_transaction_group_error_rate', async () => { - const { apmEventClient } = setup; - - const transactionNamefilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - const transactionTypefilter = transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []; - - const filter = [ - { term: { [SERVICE_NAME]: serviceName } }, - { - terms: { - [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], - }, - }, - ...transactionNamefilter, - ...transactionTypefilter, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - const outcomes = getOutcomeAggregation(); - - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], + const { apmEventClient } = setup; + + const transactionNamefilter = transactionName + ? [{ term: { [TRANSACTION_NAME]: transactionName } }] + : []; + const transactionTypefilter = transactionType + ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] + : []; + + const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, + { + terms: { + [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], }, - body: { - size: 0, - query: { bool: { filter } }, - aggs: { - outcomes, - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: getBucketSize({ start, end }).intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, - aggs: { - outcomes, - }, + }, + ...transactionNamefilter, + ...transactionTypefilter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; + + const outcomes = getOutcomeAggregation(); + + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { bool: { filter } }, + aggs: { + outcomes, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ start, end }).intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { + outcomes, }, }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); + const resp = await apmEventClient.search( + 'get_transaction_group_error_rate', + params + ); - const noHits = resp.hits.total.value === 0; + const noHits = resp.hits.total.value === 0; - if (!resp.aggregations) { - return { noHits, transactionErrorRate: [], average: null }; - } + if (!resp.aggregations) { + return { noHits, transactionErrorRate: [], average: null }; + } - const transactionErrorRate = getTransactionErrorRateTimeSeries( - resp.aggregations.timeseries.buckets - ); + const transactionErrorRate = getTransactionErrorRateTimeSeries( + resp.aggregations.timeseries.buckets + ); - const average = calculateTransactionErrorPercentage( - resp.aggregations.outcomes - ); + const average = calculateTransactionErrorPercentage( + resp.aggregations.outcomes + ); - return { noHits, transactionErrorRate, average }; - }); + return { noHits, transactionErrorRate, average }; } export async function getErrorRatePeriods({ diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 8156d52d984dfe..34fd86f2fc5989 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -11,7 +11,6 @@ import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { withApmSpan } from '../../utils/with_apm_span'; interface MetricParams { request: TransactionGroupRequestBase; @@ -39,124 +38,128 @@ function mergeRequestWithAggs< }); } -export function getAverages({ +export async function getAverages({ request, setup, searchAggregatedTransactions, }: MetricParams) { - return withApmSpan('get_avg_transaction_group_duration', async () => { - const params = mergeRequestWithAggs(request, { + const params = mergeRequestWithAggs(request, { + avg: { avg: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, - }); - - const response = await setup.apmEventClient.search(params); - - return arrayUnionToCallable( - response.aggregations?.transaction_groups.buckets ?? [] - ).map((bucket) => { - return { - key: bucket.key as BucketKey, - avg: bucket.avg.value, - }; - }); + }, + }); + + const response = await setup.apmEventClient.search( + 'get_avg_transaction_group_duration', + params + ); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + avg: bucket.avg.value, + }; }); } -export function getCounts({ request, setup }: MetricParams) { - return withApmSpan('get_transaction_group_transaction_count', async () => { - const params = mergeRequestWithAggs(request, { - transaction_type: { - top_metrics: { - sort: { - '@timestamp': 'desc' as const, - }, - metrics: [ - { - field: TRANSACTION_TYPE, - } as const, - ], +export async function getCounts({ request, setup }: MetricParams) { + const params = mergeRequestWithAggs(request, { + transaction_type: { + top_metrics: { + sort: { + '@timestamp': 'desc' as const, }, + metrics: [ + { + field: TRANSACTION_TYPE, + } as const, + ], }, - }); - - const response = await setup.apmEventClient.search(params); - - return arrayUnionToCallable( - response.aggregations?.transaction_groups.buckets ?? [] - ).map((bucket) => { - return { - key: bucket.key as BucketKey, - count: bucket.doc_count, - transactionType: bucket.transaction_type.top[0].metrics[ - TRANSACTION_TYPE - ] as string, - }; - }); + }, + }); + + const response = await setup.apmEventClient.search( + 'get_transaction_group_transaction_count', + params + ); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + count: bucket.doc_count, + transactionType: bucket.transaction_type.top[0].metrics[ + TRANSACTION_TYPE + ] as string, + }; }); } -export function getSums({ +export async function getSums({ request, setup, searchAggregatedTransactions, }: MetricParams) { - return withApmSpan('get_transaction_group_latency_sums', async () => { - const params = mergeRequestWithAggs(request, { + const params = mergeRequestWithAggs(request, { + sum: { sum: { - sum: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, - }); - - const response = await setup.apmEventClient.search(params); - - return arrayUnionToCallable( - response.aggregations?.transaction_groups.buckets ?? [] - ).map((bucket) => { - return { - key: bucket.key as BucketKey, - sum: bucket.sum.value, - }; - }); + }, + }); + + const response = await setup.apmEventClient.search( + 'get_transaction_group_latency_sums', + params + ); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + sum: bucket.sum.value, + }; }); } -export function getPercentiles({ +export async function getPercentiles({ request, setup, searchAggregatedTransactions, }: MetricParams) { - return withApmSpan('get_transaction_group_latency_percentiles', async () => { - const params = mergeRequestWithAggs(request, { - p95: { - percentiles: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - hdr: { number_of_significant_value_digits: 2 }, - percents: [95], - }, + const params = mergeRequestWithAggs(request, { + p95: { + percentiles: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + hdr: { number_of_significant_value_digits: 2 }, + percents: [95], }, - }); - - const response = await setup.apmEventClient.search(params); - - return arrayUnionToCallable( - response.aggregations?.transaction_groups.buckets ?? [] - ).map((bucket) => { - return { - key: bucket.key as BucketKey, - p95: Object.values(bucket.p95.values)[0], - }; - }); + }, + }); + + const response = await setup.apmEventClient.search( + 'get_transaction_group_latency_percentiles', + params + ); + + return arrayUnionToCallable( + response.aggregations?.transaction_groups.buckets ?? [] + ).map((bucket) => { + return { + key: bucket.key as BucketKey, + p95: Object.values(bucket.p95.values)[0], + }; }); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 6cb85b62205b87..5c1754cd36ef4c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -33,7 +33,7 @@ describe('transaction group queries', () => { ) ); - const allParams = mock.spy.mock.calls.map((call) => call[0]); + const allParams = mock.spy.mock.calls.map((call) => call[1]); expect(allParams).toMatchSnapshot(); }); @@ -51,7 +51,7 @@ describe('transaction group queries', () => { ) ); - const allParams = mock.spy.mock.calls.map((call) => call[0]); + const allParams = mock.spy.mock.calls.map((call) => call[1]); expect(allParams).toMatchSnapshot(); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 568769b52e2b4b..20534a5fa7cbfc 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -26,9 +26,8 @@ import { import { getMetricsDateHistogramParams } from '../../helpers/metrics'; import { MAX_KPIS } from './constants'; import { getVizColorForIndex } from '../../../../common/viz_colors'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export function getTransactionBreakdown({ +export async function getTransactionBreakdown({ environment, kuery, setup, @@ -43,205 +42,203 @@ export function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - return withApmSpan('get_transaction_breakdown', async () => { - const { apmEventClient, start, end, config } = setup; + const { apmEventClient, start, end, config } = setup; - const subAggs = { - sum_all_self_times: { - sum: { - field: SPAN_SELF_TIME_SUM, - }, + const subAggs = { + sum_all_self_times: { + sum: { + field: SPAN_SELF_TIME_SUM, }, - total_transaction_breakdown_count: { - sum: { - field: TRANSACTION_BREAKDOWN_COUNT, - }, + }, + total_transaction_breakdown_count: { + sum: { + field: TRANSACTION_BREAKDOWN_COUNT, }, - types: { - terms: { - field: SPAN_TYPE, - size: 20, - order: { - _count: 'desc' as const, - }, + }, + types: { + terms: { + field: SPAN_TYPE, + size: 20, + order: { + _count: 'desc' as const, }, - aggs: { - subtypes: { - terms: { - field: SPAN_SUBTYPE, - missing: '', - size: 20, - order: { - _count: 'desc' as const, - }, + }, + aggs: { + subtypes: { + terms: { + field: SPAN_SUBTYPE, + missing: '', + size: 20, + order: { + _count: 'desc' as const, }, - aggs: { - total_self_time_per_subtype: { - sum: { - field: SPAN_SELF_TIME_SUM, - }, + }, + aggs: { + total_self_time_per_subtype: { + sum: { + field: SPAN_SELF_TIME_SUM, }, }, }, }, }, - }; - - const filters = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - { + }, + }; + + const filters = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + { + bool: { + should: [ + { exists: { field: SPAN_SELF_TIME_SUM } }, + { exists: { field: TRANSACTION_BREAKDOWN_COUNT } }, + ], + minimum_should_match: 1, + }, + }, + ]; + + if (transactionName) { + filters.push({ term: { [TRANSACTION_NAME]: transactionName } }); + } + + const params = { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { bool: { - should: [ - { exists: { field: SPAN_SELF_TIME_SUM } }, - { exists: { field: TRANSACTION_BREAKDOWN_COUNT } }, - ], - minimum_should_match: 1, + filter: filters, }, }, - ]; - - if (transactionName) { - filters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - - const params = { - apm: { - events: [ProcessorEvent.metric], - }, - body: { - size: 0, - query: { - bool: { - filter: filters, - }, - }, - aggs: { - ...subAggs, - by_date: { - date_histogram: getMetricsDateHistogramParams( - start, - end, - config['xpack.apm.metricsInterval'] - ), - aggs: subAggs, - }, + aggs: { + ...subAggs, + by_date: { + date_histogram: getMetricsDateHistogramParams( + start, + end, + config['xpack.apm.metricsInterval'] + ), + aggs: subAggs, }, }, + }, + }; + + const resp = await apmEventClient.search('get_transaction_breakdown', params); + + const formatBucket = ( + aggs: + | Required['aggregations'] + | Required['aggregations']['by_date']['buckets'][0] + ) => { + const sumAllSelfTimes = aggs.sum_all_self_times.value || 0; + + const breakdowns = flatten( + aggs.types.buckets.map((bucket) => { + const type = bucket.key as string; + + return bucket.subtypes.buckets.map((subBucket) => { + return { + name: (subBucket.key as string) || type, + percentage: + (subBucket.total_self_time_per_subtype.value || 0) / + sumAllSelfTimes, + }; + }); + }) + ); + + return breakdowns; + }; + + const visibleKpis = resp.aggregations + ? orderBy(formatBucket(resp.aggregations), 'percentage', 'desc').slice( + 0, + MAX_KPIS + ) + : []; + + const kpis = orderBy( + visibleKpis.map((kpi) => ({ + ...kpi, + lowerCaseName: kpi.name.toLowerCase(), + })), + 'lowerCaseName' + ).map((kpi, index) => { + const { lowerCaseName, ...rest } = kpi; + return { + ...rest, + color: getVizColorForIndex(index), }; + }); - const resp = await apmEventClient.search(params); - - const formatBucket = ( - aggs: - | Required['aggregations'] - | Required['aggregations']['by_date']['buckets'][0] - ) => { - const sumAllSelfTimes = aggs.sum_all_self_times.value || 0; - - const breakdowns = flatten( - aggs.types.buckets.map((bucket) => { - const type = bucket.key as string; - - return bucket.subtypes.buckets.map((subBucket) => { - return { - name: (subBucket.key as string) || type, - percentage: - (subBucket.total_self_time_per_subtype.value || 0) / - sumAllSelfTimes, - }; - }); - }) - ); - - return breakdowns; - }; - - const visibleKpis = resp.aggregations - ? orderBy(formatBucket(resp.aggregations), 'percentage', 'desc').slice( - 0, - MAX_KPIS - ) - : []; - - const kpis = orderBy( - visibleKpis.map((kpi) => ({ - ...kpi, - lowerCaseName: kpi.name.toLowerCase(), - })), - 'lowerCaseName' - ).map((kpi, index) => { - const { lowerCaseName, ...rest } = kpi; - return { - ...rest, - color: getVizColorForIndex(index), - }; - }); - - const kpiNames = kpis.map((kpi) => kpi.name); + const kpiNames = kpis.map((kpi) => kpi.name); - const bucketsByDate = resp.aggregations?.by_date.buckets || []; + const bucketsByDate = resp.aggregations?.by_date.buckets || []; - const timeseriesPerSubtype = bucketsByDate.reduce((prev, bucket) => { - const formattedValues = formatBucket(bucket); - const time = bucket.key; + const timeseriesPerSubtype = bucketsByDate.reduce((prev, bucket) => { + const formattedValues = formatBucket(bucket); + const time = bucket.key; - const updatedSeries = kpiNames.reduce((p, kpiName) => { - const { name, percentage } = formattedValues.find( - (val) => val.name === kpiName - ) || { - name: kpiName, - percentage: null, - }; + const updatedSeries = kpiNames.reduce((p, kpiName) => { + const { name, percentage } = formattedValues.find( + (val) => val.name === kpiName + ) || { + name: kpiName, + percentage: null, + }; - if (!p[name]) { - p[name] = []; - } - return { - ...p, - [name]: p[name].concat({ - x: time, - y: percentage, - }), - }; - }, prev); - - const lastValues = Object.values(updatedSeries).map(last); - - // If for a given timestamp, some series have data, but others do not, - // we have to set any null values to 0 to make sure the stacked area chart - // is drawn correctly. - // If we set all values to 0, the chart always displays null values as 0, - // and the chart looks weird. - const hasAnyValues = lastValues.some((value) => value?.y !== null); - const hasNullValues = lastValues.some((value) => value?.y === null); - - if (hasAnyValues && hasNullValues) { - Object.values(updatedSeries).forEach((series) => { - const value = series[series.length - 1]; - const isEmpty = value.y === null; - if (isEmpty) { - // local mutation to prevent complicated map/reduce calls - value.y = 0; - } - }); + if (!p[name]) { + p[name] = []; } + return { + ...p, + [name]: p[name].concat({ + x: time, + y: percentage, + }), + }; + }, prev); + + const lastValues = Object.values(updatedSeries).map(last); + + // If for a given timestamp, some series have data, but others do not, + // we have to set any null values to 0 to make sure the stacked area chart + // is drawn correctly. + // If we set all values to 0, the chart always displays null values as 0, + // and the chart looks weird. + const hasAnyValues = lastValues.some((value) => value?.y !== null); + const hasNullValues = lastValues.some((value) => value?.y === null); + + if (hasAnyValues && hasNullValues) { + Object.values(updatedSeries).forEach((series) => { + const value = series[series.length - 1]; + const isEmpty = value.y === null; + if (isEmpty) { + // local mutation to prevent complicated map/reduce calls + value.y = 0; + } + }); + } - return updatedSeries; - }, {} as Record>); + return updatedSeries; + }, {} as Record>); - const timeseries = kpis.map((kpi) => ({ - title: kpi.name, - color: kpi.color, - type: 'areaStacked', - data: timeseriesPerSubtype[kpi.name], - hideLegend: false, - legendValue: asPercent(kpi.percentage, 1), - })); + const timeseries = kpis.map((kpi) => ({ + title: kpi.name, + color: kpi.color, + type: 'areaStacked', + data: timeseriesPerSubtype[kpi.name], + hideLegend: false, + legendValue: asPercent(kpi.percentage, 1), + })); - return { timeseries }; - }); + return { timeseries }; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 3b4319c37996dc..6259bb75386fb3 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -89,48 +89,47 @@ export async function getBuckets({ ] as QueryDslQueryContainer[]; async function getSamplesForDistributionBuckets() { - const response = await withApmSpan( + const response = await apmEventClient.search( 'get_samples_for_latency_distribution_buckets', - () => - apmEventClient.search({ - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - { term: { [TRANSACTION_SAMPLED]: true } }, - ], - should: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [TRANSACTION_ID]: transactionId } }, - ] as QueryDslQueryContainer[], - }, + { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + { term: { [TRANSACTION_SAMPLED]: true } }, + ], + should: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [TRANSACTION_ID]: transactionId } }, + ] as QueryDslQueryContainer[], }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - bucketSize, - field: TRANSACTION_DURATION, - distributionMax, - }), - aggs: { - samples: { - top_hits: { - _source: [TRANSACTION_ID, TRACE_ID], - size: 10, - sort: { - _score: 'desc' as const, - }, + }, + aggs: { + distribution: { + histogram: getHistogramAggOptions({ + bucketSize, + field: TRANSACTION_DURATION, + distributionMax, + }), + aggs: { + samples: { + top_hits: { + _source: [TRANSACTION_ID, TRACE_ID], + size: 10, + sort: { + _score: 'desc' as const, }, }, }, }, }, }, - }) + }, + } ); return ( @@ -148,41 +147,40 @@ export async function getBuckets({ } async function getDistributionBuckets() { - const response = await withApmSpan( + const response = await apmEventClient.search( 'get_latency_distribution_buckets', - () => - apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, + { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - bucketSize, - distributionMax, - }), - }, + }, + aggs: { + distribution: { + histogram: getHistogramAggOptions({ + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + bucketSize, + distributionMax, + }), }, }, - }) + }, + } ); return ( diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 2e86f6bb84c815..f3d4e8f6dd92d5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -20,7 +20,6 @@ import { rangeQuery, kqlQuery, } from '../../../../server/utils/queries'; -import { withApmSpan } from '../../../utils/with_apm_span'; export async function getDistributionMax({ environment, @@ -39,44 +38,45 @@ export async function getDistributionMax({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { - return withApmSpan('get_latency_distribution_max', async () => { - const { start, end, apmEventClient } = setup; + const { start, end, apmEventClient } = setup; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - }, + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [TRANSACTION_NAME]: transactionName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], }, - aggs: { - stats: { - max: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, + }, + aggs: { + stats: { + max: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); - return resp.aggregations?.stats.value ?? null; - }); + const resp = await apmEventClient.search( + 'get_latency_distribution_max', + params + ); + return resp.aggregations?.stats.value ?? null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index 5a583605978282..2d350090fa28b7 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -26,7 +26,6 @@ import { } from '../../../lib/helpers/aggregated_transactions'; import { getBucketSize } from '../../../lib/helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getLatencyAggregation, getLatencyValue, @@ -112,10 +111,10 @@ function searchLatency({ }, }; - return apmEventClient.search(params); + return apmEventClient.search('get_latency_charts', params); } -export function getLatencyTimeseries({ +export async function getLatencyTimeseries({ environment, kuery, serviceName, @@ -138,40 +137,38 @@ export function getLatencyTimeseries({ start: number; end: number; }) { - return withApmSpan('get_latency_charts', async () => { - const response = await searchLatency({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - setup, - searchAggregatedTransactions, - latencyAggregationType, - start, - end, - }); - - if (!response.aggregations) { - return { latencyTimeseries: [], overallAvgDuration: null }; - } - - return { - overallAvgDuration: - response.aggregations.overall_avg_duration.value || null, - latencyTimeseries: response.aggregations.latencyTimeseries.buckets.map( - (bucket) => { - return { - x: bucket.key, - y: getLatencyValue({ - latencyAggregationType, - aggregation: bucket.latency, - }), - }; - } - ), - }; + const response = await searchLatency({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + latencyAggregationType, + start, + end, }); + + if (!response.aggregations) { + return { latencyTimeseries: [], overallAvgDuration: null }; + } + + return { + overallAvgDuration: + response.aggregations.overall_avg_duration.value || null, + latencyTimeseries: response.aggregations.latencyTimeseries.buckets.map( + (bucket) => { + return { + x: bucket.key, + y: getLatencyValue({ + latencyAggregationType, + aggregation: bucket.latency, + }), + }; + } + ), + }; } export async function getLatencyPeriods({ diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index a0225eb47e584b..f4d9236395252c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -24,7 +24,6 @@ import { } from '../../../lib/helpers/aggregated_transactions'; import { getBucketSize } from '../../../lib/helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { getThroughputBuckets } from './transform'; export type ThroughputChartsResponse = PromiseReturnType< @@ -96,7 +95,7 @@ function searchThroughput({ }, }; - return apmEventClient.search(params); + return apmEventClient.search('get_transaction_throughput_series', params); } export async function getThroughputCharts({ @@ -116,26 +115,24 @@ export async function getThroughputCharts({ setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { - return withApmSpan('get_transaction_throughput_series', async () => { - const { bucketSize, intervalString } = getBucketSize(setup); + const { bucketSize, intervalString } = getBucketSize(setup); - const response = await searchThroughput({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - setup, - searchAggregatedTransactions, - intervalString, - }); - - return { - throughputTimeseries: getThroughputBuckets({ - throughputResultBuckets: response.aggregations?.throughput.buckets, - bucketSize, - setupTimeRange: setup, - }), - }; + const response = await searchThroughput({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + intervalString, }); + + return { + throughputTimeseries: getThroughputBuckets({ + throughputResultBuckets: response.aggregations?.throughput.buckets, + bucketSize, + setupTimeRange: setup, + }), + }; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index 6987ef07577348..c928b00cefb63c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -13,9 +13,8 @@ import { rangeQuery } from '../../../../server/utils/queries'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export function getTransaction({ +export async function getTransaction({ transactionId, traceId, setup, @@ -24,27 +23,25 @@ export function getTransaction({ traceId?: string; setup: Setup | (Setup & SetupTimeRange); }) { - return withApmSpan('get_transaction', async () => { - const { apmEventClient } = setup; + const { apmEventClient } = setup; - const resp = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - size: 1, - query: { - bool: { - filter: asMutableArray([ - { term: { [TRANSACTION_ID]: transactionId } }, - ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []), - ...('start' in setup ? rangeQuery(setup.start, setup.end) : []), - ]), - }, + const resp = await apmEventClient.search('get_transaction', { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + size: 1, + query: { + bool: { + filter: asMutableArray([ + { term: { [TRANSACTION_ID]: transactionId } }, + ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []), + ...('start' in setup ? rangeQuery(setup.start, setup.end) : []), + ]), }, }, - }); - - return resp.hits.hits[0]?._source; + }, }); + + return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts index dfdad2f59a848f..568ce16e7aedc2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -11,40 +11,43 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { withApmSpan } from '../../../utils/with_apm_span'; -export function getRootTransactionByTraceId(traceId: string, setup: Setup) { - return withApmSpan('get_root_transaction_by_trace_id', async () => { - const { apmEventClient } = setup; +export async function getRootTransactionByTraceId( + traceId: string, + setup: Setup +) { + const { apmEventClient } = setup; - const params = { - apm: { - events: [ProcessorEvent.transaction as const], - }, - body: { - size: 1, - query: { - bool: { - should: [ - { - constant_score: { - filter: { - bool: { - must_not: { exists: { field: PARENT_ID } }, - }, + const params = { + apm: { + events: [ProcessorEvent.transaction as const], + }, + body: { + size: 1, + query: { + bool: { + should: [ + { + constant_score: { + filter: { + bool: { + must_not: { exists: { field: PARENT_ID } }, }, }, }, - ], - filter: [{ term: { [TRACE_ID]: traceId } }], - }, + }, + ], + filter: [{ term: { [TRACE_ID]: traceId } }], }, }, - }; + }, + }; - const resp = await apmEventClient.search(params); - return { - transaction: resp.hits.hits[0]?._source, - }; - }); + const resp = await apmEventClient.search( + 'get_root_transaction_by_trace_id', + params + ); + return { + transaction: resp.hits.hits[0]?._source, + }; } diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 6252c33c5994dd..9c63f0140bdf2b 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -109,7 +109,7 @@ export async function inspectSearchParams( } return { - params: spy.mock.calls[0][0], + params: spy.mock.calls[0][1], response, error, spy, diff --git a/x-pack/plugins/apm/server/utils/with_apm_span.ts b/x-pack/plugins/apm/server/utils/with_apm_span.ts index 9762a7213d0a2b..1343970f04a3f3 100644 --- a/x-pack/plugins/apm/server/utils/with_apm_span.ts +++ b/x-pack/plugins/apm/server/utils/with_apm_span.ts @@ -13,7 +13,7 @@ export function withApmSpan( const options = parseSpanOptions(optionsOrName); const optionsWithDefaults = { - type: 'plugin:apm', + ...(options.intercept ? {} : { type: 'plugin:apm' }), ...options, labels: { plugin: 'apm', From 767b67d0522ec00865a7746d3ec37cd5de077821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Mon, 14 Jun 2021 18:49:58 +0200 Subject: [PATCH 73/99] [APM] Change View full trace button fill (#102066) --- .../WaterfallWithSummmary/MaybeViewTraceLink.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 4017495dd3b5dd..11a0cc1234f428 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -45,7 +45,7 @@ export const MaybeViewTraceLink = ({ } )} > - + {viewFullTraceButtonLabel}
    @@ -67,7 +67,7 @@ export const MaybeViewTraceLink = ({ } )} > - + {viewFullTraceButtonLabel} @@ -92,7 +92,9 @@ export const MaybeViewTraceLink = ({ environment={nextEnvironment} latencyAggregationType={latencyAggregationType} > - {viewFullTraceButtonLabel} + + {viewFullTraceButtonLabel} + ); From 4fb0c943a6b5048c6033f5d8d41729aa2eb4f839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Mon, 14 Jun 2021 13:31:20 -0400 Subject: [PATCH 74/99] [Security Solution] Refactor rules for modularization by updating bulkCreate and wrapHits methods (#101544) creates bulkCreate and wrapHits factories for the modularization of the detection engine Co-authored-by: Marshall Main --- .../rules_notification_alert_type.ts | 1 - .../schedule_notification_actions.ts | 3 +- .../__mocks__/build_rule_message.mock.ts | 15 + .../signals/bulk_create_factory.ts | 100 ++++++ .../signals/bulk_create_ml_signals.ts | 15 +- .../signals/executors/eql.test.ts | 39 +-- .../detection_engine/signals/executors/eql.ts | 21 +- .../signals/executors/ml.test.ts | 30 +- .../detection_engine/signals/executors/ml.ts | 21 +- .../signals/executors/query.ts | 12 +- .../signals/executors/threat_match.ts | 12 +- .../signals/executors/threshold.test.ts | 20 +- .../signals/executors/threshold.ts | 27 +- .../signals/filter_duplicate_signals.test.ts | 46 +++ .../signals/filter_duplicate_signals.ts | 14 + .../signals/search_after_bulk_create.test.ts | 67 ++-- .../signals/search_after_bulk_create.ts | 22 +- .../signals/signal_rule_alert_type.test.ts | 30 +- .../signals/signal_rule_alert_type.ts | 37 +- .../signals/single_bulk_create.test.ts | 318 ------------------ .../signals/single_bulk_create.ts | 227 ------------- .../threat_mapping/create_threat_signal.ts | 6 +- .../threat_mapping/create_threat_signals.ts | 7 +- .../signals/threat_mapping/types.ts | 8 +- .../signals/threat_mapping/utils.test.ts | 37 ++ .../signals/threat_mapping/utils.ts | 3 + .../bulk_create_threshold_signals.ts | 146 ++++---- .../lib/detection_engine/signals/types.ts | 14 +- .../detection_engine/signals/utils.test.ts | 13 +- .../lib/detection_engine/signals/utils.ts | 28 +- .../signals/wrap_hits_factory.ts | 35 ++ .../security_solution/server/lib/types.ts | 3 + .../security_solution/server/plugin.ts | 2 + 33 files changed, 543 insertions(+), 836 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 6f8dac5b49b31f..a4863e577c6bca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -91,7 +91,6 @@ export const rulesNotificationAlertType = ({ signalsCount, resultsLink, ruleParams, - // @ts-expect-error @elastic/elasticsearch _source is optional signals, }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts index e7db10380eea11..bfb96e97edf117 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -8,7 +8,6 @@ import { mapKeys, snakeCase } from 'lodash/fp'; import { AlertInstance } from '../../../../../alerting/server'; import { RuleParams } from '../schemas/rule_schemas'; -import { SignalSource } from '../signals/types'; export type NotificationRuleTypeParams = RuleParams & { name: string; @@ -20,7 +19,7 @@ interface ScheduleNotificationActions { signalsCount: number; resultsLink: string; ruleParams: NotificationRuleTypeParams; - signals: SignalSource[]; + signals: unknown[]; } export const scheduleNotificationActions = ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts new file mode 100644 index 00000000000000..f43142d1d0264f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildRuleMessageFactory } from '../rule_messages'; + +export const mockBuildRuleMessage = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts new file mode 100644 index 00000000000000..f518ac2386d0bb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_factory.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { performance } from 'perf_hooks'; +import { countBy, isEmpty, get } from 'lodash'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { BuildRuleMessage } from './rule_messages'; +import { RefreshTypes } from '../types'; +import { BaseHit } from '../../../../common/detection_engine/types'; +import { errorAggregator, makeFloatString } from './utils'; + +export interface GenericBulkCreateResponse { + success: boolean; + bulkCreateDuration: string; + createdItemsCount: number; + createdItems: Array; + errors: string[]; +} + +export const bulkCreateFactory = ( + logger: Logger, + esClient: ElasticsearchClient, + buildRuleMessage: BuildRuleMessage, + refreshForBulkCreate: RefreshTypes +) => async (wrappedDocs: Array>): Promise> => { + if (wrappedDocs.length === 0) { + return { + errors: [], + success: true, + bulkCreateDuration: '0', + createdItemsCount: 0, + createdItems: [], + }; + } + + const bulkBody = wrappedDocs.flatMap((wrappedDoc) => [ + { + create: { + _index: wrappedDoc._index, + _id: wrappedDoc._id, + }, + }, + wrappedDoc._source, + ]); + const start = performance.now(); + + const { body: response } = await esClient.bulk({ + refresh: refreshForBulkCreate, + body: bulkBody, + }); + + const end = performance.now(); + logger.debug( + buildRuleMessage( + `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` + ) + ); + logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`)); + const createdItems = wrappedDocs + .map((doc, index) => ({ + _id: response.items[index].create?._id ?? '', + _index: response.items[index].create?._index ?? '', + ...doc._source, + })) + .filter((_, index) => get(response.items[index], 'create.status') === 201); + const createdItemsCount = createdItems.length; + const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; + const errorCountByMessage = errorAggregator(response, [409]); + + logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`)); + if (duplicateSignalsCount > 0) { + logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`)); + } + if (!isEmpty(errorCountByMessage)) { + logger.error( + buildRuleMessage( + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` + ) + ); + return { + errors: Object.keys(errorCountByMessage), + success: false, + bulkCreateDuration: makeFloatString(end - start), + createdItemsCount, + createdItems, + }; + } else { + return { + errors: [], + success: true, + bulkCreateDuration: makeFloatString(end - start), + createdItemsCount, + createdItems, + }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 00ac40fa7e27ce..ebb4462817eabc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -14,11 +14,10 @@ import { AlertInstanceState, AlertServices, } from '../../../../../alerting/server'; -import { RefreshTypes } from '../types'; -import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; +import { GenericBulkCreateResponse } from './bulk_create_factory'; import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; -import { AlertAttributes } from './types'; +import { AlertAttributes, BulkCreate, WrapHits } from './types'; import { MachineLearningRuleParams } from '../schemas/rule_schemas'; interface BulkCreateMlSignalsParams { @@ -28,8 +27,9 @@ interface BulkCreateMlSignalsParams { logger: Logger; id: string; signalsIndex: string; - refresh: RefreshTypes; buildRuleMessage: BuildRuleMessage; + bulkCreate: BulkCreate; + wrapHits: WrapHits; } interface EcsAnomaly extends Anomaly { @@ -85,9 +85,10 @@ const transformAnomalyResultsToEcs = ( export const bulkCreateMlSignals = async ( params: BulkCreateMlSignalsParams -): Promise => { +): Promise> => { const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - const buildRuleMessage = params.buildRuleMessage; - return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage }); + + const wrappedDocs = params.wrapHits(ecsResults.hits.hits); + return params.bulkCreate(wrappedDocs); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts index f8f77bd2bf6e6d..947e7d573173ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts @@ -7,7 +7,6 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks'; -import { RuleStatusService } from '../rule_status_service'; import { eqlExecutor } from './eql'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; @@ -23,7 +22,6 @@ describe('eql_executor', () => { const version = '8.0.0'; let logger: ReturnType; let alertServices: AlertServicesMock; - let ruleStatusService: Record; (getIndexVersion as jest.Mock).mockReturnValue(SIGNALS_TEMPLATE_VERSION); const eqlSO = { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', @@ -51,17 +49,11 @@ describe('eql_executor', () => { beforeEach(() => { alertServices = alertsMock.createAlertServices(); logger = loggingSystemMock.createLogger(); - ruleStatusService = { - success: jest.fn(), - find: jest.fn(), - goingToRun: jest.fn(), - error: jest.fn(), - partialFailure: jest.fn(), - }; alertServices.scopedClusterClient.asCurrentUser.transport.request.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 10 }, + events: [], }, }) ); @@ -70,25 +62,16 @@ describe('eql_executor', () => { describe('eqlExecutor', () => { it('should set a warning when exception list for eql rule contains value list exceptions', async () => { const exceptionItems = [getExceptionListItemSchemaMock({ entries: [getEntryListMock()] })]; - try { - await eqlExecutor({ - rule: eqlSO, - exceptionItems, - ruleStatusService: (ruleStatusService as unknown) as RuleStatusService, - services: alertServices, - version, - logger, - refresh: false, - searchAfterSize, - }); - } catch (err) { - // eqlExecutor will throw until we have an EQL response mock that conforms to the - // expected EQL response format, so just catch the error and check the status service - } - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( - 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' - ); + const response = await eqlExecutor({ + rule: eqlSO, + exceptionItems, + services: alertServices, + version, + logger, + searchAfterSize, + bulkCreate: jest.fn(), + }); + expect(response.warningMessages.length).toEqual(1); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index 8e2f5e92ae5028..28d1f3e19baeed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -7,9 +7,9 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { performance } from 'perf_hooks'; -import { Logger } from 'src/core/server'; import { SavedObject } from 'src/core/types'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { Logger } from 'src/core/server'; import { AlertInstanceContext, AlertInstanceState, @@ -21,13 +21,12 @@ import { isOutdated } from '../../migrations/helpers'; import { getIndexVersion } from '../../routes/index/get_index_version'; import { MIN_EQL_RULE_INDEX_VERSION } from '../../routes/index/get_signals_template'; import { EqlRuleParams } from '../../schemas/rule_schemas'; -import { RefreshTypes } from '../../types'; import { buildSignalFromEvent, buildSignalGroupFromSequence } from '../build_bulk_body'; import { getInputIndex } from '../get_input_output_index'; -import { RuleStatusService } from '../rule_status_service'; -import { bulkInsertSignals, filterDuplicateSignals } from '../single_bulk_create'; +import { filterDuplicateSignals } from '../filter_duplicate_signals'; import { AlertAttributes, + BulkCreate, EqlSignalSearchResponse, SearchAfterAndBulkCreateReturnType, WrappedSignalHit, @@ -37,26 +36,24 @@ import { createSearchAfterReturnType, makeFloatString, wrapSignal } from '../uti export const eqlExecutor = async ({ rule, exceptionItems, - ruleStatusService, services, version, - searchAfterSize, logger, - refresh, + searchAfterSize, + bulkCreate, }: { rule: SavedObject>; exceptionItems: ExceptionListItemSchema[]; - ruleStatusService: RuleStatusService; services: AlertServices; version: string; - searchAfterSize: number; logger: Logger; - refresh: RefreshTypes; + searchAfterSize: number; + bulkCreate: BulkCreate; }): Promise => { const result = createSearchAfterReturnType(); const ruleParams = rule.attributes.params; if (hasLargeValueItem(exceptionItems)) { - await ruleStatusService.partialFailure( + result.warningMessages.push( 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' ); result.warning = true; @@ -125,7 +122,7 @@ export const eqlExecutor = async ({ } if (newSignals.length > 0) { - const insertResult = await bulkInsertSignals(newSignals, logger, services, refresh); + const insertResult = await bulkCreate(newSignals); result.bulkCreateTimes.push(insertResult.bulkCreateDuration); result.createdSignalsCount += insertResult.createdItemsCount; result.createdSignals = insertResult.createdItems; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts index e157750a7d51bd..25a9d2c3f510fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts @@ -7,7 +7,6 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks'; -import { RuleStatusService } from '../rule_status_service'; import { mlExecutor } from './ml'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getMlRuleParams } from '../../schemas/rule_schemas.mock'; @@ -17,7 +16,6 @@ import { findMlSignals } from '../find_ml_signals'; import { bulkCreateMlSignals } from '../bulk_create_ml_signals'; import { mlPluginServerMock } from '../../../../../../ml/server/mocks'; import { sampleRuleSO } from '../__mocks__/es_results'; -import { getRuleStatusServiceMock } from '../rule_status_service.mock'; jest.mock('../find_ml_signals'); jest.mock('../bulk_create_ml_signals'); @@ -25,7 +23,6 @@ jest.mock('../bulk_create_ml_signals'); describe('ml_executor', () => { let jobsSummaryMock: jest.Mock; let mlMock: ReturnType; - let ruleStatusService: ReturnType; const exceptionItems = [getExceptionListItemSchemaMock()]; let logger: ReturnType; let alertServices: AlertServicesMock; @@ -45,7 +42,6 @@ describe('ml_executor', () => { mlMock.jobServiceProvider.mockReturnValue({ jobsSummary: jobsSummaryMock, }); - ruleStatusService = getRuleStatusServiceMock(); (findMlSignals as jest.Mock).mockResolvedValue({ _shards: {}, hits: { @@ -66,35 +62,32 @@ describe('ml_executor', () => { rule: mlSO, ml: undefined, exceptionItems, - ruleStatusService, services: alertServices, logger, - refresh: false, buildRuleMessage, listClient: getListClientMock(), + bulkCreate: jest.fn(), + wrapHits: jest.fn(), }) ).rejects.toThrow('ML plugin unavailable during rule execution'); }); it('should record a partial failure if Machine learning job summary was null', async () => { jobsSummaryMock.mockResolvedValue([]); - await mlExecutor({ + const response = await mlExecutor({ rule: mlSO, ml: mlMock, exceptionItems, - ruleStatusService, services: alertServices, logger, - refresh: false, buildRuleMessage, listClient: getListClientMock(), + bulkCreate: jest.fn(), + wrapHits: jest.fn(), }); expect(logger.warn).toHaveBeenCalled(); expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job(s) are not started'); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( - 'Machine learning job(s) are not started' - ); + expect(response.warningMessages.length).toEqual(1); }); it('should record a partial failure if Machine learning job was not started', async () => { @@ -106,22 +99,19 @@ describe('ml_executor', () => { }, ]); - await mlExecutor({ + const response = await mlExecutor({ rule: mlSO, ml: mlMock, exceptionItems, - ruleStatusService: (ruleStatusService as unknown) as RuleStatusService, services: alertServices, logger, - refresh: false, buildRuleMessage, listClient: getListClientMock(), + bulkCreate: jest.fn(), + wrapHits: jest.fn(), }); expect(logger.warn).toHaveBeenCalled(); expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job(s) are not started'); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( - 'Machine learning job(s) are not started' - ); + expect(response.warningMessages.length).toEqual(1); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts index 28703046289f5f..f5c7d8822b51f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts @@ -17,13 +17,11 @@ import { ListClient } from '../../../../../../lists/server'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { SetupPlugins } from '../../../../plugin'; import { MachineLearningRuleParams } from '../../schemas/rule_schemas'; -import { RefreshTypes } from '../../types'; import { bulkCreateMlSignals } from '../bulk_create_ml_signals'; import { filterEventsAgainstList } from '../filters/filter_events_against_list'; import { findMlSignals } from '../find_ml_signals'; import { BuildRuleMessage } from '../rule_messages'; -import { RuleStatusService } from '../rule_status_service'; -import { AlertAttributes } from '../types'; +import { AlertAttributes, BulkCreate, WrapHits } from '../types'; import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../utils'; export const mlExecutor = async ({ @@ -31,21 +29,21 @@ export const mlExecutor = async ({ ml, listClient, exceptionItems, - ruleStatusService, services, logger, - refresh, buildRuleMessage, + bulkCreate, + wrapHits, }: { rule: SavedObject>; ml: SetupPlugins['ml']; listClient: ListClient; exceptionItems: ExceptionListItemSchema[]; - ruleStatusService: RuleStatusService; services: AlertServices; logger: Logger; - refresh: RefreshTypes; buildRuleMessage: BuildRuleMessage; + bulkCreate: BulkCreate; + wrapHits: WrapHits; }) => { const result = createSearchAfterReturnType(); const ruleParams = rule.attributes.params; @@ -67,7 +65,7 @@ export const mlExecutor = async ({ jobSummaries.length < 1 || jobSummaries.some((job) => !isJobStarted(job.jobState, job.datafeedState)) ) { - const errorMessage = buildRuleMessage( + const warningMessage = buildRuleMessage( 'Machine learning job(s) are not started:', ...jobSummaries.map((job) => [ @@ -77,9 +75,9 @@ export const mlExecutor = async ({ ].join(', ') ) ); - logger.warn(errorMessage); + result.warningMessages.push(warningMessage); + logger.warn(warningMessage); result.warning = true; - await ruleStatusService.partialFailure(errorMessage); } const anomalyResults = await findMlSignals({ @@ -120,8 +118,9 @@ export const mlExecutor = async ({ logger, id: rule.id, signalsIndex: ruleParams.outputIndex, - refresh, buildRuleMessage, + bulkCreate, + wrapHits, }); // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } const shardFailures = diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index 05e2e3056e99eb..9d76a06afa2755 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -14,11 +14,10 @@ import { AlertServices, } from '../../../../../../alerting/server'; import { ListClient } from '../../../../../../lists/server'; -import { RefreshTypes } from '../../types'; import { getFilter } from '../get_filter'; import { getInputIndex } from '../get_input_output_index'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; -import { AlertAttributes, RuleRangeTuple } from '../types'; +import { AlertAttributes, RuleRangeTuple, BulkCreate, WrapHits } from '../types'; import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas'; @@ -32,9 +31,10 @@ export const queryExecutor = async ({ version, searchAfterSize, logger, - refresh, eventsTelemetry, buildRuleMessage, + bulkCreate, + wrapHits, }: { rule: SavedObject>; tuples: RuleRangeTuple[]; @@ -44,9 +44,10 @@ export const queryExecutor = async ({ version: string; searchAfterSize: number; logger: Logger; - refresh: RefreshTypes; eventsTelemetry: TelemetryEventsSender | undefined; buildRuleMessage: BuildRuleMessage; + bulkCreate: BulkCreate; + wrapHits: WrapHits; }) => { const ruleParams = rule.attributes.params; const inputIndex = await getInputIndex(services, version, ruleParams.index); @@ -74,7 +75,8 @@ export const queryExecutor = async ({ signalsIndex: ruleParams.outputIndex, filter: esFilter, pageSize: searchAfterSize, - refresh, buildRuleMessage, + bulkCreate, + wrapHits, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts index 10b4ce939ca3ac..078eb8362069cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts @@ -14,9 +14,8 @@ import { AlertServices, } from '../../../../../../alerting/server'; import { ListClient } from '../../../../../../lists/server'; -import { RefreshTypes } from '../../types'; import { getInputIndex } from '../get_input_output_index'; -import { RuleRangeTuple, AlertAttributes } from '../types'; +import { RuleRangeTuple, AlertAttributes, BulkCreate, WrapHits } from '../types'; import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { createThreatSignals } from '../threat_mapping/create_threat_signals'; @@ -31,9 +30,10 @@ export const threatMatchExecutor = async ({ version, searchAfterSize, logger, - refresh, eventsTelemetry, buildRuleMessage, + bulkCreate, + wrapHits, }: { rule: SavedObject>; tuples: RuleRangeTuple[]; @@ -43,9 +43,10 @@ export const threatMatchExecutor = async ({ version: string; searchAfterSize: number; logger: Logger; - refresh: RefreshTypes; eventsTelemetry: TelemetryEventsSender | undefined; buildRuleMessage: BuildRuleMessage; + bulkCreate: BulkCreate; + wrapHits: WrapHits; }) => { const ruleParams = rule.attributes.params; const inputIndex = await getInputIndex(services, version, ruleParams.index); @@ -67,7 +68,6 @@ export const threatMatchExecutor = async ({ outputIndex: ruleParams.outputIndex, ruleSO: rule, searchAfterSize, - refresh, threatFilters: ruleParams.threatFilters ?? [], threatQuery: ruleParams.threatQuery, threatLanguage: ruleParams.threatLanguage, @@ -76,5 +76,7 @@ export const threatMatchExecutor = async ({ threatIndicatorPath: ruleParams.threatIndicatorPath, concurrentSearches: ruleParams.concurrentSearches ?? 1, itemsPerSearch: ruleParams.itemsPerSearch ?? 9000, + bulkCreate, + wrapHits, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index 5d62b28b73ae87..f03e8b8a147aea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -7,7 +7,6 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks'; -import { RuleStatusService } from '../rule_status_service'; import { thresholdExecutor } from './threshold'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; @@ -18,7 +17,6 @@ describe('threshold_executor', () => { const version = '8.0.0'; let logger: ReturnType; let alertServices: AlertServicesMock; - let ruleStatusService: Record; const thresholdSO = { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', type: 'alert', @@ -50,34 +48,24 @@ describe('threshold_executor', () => { beforeEach(() => { alertServices = alertsMock.createAlertServices(); logger = loggingSystemMock.createLogger(); - ruleStatusService = { - success: jest.fn(), - find: jest.fn(), - goingToRun: jest.fn(), - error: jest.fn(), - partialFailure: jest.fn(), - }; }); describe('thresholdExecutor', () => { it('should set a warning when exception list for threshold rule contains value list exceptions', async () => { const exceptionItems = [getExceptionListItemSchemaMock({ entries: [getEntryListMock()] })]; - await thresholdExecutor({ + const response = await thresholdExecutor({ rule: thresholdSO, tuples: [], exceptionItems, - ruleStatusService: (ruleStatusService as unknown) as RuleStatusService, services: alertServices, version, logger, - refresh: false, buildRuleMessage, startedAt: new Date(), + bulkCreate: jest.fn(), + wrapHits: jest.fn(), }); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( - 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' - ); + expect(response.warningMessages.length).toEqual(1); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index fa0986044e2502..5e23128c9c148a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -15,51 +15,55 @@ import { } from '../../../../../../alerting/server'; import { hasLargeValueItem } from '../../../../../common/detection_engine/utils'; import { ThresholdRuleParams } from '../../schemas/rule_schemas'; -import { RefreshTypes } from '../../types'; import { getFilter } from '../get_filter'; import { getInputIndex } from '../get_input_output_index'; -import { BuildRuleMessage } from '../rule_messages'; -import { RuleStatusService } from '../rule_status_service'; import { bulkCreateThresholdSignals, findThresholdSignals, getThresholdBucketFilters, getThresholdSignalHistory, } from '../threshold'; -import { AlertAttributes, RuleRangeTuple, SearchAfterAndBulkCreateReturnType } from '../types'; +import { + AlertAttributes, + BulkCreate, + RuleRangeTuple, + SearchAfterAndBulkCreateReturnType, + WrapHits, +} from '../types'; import { createSearchAfterReturnType, createSearchAfterReturnTypeFromResponse, mergeReturns, } from '../utils'; +import { BuildRuleMessage } from '../rule_messages'; export const thresholdExecutor = async ({ rule, tuples, exceptionItems, - ruleStatusService, services, version, logger, - refresh, buildRuleMessage, startedAt, + bulkCreate, + wrapHits, }: { rule: SavedObject>; tuples: RuleRangeTuple[]; exceptionItems: ExceptionListItemSchema[]; - ruleStatusService: RuleStatusService; services: AlertServices; version: string; logger: Logger; - refresh: RefreshTypes; buildRuleMessage: BuildRuleMessage; startedAt: Date; + bulkCreate: BulkCreate; + wrapHits: WrapHits; }): Promise => { let result = createSearchAfterReturnType(); const ruleParams = rule.attributes.params; if (hasLargeValueItem(exceptionItems)) { - await ruleStatusService.partialFailure( + result.warningMessages.push( 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' ); result.warning = true; @@ -126,14 +130,13 @@ export const thresholdExecutor = async ({ filter: esFilter, services, logger, - id: rule.id, inputIndexPattern: inputIndex, signalsIndex: ruleParams.outputIndex, startedAt, from: tuple.from.toDate(), - refresh, thresholdSignalHistory, - buildRuleMessage, + bulkCreate, + wrapHits, }); result = mergeReturns([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts new file mode 100644 index 00000000000000..5c4af83c3b03e3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { filterDuplicateSignals } from './filter_duplicate_signals'; +import { sampleWrappedSignalHit } from './__mocks__/es_results'; + +const mockRuleId1 = 'aaaaaaaa'; +const mockRuleId2 = 'bbbbbbbb'; +const mockRuleId3 = 'cccccccc'; + +const createWrappedSignalHitWithRuleId = (ruleId: string) => { + const mockSignal = sampleWrappedSignalHit(); + return { + ...mockSignal, + _source: { + ...mockSignal._source, + signal: { + ...mockSignal._source.signal, + ancestors: [ + { + ...mockSignal._source.signal.ancestors[0], + rule: ruleId, + }, + ], + }, + }, + }; +}; +const mockSignals = [ + createWrappedSignalHitWithRuleId(mockRuleId1), + createWrappedSignalHitWithRuleId(mockRuleId2), +]; + +describe('filterDuplicateSignals', () => { + it('filters duplicate signals', () => { + expect(filterDuplicateSignals(mockRuleId1, mockSignals).length).toEqual(1); + }); + + it('does not filter non-duplicate signals', () => { + expect(filterDuplicateSignals(mockRuleId3, mockSignals).length).toEqual(2); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts new file mode 100644 index 00000000000000..a648c053062894 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { WrappedSignalHit } from './types'; + +export const filterDuplicateSignals = (ruleId: string, signals: WrappedSignalHit[]) => { + return signals.filter( + (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 52c887c3ca55af..e4eb7e854f670f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -16,29 +16,28 @@ import { sampleDocWithSortId, } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; -import { buildRuleMessageFactory } from './rule_messages'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import uuid from 'uuid'; import { listMock } from '../../../../../lists/server/mocks'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { BulkResponse, RuleRangeTuple } from './types'; +import { BulkCreate, BulkResponse, RuleRangeTuple, WrapHits } from './types'; import type { SearchListItemArraySchema } from '@kbn/securitysolution-io-ts-list-types'; import { getSearchListItemResponseMock } from '../../../../../lists/common/schemas/response/search_list_item_schema.mock'; import { getRuleRangeTuples } from './utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { bulkCreateFactory } from './bulk_create_factory'; +import { wrapHitsFactory } from './wrap_hits_factory'; +import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock'; -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); +const buildRuleMessage = mockBuildRuleMessage; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; + let bulkCreate: BulkCreate; + let wrapHits: WrapHits; let inputIndexPattern: string[] = []; let listClient = listMock.getListClient(); const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); @@ -61,6 +60,13 @@ describe('searchAfterAndBulkCreate', () => { maxSignals: sampleParams.maxSignals, buildRuleMessage, })); + bulkCreate = bulkCreateFactory( + mockLogger, + mockService.scopedClusterClient.asCurrentUser, + buildRuleMessage, + false + ); + wrapHits = wrapHitsFactory({ ruleSO, signalsIndex: DEFAULT_SIGNALS_INDEX }); }); test('should return success with number of searches less than max signals', async () => { @@ -166,6 +172,7 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ tuples, ruleSO, @@ -179,8 +186,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(5); @@ -282,8 +290,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(4); @@ -359,8 +368,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); @@ -417,8 +427,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); @@ -495,8 +506,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); @@ -549,8 +561,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(1); @@ -625,18 +638,14 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); - // I don't like testing log statements since logs change but this is the best - // way I can think of to ensure this section is getting hit with this test case. - expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[14][0]).toContain( - 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' - ); }); test('should return success when no exceptions list provided', async () => { @@ -703,8 +712,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); @@ -746,8 +756,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); @@ -792,8 +803,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(0); @@ -852,8 +864,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(0); // should not create signals if search threw error @@ -977,8 +990,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(success).toEqual(false); expect(errors).toEqual(['error on creation']); @@ -1072,8 +1086,9 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, - refresh: false, buildRuleMessage, + bulkCreate, + wrapHits, }); expect(mockEnrichment).toHaveBeenCalledWith( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index b0dcc1810a6396..bb2e57b0606e59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -8,7 +8,6 @@ import { identity } from 'lodash'; import type { estypes } from '@elastic/elasticsearch'; import { singleSearchAfter } from './single_search_after'; -import { singleBulkCreate } from './single_bulk_create'; import { filterEventsAgainstList } from './filters/filter_events_against_list'; import { sendAlertTelemetryEvents } from './send_telemetry_events'; import { @@ -31,14 +30,13 @@ export const searchAfterAndBulkCreate = async ({ listClient, logger, eventsTelemetry, - id, inputIndexPattern, - signalsIndex, filter, pageSize, - refresh, buildRuleMessage, enrichment = identity, + bulkCreate, + wrapHits, }: SearchAfterAndBulkCreateParams): Promise => { const ruleParams = ruleSO.attributes.params; let toReturn = createSearchAfterReturnType(); @@ -149,6 +147,7 @@ export const searchAfterAndBulkCreate = async ({ ); } const enrichedEvents = await enrichment(filteredEvents); + const wrappedDocs = wrapHits(enrichedEvents.hits.hits); const { bulkCreateDuration: bulkDuration, @@ -156,16 +155,7 @@ export const searchAfterAndBulkCreate = async ({ createdItems, success: bulkSuccess, errors: bulkErrors, - } = await singleBulkCreate({ - buildRuleMessage, - filteredEvents: enrichedEvents, - ruleSO, - services, - logger, - id, - signalsIndex, - refresh, - }); + } = await bulkCreate(wrappedDocs); toReturn = mergeReturns([ toReturn, createSearchAfterReturnType({ @@ -180,10 +170,10 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(buildRuleMessage(`created ${createdCount} signals`)); logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`)); logger.debug( - buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) + buildRuleMessage(`enrichedEvents.hits.hits: ${enrichedEvents.hits.hits.length}`) ); - sendAlertTelemetryEvents(logger, eventsTelemetry, filteredEvents, buildRuleMessage); + sendAlertTelemetryEvents(logger, eventsTelemetry, enrichedEvents, buildRuleMessage); } if (!hasSortId) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 823d694f36514d..d8c919b50e9db0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -274,35 +274,6 @@ describe('signal_rule_alert_type', () => { expect(ruleStatusService.error).toHaveBeenCalledTimes(0); }); - it("should set refresh to 'wait_for' when actions are present", async () => { - const ruleAlert = getAlertMock(getQueryRuleParams()); - ruleAlert.actions = [ - { - actionTypeId: '.slack', - params: { - message: - 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', - }, - group: 'default', - id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', - }, - ]; - - alertServices.savedObjectsClient.get.mockResolvedValue({ - id: 'id', - type: 'type', - references: [], - attributes: ruleAlert, - }); - await alert.executor(payload); - expect((queryExecutor as jest.Mock).mock.calls[0][0].refresh).toEqual('wait_for'); - }); - - it('should set refresh to false when actions are not present', async () => { - await alert.executor(payload); - expect((queryExecutor as jest.Mock).mock.calls[0][0].refresh).toEqual(false); - }); - it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => { const ruleAlert = getAlertMock(getQueryRuleParams()); ruleAlert.actions = [ @@ -462,6 +433,7 @@ describe('signal_rule_alert_type', () => { lastLookBackDate: null, createdSignalsCount: 0, createdSignals: [], + warningMessages: [], errors: ['Error that bubbled up.'], }; (queryExecutor as jest.Mock).mockResolvedValue(result); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 13a63df6ed8b61..0a2e22bc44b60e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -65,6 +65,8 @@ import { RuleParams, savedQueryRuleParams, } from '../schemas/rule_schemas'; +import { bulkCreateFactory } from './bulk_create_factory'; +import { wrapHitsFactory } from './wrap_hits_factory'; export const signalRulesAlertType = ({ logger, @@ -218,6 +220,19 @@ export const signalRulesAlertType = ({ client: exceptionsClient, lists: params.exceptionsList ?? [], }); + + const bulkCreate = bulkCreateFactory( + logger, + services.scopedClusterClient.asCurrentUser, + buildRuleMessage, + refresh + ); + + const wrapHits = wrapHitsFactory({ + ruleSO: savedObject, + signalsIndex: params.outputIndex, + }); + if (isMlRule(type)) { const mlRuleSO = asTypeSpecificSO(savedObject, machineLearningRuleParams); result = await mlExecutor({ @@ -225,11 +240,11 @@ export const signalRulesAlertType = ({ ml, listClient, exceptionItems, - ruleStatusService, services, logger, - refresh, buildRuleMessage, + bulkCreate, + wrapHits, }); } else if (isThresholdRule(type)) { const thresholdRuleSO = asTypeSpecificSO(savedObject, thresholdRuleParams); @@ -237,13 +252,13 @@ export const signalRulesAlertType = ({ rule: thresholdRuleSO, tuples, exceptionItems, - ruleStatusService, services, version, logger, - refresh, buildRuleMessage, startedAt, + bulkCreate, + wrapHits, }); } else if (isThreatMatchRule(type)) { const threatRuleSO = asTypeSpecificSO(savedObject, threatRuleParams); @@ -256,9 +271,10 @@ export const signalRulesAlertType = ({ version, searchAfterSize, logger, - refresh, eventsTelemetry, buildRuleMessage, + bulkCreate, + wrapHits, }); } else if (isQueryRule(type)) { const queryRuleSO = validateQueryRuleTypes(savedObject); @@ -271,25 +287,30 @@ export const signalRulesAlertType = ({ version, searchAfterSize, logger, - refresh, eventsTelemetry, buildRuleMessage, + bulkCreate, + wrapHits, }); } else if (isEqlRule(type)) { const eqlRuleSO = asTypeSpecificSO(savedObject, eqlRuleParams); result = await eqlExecutor({ rule: eqlRuleSO, exceptionItems, - ruleStatusService, services, version, searchAfterSize, + bulkCreate, logger, - refresh, }); } else { throw new Error(`unknown rule type ${type}`); } + if (result.warningMessages.length) { + const warningMessage = buildRuleMessage(result.warningMessages.join()); + await ruleStatusService.partialFailure(warningMessage); + } + if (result.success) { if (actions.length) { const notificationRuleParams: NotificationRuleTypeParams = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts deleted file mode 100644 index 3fbb8c1a607e91..00000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ /dev/null @@ -1,318 +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 { generateId } from './utils'; -import { - sampleDocSearchResultsNoSortId, - mockLogger, - sampleRuleGuid, - sampleDocSearchResultsNoSortIdNoVersion, - sampleEmptyDocSearchResults, - sampleBulkCreateDuplicateResult, - sampleBulkCreateErrorResult, - sampleDocWithAncestors, - sampleRuleSO, -} from './__mocks__/es_results'; -import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; -import { buildRuleMessageFactory } from './rule_messages'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; - -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); -describe('singleBulkCreate', () => { - const mockService: AlertServicesMock = alertsMock.createAlertServices(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('create signal id gereateId', () => { - test('two docs with same index, id, and version should have same id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid, version, ruleId); - expect(firstHash).toEqual(generatedHash); - expect(secondHash).toEqual(generatedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - }); - test('two docs with different index, id, and version should have different id', () => { - const findex = 'myfakeindex'; - const findex2 = 'mysecondfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // 'mysecondfakeindexsomefakeid1rule-1' - const secondGeneratedHash = - 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex2, fid, version, ruleId); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - test('two docs with same index, different id, and same version should have different id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const fid2 = 'somefakeid2'; - const version = '1'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // 'myfakeindexsomefakeid21rule-1' - const secondGeneratedHash = - '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid2, version, ruleId); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - test('two docs with same index, same id, and different version should have different id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const version2 = '2'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // myfakeindexsomefakeid2rule-1' - const secondGeneratedHash = - 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid, version2, ruleId); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { - const longIndexName = - 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - const firstHash = generateId(longIndexName, fid, version, ruleId); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - }); - test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - const ruleId2 = 'rule-2'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // myfakeindexsomefakeid1rule-2' - const secondGeneratedHash = - '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid, version, ruleId2); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - }); - - test('create successful bulk create', async () => { - const ruleSO = sampleRuleSO(getQueryRuleParams()); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( - // @ts-expect-error not compatible response interface - elasticsearchClientMock.createSuccessTransportRequestPromise({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - ); - const { success, createdItemsCount } = await singleBulkCreate({ - filteredEvents: sampleDocSearchResultsNoSortId(), - ruleSO, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - refresh: false, - buildRuleMessage, - }); - expect(success).toEqual(true); - expect(createdItemsCount).toEqual(0); - }); - - test('create successful bulk create with docs with no versioning', async () => { - const ruleSO = sampleRuleSO(getQueryRuleParams()); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( - // @ts-expect-error not compatible response interface - elasticsearchClientMock.createSuccessTransportRequestPromise({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - ); - const { success, createdItemsCount } = await singleBulkCreate({ - filteredEvents: sampleDocSearchResultsNoSortIdNoVersion(), - ruleSO, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - refresh: false, - buildRuleMessage, - }); - expect(success).toEqual(true); - expect(createdItemsCount).toEqual(0); - }); - - test('create unsuccessful bulk create due to empty search results', async () => { - const ruleSO = sampleRuleSO(getQueryRuleParams()); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( - // @ts-expect-error not full response interface - elasticsearchClientMock.createSuccessTransportRequestPromise(false) - ); - const { success, createdItemsCount } = await singleBulkCreate({ - filteredEvents: sampleEmptyDocSearchResults(), - ruleSO, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - refresh: false, - buildRuleMessage, - }); - expect(success).toEqual(true); - expect(createdItemsCount).toEqual(0); - }); - - test('create successful bulk create when bulk create has duplicate errors', async () => { - const ruleSO = sampleRuleSO(getQueryRuleParams()); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateDuplicateResult) - ); - const { success, createdItemsCount } = await singleBulkCreate({ - filteredEvents: sampleDocSearchResultsNoSortId(), - ruleSO, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - refresh: false, - buildRuleMessage, - }); - - expect(mockLogger.error).not.toHaveBeenCalled(); - expect(success).toEqual(true); - expect(createdItemsCount).toEqual(1); - }); - - test('create failed bulk create when bulk create has multiple error statuses', async () => { - const ruleSO = sampleRuleSO(getQueryRuleParams()); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateErrorResult) - ); - const { success, createdItemsCount, errors } = await singleBulkCreate({ - filteredEvents: sampleDocSearchResultsNoSortId(), - ruleSO, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - refresh: false, - buildRuleMessage, - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(errors).toEqual(['[4]: internal server error']); - expect(success).toEqual(false); - expect(createdItemsCount).toEqual(1); - }); - - test('filter duplicate rules will return an empty array given an empty array', () => { - const filtered = filterDuplicateRules( - '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - sampleEmptyDocSearchResults() - ); - expect(filtered).toEqual([]); - }); - - test('filter duplicate rules will return nothing filtered when the two rule ids do not match with each other', () => { - const filtered = filterDuplicateRules('some id', sampleDocWithAncestors()); - expect(filtered).toEqual(sampleDocWithAncestors().hits.hits); - }); - - test('filters duplicate rules will return empty array when the two rule ids match each other', () => { - const filtered = filterDuplicateRules( - '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - sampleDocWithAncestors() - ); - expect(filtered).toEqual([]); - }); - - test('filter duplicate rules will return back search responses if they do not have a signal and will NOT filter the source out', () => { - const ancestors = sampleDocSearchResultsNoSortId(); - const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', ancestors); - expect(filtered).toEqual(ancestors.hits.hits); - }); - - test('filter duplicate rules does not attempt filters when the signal is not an event type of signal but rather a "clash" from the source index having its own numeric signal type', () => { - const doc = { ...sampleDocWithAncestors(), _source: { signal: 1234 } }; - const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', doc); - expect(filtered).toEqual([]); - }); - - test('filter duplicate rules does not attempt filters when the signal is not an event type of signal but rather a "clash" from the source index having its own object signal type', () => { - const doc = { ...sampleDocWithAncestors(), _source: { signal: {} } }; - const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', doc); - expect(filtered).toEqual([]); - }); - - test('create successful and returns proper createdItemsCount', async () => { - const ruleSO = sampleRuleSO(getQueryRuleParams()); - mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateDuplicateResult) - ); - const { success, createdItemsCount } = await singleBulkCreate({ - filteredEvents: sampleDocSearchResultsNoSortId(), - ruleSO, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - refresh: false, - buildRuleMessage, - }); - expect(success).toEqual(true); - expect(createdItemsCount).toEqual(1); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts deleted file mode 100644 index 92d01fef6e50c3..00000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { countBy, isEmpty, get } from 'lodash'; -import { performance } from 'perf_hooks'; -import { - AlertInstanceContext, - AlertInstanceState, - AlertServices, -} from '../../../../../alerting/server'; -import { AlertAttributes, SignalHit, SignalSearchResponse, WrappedSignalHit } from './types'; -import { RefreshTypes } from '../types'; -import { generateId, makeFloatString, errorAggregator } from './utils'; -import { buildBulkBody } from './build_bulk_body'; -import { BuildRuleMessage } from './rule_messages'; -import { Logger, SavedObject } from '../../../../../../../src/core/server'; -import { isEventTypeSignal } from './build_event_type_signal'; - -interface SingleBulkCreateParams { - filteredEvents: SignalSearchResponse; - ruleSO: SavedObject; - services: AlertServices; - logger: Logger; - id: string; - signalsIndex: string; - refresh: RefreshTypes; - buildRuleMessage: BuildRuleMessage; -} - -/** - * This is for signals on signals to work correctly. If given a rule id this will check if - * that rule id already exists in the ancestor tree of each signal search response and remove - * those documents so they cannot be created as a signal since we do not want a rule id to - * ever be capable of re-writing the same signal continuously if both the _input_ and _output_ - * of the signals index happens to be the same index. - * @param ruleId The rule id - * @param signalSearchResponse The search response that has all the documents - */ -export const filterDuplicateRules = ( - ruleId: string, - signalSearchResponse: SignalSearchResponse -) => { - return signalSearchResponse.hits.hits.filter((doc) => { - if (doc._source?.signal == null || !isEventTypeSignal(doc)) { - return true; - } else { - return !( - doc._source?.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) || - doc._source?.signal.rule.id === ruleId - ); - } - }); -}; - -/** - * Similar to filterDuplicateRules, but operates on candidate signal documents rather than events that matched - * the detection query. This means we only have to compare the ruleId against the ancestors array. - * @param ruleId The rule id - * @param signals The candidate new signals - */ -export const filterDuplicateSignals = (ruleId: string, signals: WrappedSignalHit[]) => { - return signals.filter( - (doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId) - ); -}; - -export interface SingleBulkCreateResponse { - success: boolean; - bulkCreateDuration?: string; - createdItemsCount: number; - createdItems: SignalHit[]; - errors: string[]; -} - -export interface BulkInsertSignalsResponse { - bulkCreateDuration: string; - createdItemsCount: number; - createdItems: SignalHit[]; -} - -// Bulk Index documents. -export const singleBulkCreate = async ({ - buildRuleMessage, - filteredEvents, - ruleSO, - services, - logger, - id, - signalsIndex, - refresh, -}: SingleBulkCreateParams): Promise => { - const ruleParams = ruleSO.attributes.params; - filteredEvents.hits.hits = filterDuplicateRules(id, filteredEvents); - logger.debug(buildRuleMessage(`about to bulk create ${filteredEvents.hits.hits.length} events`)); - if (filteredEvents.hits.hits.length === 0) { - logger.debug(buildRuleMessage(`all events were duplicates`)); - return { success: true, createdItemsCount: 0, createdItems: [], errors: [] }; - } - // index documents after creating an ID based on the - // source documents' originating index, and the original - // document _id. This will allow two documents from two - // different indexes with the same ID to be - // indexed, and prevents us from creating any updates - // to the documents once inserted into the signals index, - // while preventing duplicates from being added to the - // signals index if rules are re-run over the same time - // span. Also allow for versioning. - const bulkBody = filteredEvents.hits.hits.flatMap((doc) => [ - { - create: { - _index: signalsIndex, - _id: generateId( - doc._index, - doc._id, - doc._version ? doc._version.toString() : '', - ruleParams.ruleId ?? '' - ), - }, - }, - buildBulkBody(ruleSO, doc), - ]); - const start = performance.now(); - const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ - index: signalsIndex, - refresh, - body: bulkBody, - }); - const end = performance.now(); - logger.debug( - buildRuleMessage( - `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` - ) - ); - logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`)); - const createdItems = filteredEvents.hits.hits - .map((doc, index) => ({ - _id: response.items[index].create?._id ?? '', - _index: response.items[index].create?._index ?? '', - ...buildBulkBody(ruleSO, doc), - })) - .filter((_, index) => get(response.items[index], 'create.status') === 201); - const createdItemsCount = createdItems.length; - const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; - const errorCountByMessage = errorAggregator(response, [409]); - - logger.debug(buildRuleMessage(`bulk created ${createdItemsCount} signals`)); - if (duplicateSignalsCount > 0) { - logger.debug(buildRuleMessage(`ignored ${duplicateSignalsCount} duplicate signals`)); - } - - if (!isEmpty(errorCountByMessage)) { - logger.error( - buildRuleMessage( - `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` - ) - ); - return { - errors: Object.keys(errorCountByMessage), - success: false, - bulkCreateDuration: makeFloatString(end - start), - createdItemsCount, - createdItems, - }; - } else { - return { - errors: [], - success: true, - bulkCreateDuration: makeFloatString(end - start), - createdItemsCount, - createdItems, - }; - } -}; - -// Bulk Index new signals. -export const bulkInsertSignals = async ( - signals: WrappedSignalHit[], - logger: Logger, - services: AlertServices, - refresh: RefreshTypes -): Promise => { - // index documents after creating an ID based on the - // id and index of each parent and the rule ID - const bulkBody = signals.flatMap((doc) => [ - { - create: { - _index: doc._index, - _id: doc._id, - }, - }, - doc._source, - ]); - const start = performance.now(); - const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ - refresh, - body: bulkBody, - }); - const end = performance.now(); - logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`); - logger.debug(`took property says bulk took: ${response.took} milliseconds`); - - if (response.errors) { - const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; - logger.debug(`ignored ${duplicateSignalsCount} duplicate signals`); - const errorCountByMessage = errorAggregator(response, [409]); - if (!isEmpty(errorCountByMessage)) { - logger.error( - `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` - ); - } - } - - const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0; - const createdItems = signals - .map((doc, index) => ({ - ...doc._source, - _id: response.items[index].create?._id ?? '', - _index: response.items[index].create?._index ?? '', - })) - .filter((_, index) => get(response.items[index], 'create.status') === 201); - logger.debug(`bulk created ${createdItemsCount} signals`); - return { bulkCreateDuration: makeFloatString(end - start), createdItems, createdItemsCount }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 37b0b88d88edab..3e30a08f1ae69c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -31,10 +31,11 @@ export const createThreatSignal = async ({ outputIndex, ruleSO, searchAfterSize, - refresh, buildRuleMessage, currentThreatList, currentResult, + bulkCreate, + wrapHits, }: CreateThreatSignalOptions): Promise => { const threatFilter = buildThreatMappingFilter({ threatMapping, @@ -81,9 +82,10 @@ export const createThreatSignal = async ({ signalsIndex: outputIndex, filter: esFilter, pageSize: searchAfterSize, - refresh, buildRuleMessage, enrichment: threatEnrichment, + bulkCreate, + wrapHits, }); logger.debug( buildRuleMessage( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index b3e0e376c7794a..5054ab1b2cca50 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -32,7 +32,6 @@ export const createThreatSignals = async ({ outputIndex, ruleSO, searchAfterSize, - refresh, threatFilters, threatQuery, threatLanguage, @@ -41,6 +40,8 @@ export const createThreatSignals = async ({ threatIndicatorPath, concurrentSearches, itemsPerSearch, + bulkCreate, + wrapHits, }: CreateThreatSignalsOptions): Promise => { const params = ruleSO.attributes.params; logger.debug(buildRuleMessage('Indicator matching rule starting')); @@ -55,6 +56,7 @@ export const createThreatSignals = async ({ createdSignalsCount: 0, createdSignals: [], errors: [], + warningMessages: [], }; let threatListCount = await getThreatListCount({ @@ -120,10 +122,11 @@ export const createThreatSignals = async ({ outputIndex, ruleSO, searchAfterSize, - refresh, buildRuleMessage, currentThreatList: slicedChunk, currentResult: results, + bulkCreate, + wrapHits, }) ); const searchesPerformed = await Promise.all(concurrentSearchesPerformed); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index acb64f826f3f24..34b064b0f88053 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -29,9 +29,11 @@ import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { AlertAttributes, + BulkCreate, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, SignalsEnrichment, + WrapHits, } from '../types'; import { ThreatRuleParams } from '../../schemas/rule_schemas'; @@ -55,7 +57,6 @@ export interface CreateThreatSignalsOptions { outputIndex: string; ruleSO: SavedObject>; searchAfterSize: number; - refresh: false | 'wait_for'; threatFilters: unknown[]; threatQuery: ThreatQuery; buildRuleMessage: BuildRuleMessage; @@ -64,6 +65,8 @@ export interface CreateThreatSignalsOptions { threatLanguage: ThreatLanguageOrUndefined; concurrentSearches: ConcurrentSearches; itemsPerSearch: ItemsPerSearch; + bulkCreate: BulkCreate; + wrapHits: WrapHits; } export interface CreateThreatSignalOptions { @@ -85,10 +88,11 @@ export interface CreateThreatSignalOptions { outputIndex: string; ruleSO: SavedObject>; searchAfterSize: number; - refresh: false | 'wait_for'; buildRuleMessage: BuildRuleMessage; currentThreatList: ThreatListItem[]; currentResult: SearchAfterAndBulkCreateReturnType; + bulkCreate: BulkCreate; + wrapHits: WrapHits; } export interface BuildThreatMappingFilterOptions { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 6c6447bad09750..ec826b44023f63 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -58,6 +58,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -69,6 +70,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineResults(existingResult, newResult); expect(combinedResults.success).toEqual(true); @@ -84,6 +86,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -95,6 +98,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineResults(existingResult, newResult); expect(combinedResults.success).toEqual(false); @@ -110,6 +114,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -121,6 +126,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineResults(existingResult, newResult); expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z'); @@ -136,6 +142,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -147,6 +154,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineResults(existingResult, newResult); expect(combinedResults).toEqual( @@ -167,6 +175,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: ['error 1', 'error 2', 'error 3'], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -178,6 +187,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: ['error 4', 'error 1', 'error 3', 'error 5'], + warningMessages: [], }; const combinedResults = combineResults(existingResult, newResult); expect(combinedResults).toEqual( @@ -289,6 +299,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const expectedResult: SearchAfterAndBulkCreateReturnType = { success: true, @@ -299,6 +310,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, []); expect(combinedResults).toEqual(expectedResult); @@ -314,6 +326,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { success: true, @@ -324,6 +337,7 @@ describe('utils', () => { createdSignalsCount: 0, createdSignals: [], errors: [], + warningMessages: [], }; const expectedResult: SearchAfterAndBulkCreateReturnType = { success: true, @@ -334,6 +348,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult]); @@ -350,6 +365,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult1: SearchAfterAndBulkCreateReturnType = { success: true, @@ -360,6 +376,7 @@ describe('utils', () => { createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult2: SearchAfterAndBulkCreateReturnType = { success: true, @@ -370,6 +387,7 @@ describe('utils', () => { createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const expectedResult: SearchAfterAndBulkCreateReturnType = { @@ -381,6 +399,7 @@ describe('utils', () => { createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult1, newResult2]); @@ -397,6 +416,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult1: SearchAfterAndBulkCreateReturnType = { success: true, @@ -407,6 +427,7 @@ describe('utils', () => { createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult2: SearchAfterAndBulkCreateReturnType = { success: true, @@ -417,6 +438,7 @@ describe('utils', () => { createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const expectedResult: SearchAfterAndBulkCreateReturnType = { @@ -428,6 +450,7 @@ describe('utils', () => { createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult2, newResult1]); // two array elements are flipped @@ -444,6 +467,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult1: SearchAfterAndBulkCreateReturnType = { success: true, @@ -454,6 +478,7 @@ describe('utils', () => { createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult2: SearchAfterAndBulkCreateReturnType = { success: true, @@ -464,6 +489,7 @@ describe('utils', () => { createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const expectedResult: SearchAfterAndBulkCreateReturnType = { @@ -475,6 +501,7 @@ describe('utils', () => { createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult1, newResult2]); @@ -491,6 +518,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -502,6 +530,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult]); expect(combinedResults.success).toEqual(true); @@ -517,6 +546,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -528,6 +558,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult]); expect(combinedResults.success).toEqual(false); @@ -543,6 +574,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -554,6 +586,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult]); expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z'); @@ -569,6 +602,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -580,6 +614,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: [], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult]); expect(combinedResults).toEqual( @@ -600,6 +635,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: ['error 1', 'error 2', 'error 3'], + warningMessages: [], }; const newResult: SearchAfterAndBulkCreateReturnType = { @@ -611,6 +647,7 @@ describe('utils', () => { createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: ['error 4', 'error 1', 'error 3', 'error 5'], + warningMessages: [], }; const combinedResults = combineConcurrentResults(existingResult, [newResult]); expect(combinedResults).toEqual( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index 47a32915dd83f4..4d9fda43f032e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -75,6 +75,7 @@ export const combineResults = ( lastLookBackDate: newResult.lastLookBackDate, createdSignalsCount: currentResult.createdSignalsCount + newResult.createdSignalsCount, createdSignals: [...currentResult.createdSignals, ...newResult.createdSignals], + warningMessages: [...currentResult.warningMessages, ...newResult.warningMessages], errors: [...new Set([...currentResult.errors, ...newResult.errors])], }); @@ -100,6 +101,7 @@ export const combineConcurrentResults = ( lastLookBackDate, createdSignalsCount: accum.createdSignalsCount + item.createdSignalsCount, createdSignals: [...accum.createdSignals, ...item.createdSignals], + warningMessages: [...accum.warningMessages, ...item.warningMessages], errors: [...new Set([...accum.errors, ...item.errors])], }; }, @@ -112,6 +114,7 @@ export const combineConcurrentResults = ( createdSignalsCount: 0, createdSignals: [], errors: [], + warningMessages: [], } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index 197065f205fc5a..08fa2f14a0fd5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -19,20 +19,20 @@ import { } from '../../../../../../alerting/server'; import { BaseHit } from '../../../../../common/detection_engine/types'; import { TermAggregationBucket } from '../../../types'; -import { RefreshTypes } from '../../types'; -import { singleBulkCreate, SingleBulkCreateResponse } from '../single_bulk_create'; +import { GenericBulkCreateResponse } from '../bulk_create_factory'; import { calculateThresholdSignalUuid, getThresholdAggregationParts, getThresholdTermsHash, } from '../utils'; -import { BuildRuleMessage } from '../rule_messages'; import type { MultiAggBucket, SignalSource, SignalSearchResponse, ThresholdSignalHistory, AlertAttributes, + BulkCreate, + WrapHits, } from '../types'; import { ThresholdRuleParams } from '../../schemas/rule_schemas'; @@ -42,14 +42,13 @@ interface BulkCreateThresholdSignalsParams { services: AlertServices; inputIndexPattern: string[]; logger: Logger; - id: string; filter: unknown; signalsIndex: string; - refresh: RefreshTypes; startedAt: Date; from: Date; thresholdSignalHistory: ThresholdSignalHistory; - buildRuleMessage: BuildRuleMessage; + bulkCreate: BulkCreate; + wrapHits: WrapHits; } const getTransformedHits = ( @@ -76,7 +75,7 @@ const getTransformedHits = ( return []; } - const getCombinations = (buckets: TermAggregationBucket[], i: number, field: string) => { + const getCombinations = (buckets: TermAggregationBucket[], i: number, field: string | null) => { return buckets.reduce((acc: MultiAggBucket[], bucket: TermAggregationBucket) => { if (i < threshold.field.length - 1) { const nextLevelIdx = i + 1; @@ -100,7 +99,7 @@ const getTransformedHits = ( topThresholdHits: val.topThresholdHits, docCount: val.docCount, }; - acc.push(el); + acc.push(el as MultiAggBucket); }); } else { const el = { @@ -121,80 +120,76 @@ const getTransformedHits = ( topThresholdHits: bucket.top_threshold_hits, docCount: bucket.doc_count, }; - acc.push(el); + acc.push(el as MultiAggBucket); } return acc; }, []); }; - // Recurse through the nested buckets and collect each unique combination of terms. Collect the - // cardinality and document count from the leaf buckets and return a signal for each set of terms. - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - return getCombinations(results.aggregations![aggParts.name].buckets, 0, aggParts.field).reduce( - (acc: Array>, bucket) => { - const hit = bucket.topThresholdHits?.hits.hits[0]; - if (hit == null) { - return acc; - } - - const timestampArray = get(timestampOverride ?? '@timestamp', hit.fields); - if (timestampArray == null) { - return acc; - } - - const timestamp = timestampArray[0]; - if (typeof timestamp !== 'string') { - return acc; - } - - const termsHash = getThresholdTermsHash(bucket.terms); - const signalHit = thresholdSignalHistory[termsHash]; - - const source = { - '@timestamp': timestamp, - ...bucket.terms.reduce((termAcc, term) => { - if (!term.field.startsWith('signal.')) { - return { - ...termAcc, - [term.field]: term.value, - }; - } - return termAcc; - }, {}), - threshold_result: { - terms: bucket.terms, - cardinality: bucket.cardinality, - count: bucket.docCount, - // Store `from` in the signal so that we know the lower bound for the - // threshold set in the timeline search. The upper bound will always be - // the `original_time` of the signal (the timestamp of the latest event - // in the set). - from: - signalHit?.lastSignalTimestamp != null - ? new Date(signalHit!.lastSignalTimestamp) - : from, - }, - }; + return getCombinations( + (results.aggregations![aggParts.name] as { buckets: TermAggregationBucket[] }).buckets, + 0, + aggParts.field + ).reduce((acc: Array>, bucket) => { + const hit = bucket.topThresholdHits?.hits.hits[0]; + if (hit == null) { + return acc; + } - acc.push({ - _index: inputIndex, - _id: calculateThresholdSignalUuid( - ruleId, - startedAt, - threshold.field, - bucket.terms - .map((term) => term.value) - .sort() - .join(',') - ), - _source: source, - }); + const timestampArray = get(timestampOverride ?? '@timestamp', hit.fields); + if (timestampArray == null) { + return acc; + } + const timestamp = timestampArray[0]; + if (typeof timestamp !== 'string') { return acc; - }, - [] - ); + } + + const termsHash = getThresholdTermsHash(bucket.terms); + const signalHit = thresholdSignalHistory[termsHash]; + + const source = { + '@timestamp': timestamp, + ...bucket.terms.reduce((termAcc, term) => { + if (!term.field.startsWith('signal.')) { + return { + ...termAcc, + [term.field]: term.value, + }; + } + return termAcc; + }, {}), + threshold_result: { + terms: bucket.terms, + cardinality: bucket.cardinality, + count: bucket.docCount, + // Store `from` in the signal so that we know the lower bound for the + // threshold set in the timeline search. The upper bound will always be + // the `original_time` of the signal (the timestamp of the latest event + // in the set). + from: + signalHit?.lastSignalTimestamp != null ? new Date(signalHit!.lastSignalTimestamp) : from, + }, + }; + + acc.push({ + _index: inputIndex, + _id: calculateThresholdSignalUuid( + ruleId, + startedAt, + threshold.field, + bucket.terms + .map((term) => term.value) + .sort() + .join(',') + ), + _source: source, + }); + + return acc; + }, []); }; export const transformThresholdResultsToEcs = ( @@ -238,7 +233,7 @@ export const transformThresholdResultsToEcs = ( export const bulkCreateThresholdSignals = async ( params: BulkCreateThresholdSignalsParams -): Promise => { +): Promise> => { const ruleParams = params.ruleSO.attributes.params; const thresholdResults = params.someResult; const ecsResults = transformThresholdResultsToEcs( @@ -253,7 +248,6 @@ export const bulkCreateThresholdSignals = async ( ruleParams.timestampOverride, params.thresholdSignalHistory ); - const buildRuleMessage = params.buildRuleMessage; - return singleBulkCreate({ ...params, filteredEvents: ecsResults, buildRuleMessage }); + return params.bulkCreate(params.wrapHits(ecsResults.hits.hits)); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 4205c2d6d8b2c5..c35eb04ba12707 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -26,12 +26,12 @@ import { RuleAlertAction, SearchTypes, } from '../../../../common/detection_engine/types'; -import { RefreshTypes } from '../types'; import { ListClient } from '../../../../../lists/server'; import { Logger, SavedObject } from '../../../../../../../src/core/server'; import { BuildRuleMessage } from './rule_messages'; import { TelemetryEventsSender } from '../../telemetry/sender'; import { RuleParams } from '../schemas/rule_schemas'; +import { GenericBulkCreateResponse } from './bulk_create_factory'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -255,6 +255,12 @@ export interface QueryFilter { export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise; +export type BulkCreate = (docs: Array>) => Promise>; + +export type WrapHits = ( + hits: Array> +) => Array>; + export interface SearchAfterAndBulkCreateParams { tuples: Array<{ to: moment.Moment; @@ -272,9 +278,10 @@ export interface SearchAfterAndBulkCreateParams { signalsIndex: string; pageSize: number; filter: unknown; - refresh: RefreshTypes; buildRuleMessage: BuildRuleMessage; enrichment?: SignalsEnrichment; + bulkCreate: BulkCreate; + wrapHits: WrapHits; } export interface SearchAfterAndBulkCreateReturnType { @@ -284,8 +291,9 @@ export interface SearchAfterAndBulkCreateReturnType { bulkCreateTimes: string[]; lastLookBackDate: Date | null | undefined; createdSignalsCount: number; - createdSignals: SignalHit[]; + createdSignals: unknown[]; errors: string[]; + warningMessages: string[]; totalToFromTuples?: Array<{ to: Moment | undefined; from: Moment | undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 60bf0ec337f3db..616cf714d6a8c2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -1083,6 +1083,7 @@ describe('utils', () => { searchAfterTimes: [], success: true, warning: false, + warningMessages: [], }; expect(newSearchResult).toEqual(expected); }); @@ -1102,6 +1103,7 @@ describe('utils', () => { searchAfterTimes: [], success: true, warning: false, + warningMessages: [], }; expect(newSearchResult).toEqual(expected); }); @@ -1380,6 +1382,7 @@ describe('utils', () => { searchAfterTimes: [], success: true, warning: false, + warningMessages: [], }; expect(searchAfterReturnType).toEqual(expected); }); @@ -1394,6 +1397,7 @@ describe('utils', () => { searchAfterTimes: ['123'], success: false, warning: true, + warningMessages: ['test warning'], }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: ['123'], @@ -1404,6 +1408,7 @@ describe('utils', () => { searchAfterTimes: ['123'], success: false, warning: true, + warningMessages: ['test warning'], }; expect(searchAfterReturnType).toEqual(expected); }); @@ -1423,6 +1428,7 @@ describe('utils', () => { searchAfterTimes: [], success: true, warning: false, + warningMessages: [], }; expect(searchAfterReturnType).toEqual(expected); }); @@ -1440,6 +1446,7 @@ describe('utils', () => { searchAfterTimes: [], success: true, warning: false, + warningMessages: [], }; expect(merged).toEqual(expected); }); @@ -1494,6 +1501,7 @@ describe('utils', () => { lastLookBackDate: new Date('2020-08-21T18:51:25.193Z'), searchAfterTimes: ['123'], success: true, + warningMessages: ['warning1'], }), createSearchAfterReturnType({ bulkCreateTimes: ['456'], @@ -1503,6 +1511,8 @@ describe('utils', () => { lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), searchAfterTimes: ['567'], success: true, + warningMessages: ['warning2'], + warning: true, }), ]); const expected: SearchAfterAndBulkCreateReturnType = { @@ -1513,7 +1523,8 @@ describe('utils', () => { lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), // takes the next lastLookBackDate searchAfterTimes: ['123', '567'], // concatenates the searchAfterTimes together success: true, // Defaults to success true is all of it was successful - warning: false, + warning: true, + warningMessages: ['warning1', 'warning2'], }; expect(merged).toEqual(expected); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 1de76f64fabec0..6d67bab6eb2f76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { createHash } from 'crypto'; import moment from 'moment'; import uuidv5 from 'uuid/v5'; @@ -17,6 +16,7 @@ import type { ListArray, ExceptionListItemSchema } from '@kbn/securitysolution-i import { MAX_EXCEPTION_LIST_SIZE } from '@kbn/securitysolution-list-constants'; import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; +import { ElasticsearchClient } from '@kbn/securitysolution-es-utils'; import { TimestampOverrideOrUndefined, Privilege, @@ -38,6 +38,7 @@ import { WrappedSignalHit, RuleRangeTuple, BaseSignalHit, + SignalSourceHit, } from './types'; import { BuildRuleMessage } from './rule_messages'; import { ShardError } from '../../types'; @@ -163,9 +164,15 @@ export const hasTimestampFields = async ( export const checkPrivileges = async ( services: AlertServices, indices: string[] +): Promise => + checkPrivilegesFromEsClient(services.scopedClusterClient.asCurrentUser, indices); + +export const checkPrivilegesFromEsClient = async ( + esClient: ElasticsearchClient, + indices: string[] ): Promise => ( - await services.scopedClusterClient.asCurrentUser.transport.request({ + await esClient.transport.request({ path: '/_security/user/_has_privileges', method: 'POST', body: { @@ -608,7 +615,7 @@ export const getValidDateFromDoc = ({ doc.fields != null && doc.fields[timestamp] != null ? doc.fields[timestamp][0] : doc._source != null - ? doc._source[timestamp] + ? (doc._source as { [key: string]: unknown })[timestamp] : undefined; const lastTimestamp = typeof timestampValue === 'string' || typeof timestampValue === 'number' @@ -657,6 +664,7 @@ export const createSearchAfterReturnType = ({ createdSignalsCount, createdSignals, errors, + warningMessages, }: { success?: boolean | undefined; warning?: boolean; @@ -664,8 +672,9 @@ export const createSearchAfterReturnType = ({ bulkCreateTimes?: string[] | undefined; lastLookBackDate?: Date | undefined; createdSignalsCount?: number | undefined; - createdSignals?: SignalHit[] | undefined; + createdSignals?: unknown[] | undefined; errors?: string[] | undefined; + warningMessages?: string[] | undefined; } = {}): SearchAfterAndBulkCreateReturnType => { return { success: success ?? true, @@ -676,10 +685,12 @@ export const createSearchAfterReturnType = ({ createdSignalsCount: createdSignalsCount ?? 0, createdSignals: createdSignals ?? [], errors: errors ?? [], + warningMessages: warningMessages ?? [], }; }; export const createSearchResultReturnType = (): SignalSearchResponse => { + const hits: SignalSourceHit[] = []; return { took: 0, timed_out: false, @@ -693,7 +704,7 @@ export const createSearchResultReturnType = (): SignalSearchResponse => { hits: { total: 0, max_score: 0, - hits: [], + hits, }, }; }; @@ -711,7 +722,8 @@ export const mergeReturns = ( createdSignalsCount: existingCreatedSignalsCount, createdSignals: existingCreatedSignals, errors: existingErrors, - } = prev; + warningMessages: existingWarningMessages, + }: SearchAfterAndBulkCreateReturnType = prev; const { success: newSuccess, @@ -722,7 +734,8 @@ export const mergeReturns = ( createdSignalsCount: newCreatedSignalsCount, createdSignals: newCreatedSignals, errors: newErrors, - } = next; + warningMessages: newWarningMessages, + }: SearchAfterAndBulkCreateReturnType = next; return { success: existingSuccess && newSuccess, @@ -733,6 +746,7 @@ export const mergeReturns = ( createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount, createdSignals: [...existingCreatedSignals, ...newCreatedSignals], errors: [...new Set([...existingErrors, ...newErrors])], + warningMessages: [...existingWarningMessages, ...newWarningMessages], }; }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts new file mode 100644 index 00000000000000..3f3e4ef3631bd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SearchAfterAndBulkCreateParams, + SignalSourceHit, + WrapHits, + WrappedSignalHit, +} from './types'; +import { generateId } from './utils'; +import { buildBulkBody } from './build_bulk_body'; +import { filterDuplicateSignals } from './filter_duplicate_signals'; + +export const wrapHitsFactory = ({ + ruleSO, + signalsIndex, +}: { + ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; + signalsIndex: string; +}): WrapHits => (events) => { + const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ + { + _index: signalsIndex, + // TODO: bring back doc._version + _id: generateId(doc._index, doc._id, '', ruleSO.attributes.params.ruleId ?? ''), + _source: buildBulkBody(ruleSO, doc as SignalSourceHit), + }, + ]); + + return filterDuplicateSignals(ruleSO.id, wrappedDocs); +}; diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index f1c7a275e162c1..6ef51bc3c53d48 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -17,6 +17,7 @@ import { Notes } from './timeline/saved_object/notes'; import { PinnedEvent } from './timeline/saved_object/pinned_events'; import { Timeline } from './timeline/saved_object/timelines'; import { TotalValue, BaseHit, Explanation } from '../../common/detection_engine/types'; +import { SignalHit } from './detection_engine/signals/types'; export interface AppDomainLibs { fields: IndexFields; @@ -100,6 +101,8 @@ export interface SearchResponse extends BaseSearchResponse { export type SearchHit = SearchResponse['hits']['hits'][0]; +export type SearchSignalHit = SearchResponse['hits']['hits'][0]; + export interface TermAggregationBucket { key: string; doc_count: number; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 609ee88c319f9b..a0f466512cc1d0 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -260,6 +260,8 @@ export class Plugin implements IPlugin Date: Mon, 14 Jun 2021 20:25:04 +0200 Subject: [PATCH 75/99] [Discover] Unskip runtime field editor test (#101059) --- .../apps/discover/_runtime_fields_editor.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index 648fa3efe337c0..46fe5c34f4cf36 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.save(); }; - describe.skip('discover integration with runtime fields editor', function describeIndexTests() { + describe('discover integration with runtime fields editor', function describeIndexTests() { before(async function () { await esArchiver.load('test/functional/fixtures/es_archiver/discover'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); @@ -104,7 +104,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // flaky https://github.com/elastic/kibana/issues/100966 it('doc view includes runtime fields', async function () { // navigate to doc view const table = await PageObjects.discover.getDocTable(); @@ -121,10 +120,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[idxToClick].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); - const runtimeFieldsRow = await testSubjects.exists('tableDocViewRow-discover runtimefield'); - expect(runtimeFieldsRow).to.be(true); + await retry.waitFor('doc viewer is displayed with runtime field', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + if (!hasDocHit) { + // Maybe loading has not completed + throw new Error('test subject doc-hit is not yet displayed'); + } + const runtimeFieldsRow = await testSubjects.exists('tableDocViewRow-discover runtimefield'); + + return hasDocHit && runtimeFieldsRow; + }); }); }); } From 8f5dad98a181eed7c4b1efe6c1cf41f906cfd042 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Mon, 14 Jun 2021 19:28:08 +0100 Subject: [PATCH 76/99] [Logs / Metrics UI] Convert logs and metrics pages to the new Observability page template (#101239) * Convert Logs and Metrics pages to use the Observability page template --- x-pack/plugins/infra/kibana.json | 5 +- .../metric_anomaly/components/expression.tsx | 4 +- .../infra/public/apps/common_styles.ts | 13 +- x-pack/plugins/infra/public/apps/logs_app.tsx | 2 +- .../plugins/infra/public/apps/metrics_app.tsx | 6 +- .../public/assets/anomaly_chart_minified.svg | 1 - .../components/empty_states/no_indices.tsx | 28 +-- .../infra/public/components/error_page.tsx | 97 +++----- .../infra/public/components/loading_page.tsx | 44 ++-- .../logging/log_analysis_setup/index.ts | 2 - .../logging/log_analysis_setup/setup_page.tsx | 60 ----- .../logging/log_source_error_page.tsx | 14 +- .../components/navigation/app_navigation.tsx | 33 --- .../components/navigation/routed_tabs.tsx | 62 ----- .../infra/public/components/page_template.tsx | 22 ++ .../subscription_splash_content.tsx | 123 +++------- .../infra/public/components/toolbar_panel.ts | 27 -- x-pack/plugins/infra/public/index.scss | 16 -- .../pages/link_to/link_to_logs.test.tsx | 16 +- .../pages/logs/log_entry_categories/page.tsx | 5 +- .../log_entry_categories/page_content.tsx | 61 ++++- .../page_results_content.tsx | 77 ++++-- .../page_setup_content.tsx | 34 ++- .../top_categories/top_categories_section.tsx | 68 +---- .../public/pages/logs/log_entry_rate/page.tsx | 5 +- .../logs/log_entry_rate/page_content.tsx | 58 ++++- .../log_entry_rate/page_results_content.tsx | 160 ++++++------ .../log_entry_rate/page_setup_content.tsx | 34 ++- .../sections/anomalies/index.tsx | 19 -- .../infra/public/pages/logs/page_content.tsx | 25 +- .../infra/public/pages/logs/page_template.tsx | 24 ++ .../source_configuration_settings.tsx | 196 +++++++-------- .../infra/public/pages/logs/stream/page.tsx | 7 +- .../public/pages/logs/stream/page_content.tsx | 33 ++- .../pages/logs/stream/page_logs_content.tsx | 4 +- .../public/pages/logs/stream/page_toolbar.tsx | 7 +- .../infra/public/pages/metrics/index.tsx | 199 +++++++-------- .../components/bottom_drawer.tsx | 4 +- .../inventory_view/components/filter_bar.tsx | 24 +- .../ml/anomaly_detection/flyout_home.tsx | 8 +- .../waffle/waffle_time_controls.tsx | 13 +- .../pages/metrics/inventory_view/index.tsx | 157 +++++++----- .../components/node_details_page.tsx | 118 ++++----- .../metric_detail/components/side_nav.tsx | 25 +- .../pages/metrics/metric_detail/index.tsx | 80 +++--- .../metrics_explorer/components/toolbar.tsx | 5 +- .../pages/metrics/metrics_explorer/index.tsx | 75 +++--- .../public/pages/metrics/page_template.tsx | 24 ++ .../source_configuration_settings.tsx | 232 +++++++++--------- x-pack/plugins/infra/public/plugin.ts | 59 ++++- .../public/components/shared/index.tsx | 1 + .../components/shared/page_template/index.ts | 1 + .../shared/page_template/page_template.tsx | 1 + x-pack/plugins/observability/public/index.ts | 2 + .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../page_objects/infra_home_page.ts | 24 +- 57 files changed, 1149 insertions(+), 1307 deletions(-) delete mode 100644 x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg delete mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx delete mode 100644 x-pack/plugins/infra/public/components/navigation/app_navigation.tsx delete mode 100644 x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx create mode 100644 x-pack/plugins/infra/public/components/page_template.tsx delete mode 100644 x-pack/plugins/infra/public/components/toolbar_panel.ts create mode 100644 x-pack/plugins/infra/public/pages/logs/page_template.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/page_template.tsx diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index c0d567ef83cedb..ec1b11c90f7a31 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -11,9 +11,10 @@ "dataEnhanced", "visTypeTimeseries", "alerting", - "triggersActionsUi" + "triggersActionsUi", + "observability" ], - "optionalPlugins": ["ml", "observability", "home", "embeddable"], + "optionalPlugins": ["ml", "home", "embeddable"], "server": true, "ui": true, "configPath": ["xpack", "infra"], 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 afbd6ffa8b5f7e..e44a747aa07e71 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 @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; -import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; +import { SubscriptionSplashPrompt } from '../../../components/subscription_splash_content'; import { AlertPreview } from '../../common'; import { METRIC_ANOMALY_ALERT_TYPE_ID, @@ -185,7 +185,7 @@ export const Expression: React.FC = (props) => { }, [metadata, derivedIndexPattern, defaultExpression, source, space]); // eslint-disable-line react-hooks/exhaustive-deps if (isLoadingMLCapabilities) return ; - if (!hasInfraMLCapabilities) return ; + if (!hasInfraMLCapabilities) return ; return ( // https://github.com/elastic/kibana/issues/89506 diff --git a/x-pack/plugins/infra/public/apps/common_styles.ts b/x-pack/plugins/infra/public/apps/common_styles.ts index be12c6cdc937fd..68c820d538ca9b 100644 --- a/x-pack/plugins/infra/public/apps/common_styles.ts +++ b/x-pack/plugins/infra/public/apps/common_styles.ts @@ -5,10 +5,15 @@ * 2.0. */ +import { APP_WRAPPER_CLASS } from '../../../../../src/core/public'; + export const CONTAINER_CLASSNAME = 'infra-container-element'; -export const prepareMountElement = (element: HTMLElement) => { - // Ensure the element we're handed from application mounting is assigned a class - // for our index.scss styles to apply to. - element.classList.add(CONTAINER_CLASSNAME); +export const prepareMountElement = (element: HTMLElement, testSubject?: string) => { + // Ensure all wrapping elements have the APP_WRAPPER_CLASS so that the KinanaPageTemplate works as expected + element.classList.add(APP_WRAPPER_CLASS); + + if (testSubject) { + element.setAttribute('data-test-subj', testSubject); + } }; diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index 61082efe436473..b512b5ce4a1764 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -27,7 +27,7 @@ export const renderApp = ( ) => { const storage = new Storage(window.localStorage); - prepareMountElement(element); + prepareMountElement(element, 'infraLogsPage'); ReactDOM.render( { const storage = new Storage(window.localStorage); - prepareMountElement(element); + prepareMountElement(element, 'infraMetricsPage'); ReactDOM.render( )} - {uiCapabilities?.infrastructure?.show && ( - - )} {uiCapabilities?.infrastructure?.show && ( )} diff --git a/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg b/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg deleted file mode 100644 index dd1b39248bba25..00000000000000 --- a/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx b/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx index 264428e44a44ad..c61a567ac73b15 100644 --- a/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx +++ b/x-pack/plugins/infra/public/components/empty_states/no_indices.tsx @@ -7,8 +7,7 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; - -import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { PageTemplate } from '../page_template'; interface NoIndicesProps { message: string; @@ -17,15 +16,16 @@ interface NoIndicesProps { 'data-test-subj'?: string; } -export const NoIndices: React.FC = ({ actions, message, title, ...rest }) => ( - {title}} - body={

    {message}

    } - actions={actions} - {...rest} - /> -); - -const CenteredEmptyPrompt = euiStyled(EuiEmptyPrompt)` - align-self: center; -`; +// Represents a fully constructed page, including page template. +export const NoIndices: React.FC = ({ actions, message, title, ...rest }) => { + return ( + + {title}} + body={

    {message}

    } + actions={actions} + {...rest} + /> +
    + ); +}; diff --git a/x-pack/plugins/infra/public/components/error_page.tsx b/x-pack/plugins/infra/public/components/error_page.tsx index 184901b4fdd9b0..da6716ddc7f723 100644 --- a/x-pack/plugins/infra/public/components/error_page.tsx +++ b/x-pack/plugins/infra/public/components/error_page.tsx @@ -5,20 +5,10 @@ * 2.0. */ -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; -import { FlexPage } from './page'; +import { PageTemplate } from './page_template'; interface Props { detailedMessage?: React.ReactNode; @@ -26,51 +16,40 @@ interface Props { shortMessage: React.ReactNode; } -export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessage }) => ( - - - = ({ detailedMessage, retry, shortMessage }) => { + return ( + + + } > - - - } - > - - {shortMessage} - {retry ? ( - - - - - - ) : null} - - {detailedMessage ? ( - <> - -
    {detailedMessage}
    - - ) : null} -
    -
    -
    -
    -
    -); - -const MinimumPageContent = euiStyled(EuiPageContent)` - min-width: 50vh; -`; + + {shortMessage} + {retry ? ( + + + + + + ) : null} + + {detailedMessage ? ( + <> + +
    {detailedMessage}
    + + ) : null} + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx index 755511374b75fa..2b2859707a20d7 100644 --- a/x-pack/plugins/infra/public/components/loading_page.tsx +++ b/x-pack/plugins/infra/public/components/loading_page.tsx @@ -5,34 +5,38 @@ * 2.0. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageBody, - EuiPageContent, -} from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { ReactNode } from 'react'; - -import { FlexPage } from './page'; +import { PageTemplate } from './page_template'; interface LoadingPageProps { message?: ReactNode; 'data-test-subj'?: string; } +// Represents a fully constructed page, including page template. export const LoadingPage = ({ message, 'data-test-subj': dataTestSubj = 'loadingPage', -}: LoadingPageProps) => ( - - - - - - {message} +}: LoadingPageProps) => { + return ( + + + + ); +}; + +export const LoadingPrompt = ({ message }: LoadingPageProps) => { + return ( + + + + + {message} - - - -); + } + /> + ); +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts index db5a996c604fcd..9ca08c69cf6006 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -export * from './setup_page'; - export * from './initial_configuration_step'; export * from './process_step'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx deleted file mode 100644 index a998d0c304a5ee..00000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - CommonProps, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiTitle, -} from '@elastic/eui'; -import React from 'react'; - -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; - -export const LogAnalysisSetupPage: React.FunctionComponent = ({ - children, - ...rest -}) => { - return ( - - - - {children} - - - - ); -}; - -export const LogAnalysisSetupPageHeader: React.FunctionComponent = ({ children }) => ( - - - -

    {children}

    -
    -
    -
    -); - -export const LogAnalysisSetupPageContent = EuiPageContentBody; - -// !important due to https://github.com/elastic/eui/issues/2232 -const LogEntryRateSetupPageContent = euiStyled(EuiPageContent)` - max-width: 768px !important; -`; - -const LogEntryRateSetupPage = euiStyled(EuiPage)` - height: 100%; -`; diff --git a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx index 8ea35fd8f259f7..6c757f7383a060 100644 --- a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx @@ -5,14 +5,7 @@ * 2.0. */ -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiEmptyPrompt, - EuiPageTemplate, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { SavedObjectNotFound } from '../../../../../../src/plugins/kibana_utils/common'; @@ -22,6 +15,7 @@ import { ResolveLogSourceConfigurationError, } from '../../../common/log_sources'; import { useLinkProps } from '../../hooks/use_link_props'; +import { LogsPageTemplate } from '../../pages/logs/page_template'; export const LogSourceErrorPage: React.FC<{ errors: Error[]; @@ -30,7 +24,7 @@ export const LogSourceErrorPage: React.FC<{ const settingsLinkProps = useLinkProps({ app: 'logs', pathname: '/settings' }); return ( - + , ]} /> - + ); }; diff --git a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx deleted file mode 100644 index 966b91537d3bd8..00000000000000 --- a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; - -interface AppNavigationProps { - 'aria-label': string; - children: React.ReactNode; -} - -export const AppNavigation = ({ 'aria-label': label, children }: AppNavigationProps) => ( - -); - -const Nav = euiStyled.nav` - background: ${(props) => props.theme.eui.euiColorEmptyShade}; - border-bottom: ${(props) => props.theme.eui.euiBorderThin}; - padding: ${(props) => `${props.theme.eui.euiSizeS} ${props.theme.eui.euiSizeL}`}; - .euiTabs { - padding-left: 3px; - margin-left: -3px; - }; -`; diff --git a/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx b/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx deleted file mode 100644 index 2a5ffcd826e7c3..00000000000000 --- a/x-pack/plugins/infra/public/components/navigation/routed_tabs.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiLink, EuiTab, EuiTabs } from '@elastic/eui'; -import React from 'react'; -import { Route } from 'react-router-dom'; - -import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; -import { useLinkProps } from '../../hooks/use_link_props'; -import { LinkDescriptor } from '../../hooks/use_link_props'; - -interface TabConfig { - title: string | React.ReactNode; -} - -type TabConfiguration = TabConfig & LinkDescriptor; - -interface RoutedTabsProps { - tabs: TabConfiguration[]; -} - -const noop = () => {}; - -export const RoutedTabs = ({ tabs }: RoutedTabsProps) => { - return ( - - {tabs.map((tab) => { - return ; - })} - - ); -}; - -const Tab = ({ title, pathname, app }: TabConfiguration) => { - const linkProps = useLinkProps({ app, pathname }); - return ( - { - return ( - - - - {title} - - - - ); - }} - /> - ); -}; - -const TabContainer = euiStyled.div` - .euiLink { - color: inherit !important; - } -`; diff --git a/x-pack/plugins/infra/public/components/page_template.tsx b/x-pack/plugins/infra/public/components/page_template.tsx new file mode 100644 index 00000000000000..1a10a6cd831b91 --- /dev/null +++ b/x-pack/plugins/infra/public/components/page_template.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useKibanaContextForPlugin } from '../hooks/use_kibana'; +import type { LazyObservabilityPageTemplateProps } from '../../../observability/public'; + +export const PageTemplate: React.FC = (pageTemplateProps) => { + const { + services: { + observability: { + navigation: { PageTemplate: Template }, + }, + }, + } = useKibanaContextForPlugin(); + + return