From a231f9c4fd7cde8ff19d41b1810b9e95f5aafd8c Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 6 Oct 2022 10:21:09 -0400 Subject: [PATCH 1/7] [Response Ops][Alerting] Update stack rules to respect max alert limit (#141000) * wip * wip * Adding bucket selector clauses * Adding comparator script generator * Generating all the right queries * Skip condition check if group agg * Fixing functional test * Fixing comparator script * Fixing tests * Fixing tests * Renaming * Using limit services in es query rule executor Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/alert_types/es_query/executor.ts | 8 + .../index_threshold/action_context.test.ts | 26 +- .../index_threshold/action_context.ts | 8 +- .../alert_types/index_threshold/index.ts | 4 +- .../{alert_type.test.ts => rule_type.test.ts} | 124 +- .../{alert_type.ts => rule_type.ts} | 33 +- ...arams.test.ts => rule_type_params.test.ts} | 4 +- ...ert_type_params.ts => rule_type_params.ts} | 2 +- .../server/alert_types/lib/comparator.test.ts | 47 + .../server/alert_types/lib/comparator.ts | 39 + x-pack/plugins/stack_alerts/server/feature.ts | 2 +- x-pack/plugins/stack_alerts/server/index.ts | 2 +- .../triggers_actions_ui/common/data/index.ts | 1 + .../triggers_actions_ui/server/data/index.ts | 1 + .../server/data/lib/index.ts | 1 + .../server/data/lib/time_series_query.test.ts | 1606 ++++++++++++++++- .../server/data/lib/time_series_query.ts | 98 +- .../server/data/lib/time_series_types.ts | 7 + .../triggers_actions_ui/server/index.ts | 1 + .../common/lib/es_test_index_tool.ts | 3 + .../index_threshold/alert.ts | 39 + .../time_series_query_endpoint.ts | 9 + .../lib/create_test_data.ts | 1 + 23 files changed, 1918 insertions(+), 148 deletions(-) rename x-pack/plugins/stack_alerts/server/alert_types/index_threshold/{alert_type.test.ts => rule_type.test.ts} (73%) rename x-pack/plugins/stack_alerts/server/alert_types/index_threshold/{alert_type.ts => rule_type.ts} (88%) rename x-pack/plugins/stack_alerts/server/alert_types/index_threshold/{alert_type_params.test.ts => rule_type_params.test.ts} (98%) rename x-pack/plugins/stack_alerts/server/alert_types/index_threshold/{alert_type_params.ts => rule_type_params.ts} (98%) create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index 5f33eeb0af845..b52f5803405a7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -29,6 +29,8 @@ export async function executor( const currentTimestamp = new Date().toISOString(); const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; + const alertLimit = alertFactory.alertLimit.getValue(); + const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { throw new Error(getInvalidComparatorError(params.thresholdComparator)); @@ -91,6 +93,12 @@ export async function executor( if (firstValidTimefieldSort) { latestTimestamp = firstValidTimefieldSort; } + + // we only create one alert if the condition is met, so we would only ever + // reach the alert limit if the limit is less than 1 + alertFactory.alertLimit.setLimitReached(alertLimit < 1); + } else { + alertFactory.alertLimit.setLimitReached(false); } const { getRecoveredAlerts } = alertFactory.done(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts index 4d8c1dc3d9b9f..0df74bb2f89c5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts @@ -6,7 +6,7 @@ */ import { BaseActionContext, addMessages } from './action_context'; -import { ParamsSchema } from './alert_type_params'; +import { ParamsSchema } from './rule_type_params'; describe('ActionContext', () => { it('generates expected properties if aggField is null', async () => { @@ -28,10 +28,10 @@ describe('ActionContext', () => { value: 42, conditions: 'count greater than 4', }; - const context = addMessages({ name: '[alert-name]' }, base, params); - expect(context.title).toMatchInlineSnapshot(`"alert [alert-name] group [group] met threshold"`); + const context = addMessages({ name: '[rule-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert [rule-name] group [group] met threshold"`); expect(context.message).toEqual( - `alert '[alert-name]' is active for group '[group]': + `alert '[rule-name]' is active for group '[group]': - Value: 42 - Conditions Met: count greater than 4 over 5m @@ -59,10 +59,10 @@ describe('ActionContext', () => { value: 42, conditions: 'avg([aggField]) greater than 4.2', }; - const context = addMessages({ name: '[alert-name]' }, base, params); - expect(context.title).toMatchInlineSnapshot(`"alert [alert-name] group [group] met threshold"`); + const context = addMessages({ name: '[rule-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert [rule-name] group [group] met threshold"`); expect(context.message).toEqual( - `alert '[alert-name]' is active for group '[group]': + `alert '[rule-name]' is active for group '[group]': - Value: 42 - Conditions Met: avg([aggField]) greater than 4.2 over 5m @@ -89,10 +89,10 @@ describe('ActionContext', () => { value: 4, conditions: 'count between 4 and 5', }; - const context = addMessages({ name: '[alert-name]' }, base, params); - expect(context.title).toMatchInlineSnapshot(`"alert [alert-name] group [group] met threshold"`); + const context = addMessages({ name: '[rule-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert [rule-name] group [group] met threshold"`); expect(context.message).toEqual( - `alert '[alert-name]' is active for group '[group]': + `alert '[rule-name]' is active for group '[group]': - Value: 4 - Conditions Met: count between 4 and 5 over 5m @@ -119,10 +119,10 @@ describe('ActionContext', () => { value: 'unknown', conditions: 'count between 4 and 5', }; - const context = addMessages({ name: '[alert-name]' }, base, params); - expect(context.title).toMatchInlineSnapshot(`"alert [alert-name] group [group] met threshold"`); + const context = addMessages({ name: '[rule-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert [rule-name] group [group] met threshold"`); expect(context.message).toEqual( - `alert '[alert-name]' is active for group '[group]': + `alert '[rule-name]' is active for group '[group]': - Value: unknown - Conditions Met: count between 4 and 5 over 5m diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts index 94e7f9b8501a4..36ed27d8a7391 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import { RuleExecutorOptions, AlertInstanceContext } from '@kbn/alerting-plugin/server'; -import { Params } from './alert_type_params'; +import { Params } from './rule_type_params'; -// alert type context provided to actions +// rule type context provided to actions type RuleInfo = Pick; @@ -21,10 +21,10 @@ export interface ActionContext extends BaseActionContext { } export interface BaseActionContext extends AlertInstanceContext { - // the aggType used in the alert + // the aggType used in the rule // the value of the aggField, if used, otherwise 'all documents' group: string; - // the date the alert was run as an ISO date + // the date the rule was run as an ISO date date: string; // the value that met the threshold value: number | string; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts index 065ef6b5ee22c..449c6528798a6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts @@ -7,7 +7,7 @@ import { Logger } from '@kbn/core/server'; import { AlertingSetup, StackAlertsStartDeps } from '../../types'; -import { getAlertType } from './alert_type'; +import { getRuleType } from './rule_type'; // future enhancement: make these configurable? export const MAX_INTERVALS = 1000; @@ -22,5 +22,5 @@ interface RegisterParams { export function register(params: RegisterParams) { const { logger, data, alerting } = params; - alerting.registerType(getAlertType(logger, data)); + alerting.registerType(getRuleType(logger, data)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts similarity index 73% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts index 1751fc6fc2344..656a1e5f275e5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts @@ -6,34 +6,44 @@ */ import uuid from 'uuid'; +import sinon from 'sinon'; import type { Writable } from '@kbn/utility-types'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; -import { getAlertType, ActionGroupId } from './alert_type'; +import { getRuleType, ActionGroupId } from './rule_type'; import { ActionContext } from './action_context'; -import { Params } from './alert_type_params'; +import { Params } from './rule_type_params'; +import { TIME_SERIES_BUCKET_SELECTOR_FIELD } from '@kbn/triggers-actions-ui-plugin/server'; import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { Comparator } from '../../../common/comparator_types'; -describe('alertType', () => { +let fakeTimer: sinon.SinonFakeTimers; + +describe('ruleType', () => { const logger = loggingSystemMock.create().get(); const data = { timeSeriesQuery: jest.fn(), }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); - const alertType = getAlertType(logger, Promise.resolve(data)); + const ruleType = getRuleType(logger, Promise.resolve(data)); + + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(); + }); afterEach(() => { data.timeSeriesQuery.mockReset(); }); - it('alert type creation structure is the expected value', async () => { - expect(alertType.id).toBe('.index-threshold'); - expect(alertType.name).toBe('Index threshold'); - expect(alertType.actionGroups).toEqual([{ id: 'threshold met', name: 'Threshold met' }]); + afterAll(() => fakeTimer.restore()); - expect(alertType.actionVariables).toMatchInlineSnapshot(` + it('rule type creation structure is the expected value', async () => { + expect(ruleType.id).toBe('.index-threshold'); + expect(ruleType.name).toBe('Index threshold'); + expect(ruleType.actionGroups).toEqual([{ id: 'threshold met', name: 'Threshold met' }]); + + expect(ruleType.actionVariables).toMatchInlineSnapshot(` Object { "context": Array [ Object { @@ -123,11 +133,11 @@ describe('alertType', () => { threshold: [0], }; - expect(alertType.validate?.params?.validate(params)).toBeTruthy(); + expect(ruleType.validate?.params?.validate(params)).toBeTruthy(); }); it('validator fails with invalid params', async () => { - const paramsSchema = alertType.validate?.params; + const paramsSchema = ruleType.validate?.params; if (!paramsSchema) throw new Error('params validator not set'); const params: Partial> = { @@ -168,7 +178,7 @@ describe('alertType', () => { threshold: [1], }; - await alertType.executor({ + await ruleType.executor({ alertId: uuid.v4(), executionId: uuid.v4(), startedAt: new Date(), @@ -234,7 +244,7 @@ describe('alertType', () => { threshold: [1], }; - await alertType.executor({ + await ruleType.executor({ alertId: uuid.v4(), executionId: uuid.v4(), startedAt: new Date(), @@ -300,7 +310,7 @@ describe('alertType', () => { threshold: [1], }; - await alertType.executor({ + await ruleType.executor({ alertId: uuid.v4(), executionId: uuid.v4(), startedAt: new Date(), @@ -342,4 +352,90 @@ describe('alertType', () => { expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); + + it('should correctly pass comparator script to timeSeriesQuery', async () => { + data.timeSeriesQuery.mockImplementation((...args) => { + return { + results: [ + { + group: 'all documents', + metrics: [['2021-07-14T14:49:30.978Z', 0]], + }, + ], + }; + }); + const params: Params = { + index: 'index-name', + timeField: 'time-field', + aggType: 'foo', + groupBy: 'all', + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: Comparator.LT, + threshold: [1], + }; + + await ruleType.executor({ + alertId: uuid.v4(), + executionId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: alertServices as unknown as RuleExecutorServices< + {}, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + latestTimestamp: undefined, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + rule: { + name: uuid.v4(), + tags: [], + consumer: '', + producer: '', + ruleTypeId: '', + ruleTypeName: '', + enabled: true, + schedule: { + interval: '1h', + }, + actions: [], + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + throttle: null, + notifyWhen: null, + }, + }); + + expect(data.timeSeriesQuery).toHaveBeenCalledWith( + expect.objectContaining({ + query: { + aggField: undefined, + aggType: 'foo', + dateEnd: '1970-01-01T00:00:00.000Z', + dateStart: '1970-01-01T00:00:00.000Z', + groupBy: 'all', + index: 'index-name', + interval: undefined, + termField: undefined, + termSize: undefined, + timeField: 'time-field', + timeWindowSize: 5, + timeWindowUnit: 'm', + }, + condition: { + conditionScript: `${TIME_SERIES_BUCKET_SELECTOR_FIELD} < 1L`, + resultLimit: 1000, + }, + }) + ); + }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts similarity index 88% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts rename to x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts index 58d680794aedf..3b3480407fcfc 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts @@ -10,21 +10,23 @@ import { Logger } from '@kbn/core/server'; import { CoreQueryParamsSchemaProperties, TimeSeriesQuery, + TIME_SERIES_BUCKET_SELECTOR_FIELD, } from '@kbn/triggers-actions-ui-plugin/server'; import { RuleType, RuleExecutorOptions, StackAlertsStartDeps } from '../../types'; -import { Params, ParamsSchema } from './alert_type_params'; +import { Params, ParamsSchema } from './rule_type_params'; import { ActionContext, BaseActionContext, addMessages } from './action_context'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ComparatorFns, getHumanReadableComparator } from '../lib'; +import { getComparatorScript } from '../lib/comparator'; export const ID = '.index-threshold'; export const ActionGroupId = 'threshold met'; -export function getAlertType( +export function getRuleType( logger: Logger, data: Promise ): RuleType { - const alertTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', { + const ruleTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', { defaultMessage: 'Index threshold', }); @@ -92,7 +94,7 @@ export function getAlertType( } ); - const alertParamsVariables = Object.keys(CoreQueryParamsSchemaProperties).map( + const ruleParamsVariables = Object.keys(CoreQueryParamsSchemaProperties).map( (propKey: string) => { return { name: propKey, @@ -103,7 +105,7 @@ export function getAlertType( return { id: ID, - name: alertTypeName, + name: ruleTypeName, actionGroups: [{ id: ActionGroupId, name: actionGroupName }], defaultActionGroupId: ActionGroupId, validate: { @@ -121,7 +123,7 @@ export function getAlertType( params: [ { name: 'threshold', description: actionVariableContextThresholdLabel }, { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, - ...alertParamsVariables, + ...ruleParamsVariables, ], }, minimumLicenseRequired: 'basic', @@ -137,6 +139,8 @@ export function getAlertType( const { alertId: ruleId, name, services, params } = options; const { alertFactory, scopedClusterClient } = services; + const alertLimit = alertFactory.alertLimit.getValue(); + const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { throw new Error( @@ -173,9 +177,19 @@ export function getAlertType( logger, esClient, query: queryParams, + condition: { + resultLimit: alertLimit, + conditionScript: getComparatorScript( + params.thresholdComparator, + params.threshold, + TIME_SERIES_BUCKET_SELECTOR_FIELD + ), + }, }); logger.debug(`rule ${ID}:${ruleId} "${name}" query result: ${JSON.stringify(result)}`); + const isGroupAgg = !!queryParams.termField; + const unmetGroupValues: Record = {}; const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`; @@ -196,7 +210,10 @@ export function getAlertType( continue; } - const met = compareFn(value, params.threshold); + // group aggregations use the bucket selector agg to compare conditions + // within the ES query, so only 'met' results are returned, therefore we don't need + // to use the compareFn + const met = isGroupAgg ? true : compareFn(value, params.threshold); if (!met) { unmetGroupValues[alertId] = value; @@ -219,6 +236,8 @@ export function getAlertType( logger.debug(`scheduled actionGroup: ${JSON.stringify(actionContext)}`); } + alertFactory.alertLimit.setLimitReached(result.truncated); + const { getRecoveredAlerts } = services.alertFactory.done(); for (const recoveredAlert of getRecoveredAlerts()) { const alertId = recoveredAlert.getId(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type_params.test.ts similarity index 98% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type_params.test.ts index 7bcf84db20a1f..c4dd8f2149255 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type_params.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ParamsSchema, Params } from './alert_type_params'; +import { ParamsSchema, Params } from './rule_type_params'; import { ObjectType, TypeOf } from '@kbn/config-schema'; import type { Writable } from '@kbn/utility-types'; import { CoreQueryParams, MAX_GROUPS } from '@kbn/triggers-actions-ui-plugin/server'; @@ -22,7 +22,7 @@ const DefaultParams: Writable> = { threshold: [0], }; -describe('alertType Params validate()', () => { +describe('ruleType Params validate()', () => { runTests(ParamsSchema, DefaultParams); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type_params.ts similarity index 98% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts rename to x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type_params.ts index 5783b6a64ab83..9018d915f4e48 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type_params.ts @@ -15,7 +15,7 @@ import { ComparatorFnNames } from '../lib'; import { Comparator } from '../../../common/comparator_types'; import { getComparatorSchemaType } from '../lib/comparator'; -// alert type parameters +// rule type parameters export type Params = TypeOf; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.test.ts new file mode 100644 index 0000000000000..62447c12fdf49 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getComparatorScript } from './comparator'; +import { Comparator } from '../../../common/comparator_types'; + +describe('getComparatorScript', () => { + it('correctly returns script when comparator is LT', () => { + expect(getComparatorScript(Comparator.LT, [10], 'fieldName')).toEqual(`fieldName < 10L`); + }); + it('correctly returns script when comparator is LT_OR_EQ', () => { + expect(getComparatorScript(Comparator.LT_OR_EQ, [10], 'fieldName')).toEqual(`fieldName <= 10L`); + }); + it('correctly returns script when comparator is GT', () => { + expect(getComparatorScript(Comparator.GT, [10], 'fieldName')).toEqual(`fieldName > 10L`); + }); + it('correctly returns script when comparator is GT_OR_EQ', () => { + expect(getComparatorScript(Comparator.GT_OR_EQ, [10], 'fieldName')).toEqual(`fieldName >= 10L`); + }); + it('correctly returns script when comparator is BETWEEN', () => { + expect(getComparatorScript(Comparator.BETWEEN, [10, 100], 'fieldName')).toEqual( + `fieldName >= 10L && fieldName <= 100L` + ); + }); + it('correctly returns script when comparator is NOT_BETWEEN', () => { + expect(getComparatorScript(Comparator.NOT_BETWEEN, [10, 100], 'fieldName')).toEqual( + `fieldName < 10L || fieldName > 100L` + ); + }); + it('correctly returns script when threshold is float', () => { + expect(getComparatorScript(Comparator.LT, [3.5454], 'fieldName')).toEqual(`fieldName < 3.5454`); + }); + it('throws error when threshold is empty', () => { + expect(() => { + getComparatorScript(Comparator.LT, [], 'fieldName'); + }).toThrowErrorMatchingInlineSnapshot(`"Threshold value required"`); + }); + it('throws error when comparator requires two thresholds and two thresholds are not defined', () => { + expect(() => { + getComparatorScript(Comparator.BETWEEN, [1], 'fieldName'); + }).toThrowErrorMatchingInlineSnapshot(`"Threshold values required"`); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts index 524e3a7554dc2..dbdc310b88038 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts @@ -34,6 +34,45 @@ export const ComparatorFns = new Map([ ], ]); +export const getComparatorScript = ( + comparator: Comparator, + threshold: number[], + fieldName: string +) => { + if (threshold.length === 0) { + throw new Error('Threshold value required'); + } + + function getThresholdString(thresh: number) { + return Number.isInteger(thresh) ? `${thresh}L` : `${thresh}`; + } + + switch (comparator) { + case Comparator.LT: + return `${fieldName} < ${getThresholdString(threshold[0])}`; + case Comparator.LT_OR_EQ: + return `${fieldName} <= ${getThresholdString(threshold[0])}`; + case Comparator.GT: + return `${fieldName} > ${getThresholdString(threshold[0])}`; + case Comparator.GT_OR_EQ: + return `${fieldName} >= ${getThresholdString(threshold[0])}`; + case Comparator.BETWEEN: + if (threshold.length < 2) { + throw new Error('Threshold values required'); + } + return `${fieldName} >= ${getThresholdString( + threshold[0] + )} && ${fieldName} <= ${getThresholdString(threshold[1])}`; + case Comparator.NOT_BETWEEN: + if (threshold.length < 2) { + throw new Error('Threshold values required'); + } + return `${fieldName} < ${getThresholdString( + threshold[0] + )} || ${fieldName} > ${getThresholdString(threshold[1])}`; + } +}; + export const getComparatorSchemaType = (validate: (comparator: Comparator) => string | void) => schema.oneOf( [ diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index f7257651f2aeb..9005a8435657b 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { TRANSFORM_RULE_TYPE } from '@kbn/transform-plugin/common'; -import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; +import { ID as IndexThreshold } from './alert_types/index_threshold/rule_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/constants'; import { STACK_ALERTS_FEATURE_ID } from '../common'; diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 8f18ac9a4df9a..5483124209028 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; import { AlertingBuiltinsPlugin } from './plugin'; import { configSchema, Config } from '../common/config'; -export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type'; +export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/rule_type'; export const config: PluginConfigDescriptor = { exposeToBrowser: {}, diff --git a/x-pack/plugins/triggers_actions_ui/common/data/index.ts b/x-pack/plugins/triggers_actions_ui/common/data/index.ts index e368c77fe5479..e0c0f240c3b2f 100644 --- a/x-pack/plugins/triggers_actions_ui/common/data/index.ts +++ b/x-pack/plugins/triggers_actions_ui/common/data/index.ts @@ -7,6 +7,7 @@ export interface TimeSeriesResult { results: TimeSeriesResultRow[]; + truncated: boolean; } export interface TimeSeriesResultRow { diff --git a/x-pack/plugins/triggers_actions_ui/server/data/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/index.ts index c65f44162ecd7..aae400a7469ea 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/index.ts @@ -11,6 +11,7 @@ import { registerRoutes } from './routes'; export type { TimeSeriesQuery, CoreQueryParams } from './lib'; export { + TIME_SERIES_BUCKET_SELECTOR_FIELD, CoreQueryParamsSchemaProperties, validateCoreQueryBody, validateTimeWindowUnits, diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts index c76eb1cafa867..b99f278837f26 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts @@ -6,6 +6,7 @@ */ export type { TimeSeriesQuery } from './time_series_query'; +export { TIME_SERIES_BUCKET_SELECTOR_FIELD } from './time_series_query'; export type { CoreQueryParams } from './core_query_types'; export { CoreQueryParamsSchemaProperties, diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts index dc037dde8f499..5e2550e15f35d 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts @@ -5,9 +5,6 @@ * 2.0. */ -// test error conditions of calling timeSeriesQuery - postive results tested in FT - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { Logger } from '@kbn/core/server'; import { TimeSeriesQuery, timeSeriesQuery, getResultFromEs } from './time_series_query'; @@ -20,9 +17,9 @@ const DefaultQueryParams: TimeSeriesQuery = { aggField: undefined, timeWindowSize: 5, timeWindowUnit: 'm', - dateStart: undefined, - dateEnd: undefined, - interval: undefined, + dateStart: '2021-04-22T15:19:31Z', + dateEnd: '2021-04-22T15:20:31Z', + interval: '1m', groupBy: 'all', termField: undefined, termSize: undefined, @@ -37,6 +34,10 @@ describe('timeSeriesQuery', () => { query: DefaultQueryParams, }; + beforeEach(() => { + jest.clearAllMocks(); + }); + it('fails as expected when the callCluster call fails', async () => { esClient.search.mockRejectedValue(new Error('woopsie')); await timeSeriesQuery(params); @@ -48,141 +49,1560 @@ describe('timeSeriesQuery', () => { }); it('fails as expected when the query params are invalid', async () => { - params.query = { ...params.query, dateStart: 'x' }; - expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot( - `"invalid date format for dateStart: \\"x\\""` + expect( + timeSeriesQuery({ ...params, query: { ...params.query, dateStart: 'x' } }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid date format for dateStart: \\"x\\""`); + }); + + it('should create correct query when aggType=count and termField is undefined (count over all) and selector params are undefined', async () => { + await timeSeriesQuery(params); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType=count and termField is undefined (count over all) and selector params are defined', async () => { + await timeSeriesQuery({ + ...params, + condition: { + resultLimit: 1000, + conditionScript: `params.compareValue > 1`, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType=count and termField is specified (count over top N termField) and selector params are undefined', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + termField: 'the-term', + termSize: 10, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + groupAgg: { + terms: { + field: 'the-term', + size: 10, + }, + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + }, + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType=count and termField is specified (count over top N termField) and selector params are defined', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + termField: 'the-term', + termSize: 10, + }, + condition: { + resultLimit: 1000, + conditionScript: `params.compareValue > 1`, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + groupAgg: { + terms: { + field: 'the-term', + size: 10, + }, + aggs: { + conditionSelector: { + bucket_selector: { + buckets_path: { + compareValue: '_count', + }, + script: `params.compareValue > 1`, + }, + }, + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + }, + }, + }, + groupAggCount: { + stats_bucket: { + buckets_path: 'groupAgg._count', + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType!=count and termField is undefined (aggregate metric over all) and selector params are undefined', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + aggType: 'avg', + aggField: 'avg-field', + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + aggs: { + metricAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + }, + sortValueAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType!=count and termField is undefined (aggregate metric over all) and selector params are defined', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + aggType: 'avg', + aggField: 'avg-field', + }, + condition: { + resultLimit: 1000, + conditionScript: `params.compareValue > 1`, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + aggs: { + metricAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + }, + sortValueAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType!=count and termField is specified (aggregate metric over top N termField) and selector params are undefined', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + aggType: 'avg', + aggField: 'avg-field', + termField: 'the-field', + termSize: 20, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + groupAgg: { + terms: { + field: 'the-field', + order: { + sortValueAgg: 'desc', + }, + size: 20, + }, + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + aggs: { + metricAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + }, + sortValueAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should create correct query when aggType!=count and termField is specified (aggregate metric over top N termField) and selector params are defined', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + aggType: 'avg', + aggField: 'avg-field', + termField: 'the-field', + termSize: 20, + }, + condition: { + resultLimit: 1000, + conditionScript: `params.compareValue > 1`, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + groupAgg: { + terms: { + field: 'the-field', + order: { + sortValueAgg: 'desc', + }, + size: 20, + }, + aggs: { + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + aggs: { + metricAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + }, + conditionSelector: { + bucket_selector: { + buckets_path: { + compareValue: 'sortValueAgg', + }, + script: 'params.compareValue > 1', + }, + }, + sortValueAgg: { + avg: { + field: 'avg-field', + }, + }, + }, + }, + groupAggCount: { + stats_bucket: { + buckets_path: 'groupAgg._count', + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } + ); + }); + + it('should correctly apply the resultLimit if specified', async () => { + await timeSeriesQuery({ + ...params, + query: { + ...params.query, + termField: 'the-term', + termSize: 100, + }, + condition: { + resultLimit: 5, + conditionScript: `params.compareValue > 1`, + }, + }); + expect(esClient.search).toHaveBeenCalledWith( + { + allow_no_indices: true, + body: { + aggs: { + groupAgg: { + terms: { + field: 'the-term', + size: 6, + }, + aggs: { + conditionSelector: { + bucket_selector: { + buckets_path: { + compareValue: '_count', + }, + script: `params.compareValue > 1`, + }, + }, + dateAgg: { + date_range: { + field: 'time-field', + format: 'strict_date_time', + ranges: [ + { + from: '2021-04-22T15:14:31.000Z', + to: '2021-04-22T15:19:31.000Z', + }, + { + from: '2021-04-22T15:15:31.000Z', + to: '2021-04-22T15:20:31.000Z', + }, + ], + }, + }, + }, + }, + groupAggCount: { + stats_bucket: { + buckets_path: 'groupAgg._count', + }, + }, + }, + query: { + bool: { + filter: { + range: { + 'time-field': { + format: 'strict_date_time', + gte: '2021-04-22T15:14:31.000Z', + lt: '2021-04-22T15:20:31.000Z', + }, + }, + }, + }, + }, + size: 0, + }, + ignore_unavailable: true, + index: 'index-name', + }, + { ignore: [404], meta: true } ); }); }); describe('getResultFromEs', () => { - it('correctly parses time series results for count aggregation', () => { + it('correctly parses time series results for count over all aggregation', () => { + // results should be same whether isConditionInQuery is true or false expect( - getResultFromEs(true, false, { - took: 0, - timed_out: false, - _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, - hits: { total: { value: 0, relation: 'eq' }, hits: [] }, - aggregations: { - dateAgg: { - buckets: [ - { - key: '2021-04-22T15:14:31.075Z-2021-04-22T15:19:31.075Z', - from: 1619104471075, - from_as_string: '2021-04-22T15:14:31.075Z', - to: 1619104771075, - to_as_string: '2021-04-22T15:19:31.075Z', - doc_count: 0, - }, - ], + getResultFromEs({ + isCountAgg: true, + isGroupAgg: false, + isConditionInQuery: true, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + dateAgg: { + buckets: [ + { + key: '2022-09-20T00:14:31.000Z-2022-09-20T23:19:31.000Z', + from: 1663632871000, + from_as_string: '2022-09-20T00:14:31.000Z', + to: 1663715971000, + to_as_string: '2022-09-20T23:19:31.000Z', + doc_count: 481, + }, + ], + }, }, }, - } as estypes.SearchResponse) + }) ).toEqual({ results: [ { group: 'all documents', - metrics: [['2021-04-22T15:19:31.075Z', 0]], + metrics: [['2022-09-20T23:19:31.000Z', 481]], }, ], + truncated: false, + }); + + expect( + getResultFromEs({ + isCountAgg: true, + isGroupAgg: false, + isConditionInQuery: false, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + dateAgg: { + buckets: [ + { + key: '2022-09-20T00:14:31.000Z-2022-09-20T23:19:31.000Z', + from: 1663632871000, + from_as_string: '2022-09-20T00:14:31.000Z', + to: 1663715971000, + to_as_string: '2022-09-20T23:19:31.000Z', + doc_count: 481, + }, + ], + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'all documents', + metrics: [['2022-09-20T23:19:31.000Z', 481]], + }, + ], + truncated: false, }); }); - it('correctly parses time series results with no aggregation data for count aggregation', () => { + it('correctly parses time series results with no aggregation data for count over all aggregation', () => { // this could happen with cross cluster searches when cluster permissions are incorrect // the query completes but doesn't return any aggregations + + // results should be same whether isConditionInQuery is true or false expect( - getResultFromEs(true, false, { - took: 0, - timed_out: false, - _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, - _clusters: { total: 1, successful: 1, skipped: 0 }, - hits: { total: { value: 0, relation: 'eq' }, hits: [] }, - } as estypes.SearchResponse) + getResultFromEs({ + isCountAgg: true, + isGroupAgg: false, + isConditionInQuery: true, + esResult: { + took: 0, + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + _clusters: { total: 1, successful: 1, skipped: 0 }, + hits: { total: { value: 0, relation: 'eq' }, hits: [] }, + }, + }) ).toEqual({ results: [], + truncated: false, + }); + + expect( + getResultFromEs({ + isCountAgg: true, + isGroupAgg: false, + isConditionInQuery: false, + esResult: { + took: 0, + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + _clusters: { total: 1, successful: 1, skipped: 0 }, + hits: { total: { value: 0, relation: 'eq' }, hits: [] }, + }, + }) + ).toEqual({ + results: [], + truncated: false, }); }); - it('correctly parses time series results for group aggregation', () => { + it('correctly parses time series results for count over top N termField aggregation when isConditionInQuery = false', () => { expect( - getResultFromEs(false, true, { - took: 1, - timed_out: false, - _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, - hits: { total: { value: 298, relation: 'eq' }, hits: [] }, - aggregations: { - groupAgg: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'host-2', - doc_count: 149, - sortValueAgg: { value: 0.5000000018251423 }, - dateAgg: { - buckets: [ - { - key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', - from: 1619104723191, - from_as_string: '2021-04-22T15:18:43.191Z', - to: 1619105023191, - to_as_string: '2021-04-22T15:23:43.191Z', - doc_count: 149, - metricAgg: { value: 0.5000000018251423 }, - }, - ], + getResultFromEs({ + isCountAgg: true, + isGroupAgg: true, + isConditionInQuery: false, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + groupAgg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'host-2', + doc_count: 149, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 149, + }, + ], + }, }, - }, - { - key: 'host-1', - doc_count: 149, - sortValueAgg: { value: 0.5000000011000857 }, - dateAgg: { - buckets: [ - { - key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', - from: 1619104723191, - from_as_string: '2021-04-22T15:18:43.191Z', - to: 1619105023191, - to_as_string: '2021-04-22T15:23:43.191Z', - doc_count: 149, - metricAgg: { value: 0.5000000011000857 }, - }, - ], + { + key: 'host-1', + doc_count: 53, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 53, + }, + ], + }, }, - }, - ], + ], + }, }, }, - } as estypes.SearchResponse) + }) ).toEqual({ results: [ { group: 'host-2', + metrics: [['2021-04-22T15:23:43.191Z', 149]], + }, + { + group: 'host-1', + metrics: [['2021-04-22T15:23:43.191Z', 53]], + }, + ], + truncated: false, + }); + }); + + it('correctly parses time series results for count over top N termField aggregation when isConditionInQuery = true', () => { + expect( + getResultFromEs({ + isCountAgg: true, + isGroupAgg: true, + isConditionInQuery: true, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + groupAgg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'host-2', + doc_count: 149, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 149, + }, + ], + }, + }, + { + key: 'host-1', + doc_count: 53, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 53, + }, + ], + }, + }, + ], + }, + groupAggCount: { + count: 2, + min: 90, + max: 90, + avg: 90, + sum: 180, + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'host-2', + metrics: [['2021-04-22T15:23:43.191Z', 149]], + }, + { + group: 'host-1', + metrics: [['2021-04-22T15:23:43.191Z', 53]], + }, + ], + truncated: false, + }); + }); + + it('correctly returns truncated status for time series results for count over top N termField aggregation when isConditionInQuery = true', () => { + expect( + getResultFromEs({ + isCountAgg: true, + isGroupAgg: true, + isConditionInQuery: true, + resultLimit: 5, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + groupAgg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'host-2', + doc_count: 149, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 149, + }, + ], + }, + }, + { + key: 'host-1', + doc_count: 53, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 53, + }, + ], + }, + }, + { + key: 'host-3', + doc_count: 40, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 40, + }, + ], + }, + }, + { + key: 'host-6', + doc_count: 55, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 55, + }, + ], + }, + }, + { + key: 'host-9', + doc_count: 54, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 54, + }, + ], + }, + }, + { + key: 'host-11', + doc_count: 2, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 2, + }, + ], + }, + }, + ], + }, + groupAggCount: { + count: 6, + min: 90, + max: 90, + avg: 90, + sum: 180, + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'host-2', + metrics: [['2021-04-22T15:23:43.191Z', 149]], + }, + { + group: 'host-1', + metrics: [['2021-04-22T15:23:43.191Z', 53]], + }, + { + group: 'host-3', + metrics: [['2021-04-22T15:23:43.191Z', 40]], + }, + { + group: 'host-6', + metrics: [['2021-04-22T15:23:43.191Z', 55]], + }, + { + group: 'host-9', + metrics: [['2021-04-22T15:23:43.191Z', 54]], + }, + ], + truncated: true, + }); + }); + + it('correctly parses time series results with no aggregation data for count over top N termField aggregation', () => { + // this could happen with cross cluster searches when cluster permissions are incorrect + // the query completes but doesn't return any aggregations + + // results should be same whether isConditionInQuery is true or false + expect( + getResultFromEs({ + isCountAgg: true, + isGroupAgg: true, + isConditionInQuery: true, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + }, + }) + ).toEqual({ + results: [], + truncated: false, + }); + + expect( + getResultFromEs({ + isCountAgg: true, + isGroupAgg: true, + isConditionInQuery: false, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + }, + }) + ).toEqual({ + results: [], + truncated: false, + }); + }); + + it('correctly parses time series results for aggregate metric over all aggregation', () => { + // results should be same whether isConditionInQuery is true or false + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: false, + isConditionInQuery: true, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + sortValueAgg: { value: 0.5000000018251423 }, + dateAgg: { + buckets: [ + { + key: '2022-09-20T00:14:31.000Z-2022-09-20T23:19:31.000Z', + from: 1663632871000, + from_as_string: '2022-09-20T00:14:31.000Z', + to: 1663715971000, + to_as_string: '2022-09-20T23:19:31.000Z', + doc_count: 481, + metricAgg: { value: 0.5000000018251423 }, + }, + ], + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'all documents', + metrics: [['2022-09-20T23:19:31.000Z', 0.5000000018251423]], + }, + ], + truncated: false, + }); + + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: false, + isConditionInQuery: false, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + sortValueAgg: { value: 0.5000000018251423 }, + dateAgg: { + buckets: [ + { + key: '2022-09-20T00:14:31.000Z-2022-09-20T23:19:31.000Z', + from: 1663632871000, + from_as_string: '2022-09-20T00:14:31.000Z', + to: 1663715971000, + to_as_string: '2022-09-20T23:19:31.000Z', + doc_count: 481, + metricAgg: { value: 0.5000000018251423 }, + }, + ], + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'all documents', + metrics: [['2022-09-20T23:19:31.000Z', 0.5000000018251423]], + }, + ], + truncated: false, + }); + }); + + it('correctly parses time series results with no aggregation data for aggregate metric over all aggregation', () => { + // this could happen with cross cluster searches when cluster permissions are incorrect + // the query completes but doesn't return any aggregations + + // results should be same whether isConditionInQuery is true or false + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: false, + isConditionInQuery: true, + esResult: { + took: 0, + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + _clusters: { total: 1, successful: 1, skipped: 0 }, + hits: { total: { value: 0, relation: 'eq' }, hits: [] }, + }, + }) + ).toEqual({ + results: [], + truncated: false, + }); + + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: false, + isConditionInQuery: false, + esResult: { + took: 0, + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + _clusters: { total: 1, successful: 1, skipped: 0 }, + hits: { total: { value: 0, relation: 'eq' }, hits: [] }, + }, + }) + ).toEqual({ + results: [], + truncated: false, + }); + }); + + it('correctly parses time series results for aggregate metric over top N termField aggregation when isConditionInQuery = false', () => { + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: true, + isConditionInQuery: false, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + groupAgg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'host-2', + doc_count: 149, + sortValueAgg: { value: 0.7100000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 149, + metricAgg: { value: 0.7100000018251423 }, + }, + ], + }, + }, + { + key: 'host-1', + doc_count: 53, + sortValueAgg: { value: 0.5000000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 53, + metricAgg: { value: 0.5000000018251423 }, + }, + ], + }, + }, + ], + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'host-2', + metrics: [['2021-04-22T15:23:43.191Z', 0.7100000018251423]], + }, + { + group: 'host-1', + metrics: [['2021-04-22T15:23:43.191Z', 0.5000000018251423]], + }, + ], + truncated: false, + }); + }); + + it('correctly parses time series results for aggregate metric over top N termField aggregation when isConditionInQuery = true', () => { + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: true, + isConditionInQuery: true, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + groupAgg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'host-2', + doc_count: 149, + sortValueAgg: { value: 0.7100000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 149, + metricAgg: { value: 0.7100000018251423 }, + }, + ], + }, + }, + { + key: 'host-1', + doc_count: 53, + sortValueAgg: { value: 0.5000000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 53, + metricAgg: { value: 0.5000000018251423 }, + }, + ], + }, + }, + ], + }, + groupAggCount: { + count: 2, + min: 75, + max: 90, + avg: 82.5, + sum: 165, + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'host-2', + metrics: [['2021-04-22T15:23:43.191Z', 0.7100000018251423]], + }, + { + group: 'host-1', metrics: [['2021-04-22T15:23:43.191Z', 0.5000000018251423]], }, + ], + truncated: false, + }); + }); + + it('correctly returns truncated status for time series results for aggregate metrics over top N termField aggregation when isConditionInQuery = true', () => { + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: true, + isConditionInQuery: true, + resultLimit: 5, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + groupAgg: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'host-2', + doc_count: 149, + sortValueAgg: { value: 0.7100000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 149, + metricAgg: { value: 0.7100000018251423 }, + }, + ], + }, + }, + { + key: 'host-1', + doc_count: 53, + sortValueAgg: { value: 0.5000000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 53, + metricAgg: { value: 0.5000000018251423 }, + }, + ], + }, + }, + { + key: 'host-3', + doc_count: 40, + sortValueAgg: { value: 0.4900000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 40, + metricAgg: { value: 0.4900000018251423 }, + }, + ], + }, + }, + { + key: 'host-6', + doc_count: 55, + sortValueAgg: { value: 0.4600000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 55, + metricAgg: { value: 0.4600000018251423 }, + }, + ], + }, + }, + { + key: 'host-9', + doc_count: 54, + sortValueAgg: { value: 0.3300000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 54, + metricAgg: { value: 0.3300000018251423 }, + }, + ], + }, + }, + { + key: 'host-11', + doc_count: 2, + sortValueAgg: { value: 0.1200000018251423 }, + dateAgg: { + buckets: [ + { + key: '2021-04-22T15:18:43.191Z-2021-04-22T15:23:43.191Z', + from: 1619104723191, + from_as_string: '2021-04-22T15:18:43.191Z', + to: 1619105023191, + to_as_string: '2021-04-22T15:23:43.191Z', + doc_count: 2, + metricAgg: { value: 0.1200000018251423 }, + }, + ], + }, + }, + ], + }, + groupAggCount: { + count: 6, + min: 75, + max: 90, + avg: 82.5, + sum: 165, + }, + }, + }, + }) + ).toEqual({ + results: [ + { + group: 'host-2', + metrics: [['2021-04-22T15:23:43.191Z', 0.7100000018251423]], + }, { group: 'host-1', - metrics: [['2021-04-22T15:23:43.191Z', 0.5000000011000857]], + metrics: [['2021-04-22T15:23:43.191Z', 0.5000000018251423]], + }, + { + group: 'host-3', + metrics: [['2021-04-22T15:23:43.191Z', 0.4900000018251423]], + }, + { + group: 'host-6', + metrics: [['2021-04-22T15:23:43.191Z', 0.4600000018251423]], + }, + { + group: 'host-9', + metrics: [['2021-04-22T15:23:43.191Z', 0.3300000018251423]], }, ], + truncated: true, }); }); - it('correctly parses time series results with no aggregation data for group aggregation', () => { + it('correctly parses time series results with no aggregation data for aggregate metric over top N termField aggregation', () => { // this could happen with cross cluster searches when cluster permissions are incorrect // the query completes but doesn't return any aggregations + + // results should be same whether isConditionInQuery is true or false + expect( + getResultFromEs({ + isCountAgg: false, + isGroupAgg: true, + isConditionInQuery: true, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + }, + }) + ).toEqual({ + results: [], + truncated: false, + }); + expect( - getResultFromEs(false, true, { - took: 0, - timed_out: false, - _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, - _clusters: { total: 1, successful: 1, skipped: 0 }, - hits: { total: { value: 0, relation: 'eq' }, hits: [] }, - } as estypes.SearchResponse) + getResultFromEs({ + isCountAgg: false, + isGroupAgg: true, + isConditionInQuery: false, + esResult: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 481, relation: 'eq' }, max_score: null, hits: [] }, + }, + }) ).toEqual({ results: [], + truncated: false, }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index 6f5ebbe34a891..885be0bf59f5b 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -12,19 +12,28 @@ import { getEsErrorMessage } from '@kbn/alerting-plugin/server'; import { DEFAULT_GROUPS } from '..'; import { getDateRangeInfo } from './date_range_info'; -import { TimeSeriesQuery, TimeSeriesResult, TimeSeriesResultRow } from './time_series_types'; +import { + TimeSeriesQuery, + TimeSeriesResult, + TimeSeriesResultRow, + TimeSeriesCondition, +} from './time_series_types'; export type { TimeSeriesQuery, TimeSeriesResult } from './time_series_types'; +export const TIME_SERIES_BUCKET_SELECTOR_PATH_NAME = 'compareValue'; +export const TIME_SERIES_BUCKET_SELECTOR_FIELD = `params.${TIME_SERIES_BUCKET_SELECTOR_PATH_NAME}`; + export interface TimeSeriesQueryParameters { logger: Logger; esClient: ElasticsearchClient; query: TimeSeriesQuery; + condition?: TimeSeriesCondition; } export async function timeSeriesQuery( params: TimeSeriesQueryParameters ): Promise { - const { logger, esClient, query: queryParams } = params; + const { logger, esClient, query: queryParams, condition: conditionParams } = params; const { index, timeWindowSize, timeWindowUnit, interval, timeField, dateStart, dateEnd } = queryParams; @@ -62,6 +71,22 @@ export async function timeSeriesQuery( const isCountAgg = aggType === 'count'; const isGroupAgg = !!termField; + const includeConditionInQuery = !!conditionParams; + + // Cap the maximum number of terms returned to the resultLimit if defined + // Use resultLimit + 1 because we're using the bucket selector aggregation + // to apply the threshold condition to the ES query. We don't seem to be + // able to get the true cardinality from the bucket selector (i.e., get + // the number of buckets that matched the selector condition without actually + // retrieving the bucket data). By using resultLimit + 1, we can count the number + // of buckets returned and if the value is greater than resultLimit, we know that + // there is additional alert data that we're not returning. + let terms = termSize || DEFAULT_GROUPS; + terms = includeConditionInQuery + ? terms > conditionParams.resultLimit + ? conditionParams.resultLimit + 1 + : terms + : terms; let aggParent = esQuery.body; @@ -71,9 +96,18 @@ export async function timeSeriesQuery( groupAgg: { terms: { field: termField, - size: termSize || DEFAULT_GROUPS, + size: terms, }, }, + ...(includeConditionInQuery + ? { + groupAggCount: { + stats_bucket: { + buckets_path: 'groupAgg._count', + }, + }, + } + : {}), }; // if not count add an order @@ -82,6 +116,17 @@ export async function timeSeriesQuery( aggParent.aggs.groupAgg.terms.order = { sortValueAgg: sortOrder, }; + } else if (includeConditionInQuery) { + aggParent.aggs.groupAgg.aggs = { + conditionSelector: { + bucket_selector: { + buckets_path: { + [TIME_SERIES_BUCKET_SELECTOR_PATH_NAME]: '_count', + }, + script: conditionParams.conditionScript, + }, + }, + }; } aggParent = aggParent.aggs.groupAgg; @@ -89,6 +134,7 @@ export async function timeSeriesQuery( // next, add the time window aggregation aggParent.aggs = { + ...aggParent.aggs, dateAgg: { date_range: { field: timeField, @@ -105,6 +151,17 @@ export async function timeSeriesQuery( field: aggField, }, }; + + if (isGroupAgg && includeConditionInQuery) { + aggParent.aggs.conditionSelector = { + bucket_selector: { + buckets_path: { + [TIME_SERIES_BUCKET_SELECTOR_PATH_NAME]: 'sortValueAgg', + }, + script: conditionParams.conditionScript, + }, + }; + } } aggParent = aggParent.aggs.dateAgg; @@ -133,19 +190,35 @@ export async function timeSeriesQuery( } catch (err) { // console.log('time_series_query.ts error\n', JSON.stringify(err, null, 4)); logger.warn(`${logPrefix} error: ${getEsErrorMessage(err)}`); - return { results: [] }; + return { results: [], truncated: false }; } // console.log('time_series_query.ts response\n', JSON.stringify(esResult, null, 4)); logger.debug(`${logPrefix} result: ${JSON.stringify(esResult)}`); - return getResultFromEs(isCountAgg, isGroupAgg, esResult); + return getResultFromEs({ + isCountAgg, + isGroupAgg, + isConditionInQuery: includeConditionInQuery, + esResult, + resultLimit: conditionParams?.resultLimit, + }); } -export function getResultFromEs( - isCountAgg: boolean, - isGroupAgg: boolean, - esResult: estypes.SearchResponse -): TimeSeriesResult { +interface GetResultFromEsParams { + isCountAgg: boolean; + isGroupAgg: boolean; + isConditionInQuery: boolean; + esResult: estypes.SearchResponse; + resultLimit?: number; +} + +export function getResultFromEs({ + isCountAgg, + isGroupAgg, + isConditionInQuery, + esResult, + resultLimit, +}: GetResultFromEsParams): TimeSeriesResult { const aggregations = esResult?.aggregations || {}; // add a fake 'all documents' group aggregation, if a group aggregation wasn't used @@ -161,11 +234,16 @@ export function getResultFromEs( // @ts-expect-error specify aggregations type explicitly const groupBuckets = aggregations.groupAgg?.buckets || []; + // @ts-expect-error specify aggregations type explicitly + const numGroupsTotal = aggregations.groupAggCount?.count ?? 0; const result: TimeSeriesResult = { results: [], + truncated: isConditionInQuery && resultLimit ? numGroupsTotal > resultLimit : false, }; for (const groupBucket of groupBuckets) { + if (resultLimit && result.results.length === resultLimit) break; + const groupName: string = `${groupBucket?.key}`; const dateBuckets = groupBucket?.dateAgg?.buckets || []; const groupResult: TimeSeriesResultRow = { diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts index d59d99c59419f..491bea6522dec 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts @@ -45,6 +45,13 @@ export const TimeSeriesQuerySchema = schema.object( } ); +export const TimeSeriesConditionSchema = schema.object({ + resultLimit: schema.number(), + conditionScript: schema.string({ minLength: 1 }), +}); + +export type TimeSeriesCondition = TypeOf; + // using direct type not allowed, circular reference, so body is typed to unknown function validateBody(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index 5b81caa235a29..e96ced7c7583c 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -17,6 +17,7 @@ export { MAX_INTERVALS, MAX_GROUPS, DEFAULT_GROUPS, + TIME_SERIES_BUCKET_SELECTOR_FIELD, } from './data'; export const config: PluginConfigDescriptor = { diff --git a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts index f66ad0bcd46e1..2af943785e612 100644 --- a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts +++ b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts @@ -50,6 +50,9 @@ export class ESTestIndexTool { testedValue: { type: 'long', }, + testedValueFloat: { + type: 'float', + }, testedValueUnsigned: { type: 'unsigned_long', }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index ab6a3023141a8..c06eb41759d63 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -265,6 +265,45 @@ export default function ruleTests({ getService }: FtrProviderContext) { expect(inGroup2).to.be.greaterThan(0); }); + it('runs correctly: max grouped on float', async () => { + await createRule({ + name: 'never fire', + aggType: 'max', + aggField: 'testedValueFloat', + groupBy: 'top', + termField: 'group', + termSize: 2, + thresholdComparator: '<', + threshold: [3.235423], + }); + + await createRule({ + name: 'always fire', + aggType: 'max', + aggField: 'testedValueFloat', + groupBy: 'top', + termField: 'group', + termSize: 2, // two actions will fire each interval + thresholdComparator: '>=', + threshold: [200.2354364], + }); + + // create some more documents in the first group + await createEsDocumentsInGroups(1); + + const docs = await waitForDocs(4); + + for (const doc of docs) { + const { name, message } = doc._source.params; + + expect(name).to.be('always fire'); + + const messagePattern = + /alert 'always fire' is active for group \'group-\d\':\n\n- Value: 234.2534637451172\n- Conditions Met: max\(testedValueFloat\) is greater than or equal to 200.2354364 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + } + }); + it('runs correctly: max grouped on unsigned long', async () => { await createRule({ name: 'never fire', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index 5eee05a916da3..61fec5cf7e481 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -82,6 +82,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider const expected = { results: [{ group: 'all documents', metrics: [[START_DATE_PLUS_YEAR, 0]] }], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -95,6 +96,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider const expected = { results: [{ group: 'all documents', metrics: [[START_DATE_MINUS_YEAR, 0]] }], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -108,6 +110,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider const expected = { results: [{ group: 'all documents', metrics: [[START_DATE, 6]] }], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -130,6 +133,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider ], }, ], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -154,6 +158,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider ], }, ], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -185,6 +190,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider ], }, ], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -220,6 +226,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider ], }, ], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); @@ -289,6 +296,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider }); const expected = { results: [{ group: 'all documents', metrics: [[START_DATE, 6]] }], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); }); @@ -303,6 +311,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider }); const expected = { results: [], + truncated: false, }; expect(await runQueryExpect(query, 200)).eql(expected); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts index 8fbb4d0bc6805..58df573fa629e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts @@ -106,6 +106,7 @@ async function createEsDocument( date: new Date(epochMillis).toISOString(), date_epoch_millis: epochMillis, testedValue, + testedValueFloat: 234.2534643, testedValueUnsigned: '18446744073709551615', '@timestamp': new Date(epochMillis).toISOString(), ...(group ? { group } : {}), From 42f0868a0451b3ae34539d030ae975278914dbf9 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 6 Oct 2022 17:23:53 +0300 Subject: [PATCH 2/7] Fix Failing test: Jest Tests.x-pack/plugins/lens/common/expressions/time_scale - time_scale should work with relative time range (#142837) Closes: #142820 --- .../common/expressions/time_scale/time_scale.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts index fceabbd5542b3..a2b7d730766c7 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts @@ -22,7 +22,11 @@ describe('time_scale', () => { context?: ExecutionContext ) => Promise; - const timeScale = getTimeScale(createDatatableUtilitiesMock, () => 'UTC'); + const timeScale = getTimeScale( + createDatatableUtilitiesMock, + () => 'UTC', + () => new Date('2010-01-04T06:30:30') + ); const emptyTable: Datatable = { type: 'datatable', @@ -390,7 +394,6 @@ describe('time_scale', () => { ...emptyTable, rows: [ { - date: moment('2010-01-01T00:00:00.000Z').valueOf(), metric: 300, }, ], @@ -419,7 +422,6 @@ describe('time_scale', () => { ...emptyTable, rows: [ { - date: moment().subtract('1d').valueOf(), metric: 300, }, ], @@ -432,14 +434,14 @@ describe('time_scale', () => { { getSearchContext: () => ({ timeRange: { - from: 'now-10d', + from: 'now-2d', to: 'now', }, }), } as unknown as ExecutionContext ); - expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([30]); + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([150]); }); it('should apply fn for non-histogram fields (with Reduced time range)', async () => { From 8efefb11ef54f1735a2a98f9df57cb4148dc6fae Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 6 Oct 2022 10:25:32 -0400 Subject: [PATCH 3/7] synthetics - unskip flaky test (#142801) --- .../api_integration/apis/uptime/rest/add_monitor_project.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts index 9b0ae053566b2..2be3509a61cf6 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts @@ -19,8 +19,7 @@ import { PrivateLocationTestService } from './services/private_location_test_ser import { comparePolicies, getTestProjectSyntheticsPolicy } from './sample_data/test_policy'; export default function ({ getService }: FtrProviderContext) { - // FLAKY: https://github.com/elastic/kibana/issues/142110 - describe.skip('AddProjectMonitors', function () { + describe('AddProjectMonitors', function () { this.tags('skipCloud'); const supertest = getService('supertest'); @@ -708,6 +707,7 @@ export default function ({ getService }: FtrProviderContext) { ...projectMonitors, keep_stale: false, monitors: testMonitors, + project: 'test-project-2', }); const messages = await parseStreamApiResponse( @@ -715,6 +715,7 @@ export default function ({ getService }: FtrProviderContext) { JSON.stringify({ ...projectMonitors, keep_stale: false, + project: 'test-project-2', }) ); From 4142f65ba63d41b0e66bce2023c08488ca964116 Mon Sep 17 00:00:00 2001 From: Marius Dragomir Date: Thu, 6 Oct 2022 10:37:06 -0400 Subject: [PATCH 4/7] change reporting url due to sample data dashboard change (#142794) --- .../apps/reporting/reporting_watcher.js | 4 ++-- .../apps/reporting/reporting_watcher_png.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js index 869d56fe90047..ca6ad964d617f 100644 --- a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js +++ b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }) { const watch = { id }; const interval = 10; const emails = REPORTING_TEST_EMAILS.split(','); - + // http://localhost:5601/api/reporting/generate/printablePdfV2?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A2024%2Cwidth%3A1920%29%2Cid%3Apreserve_layout%29%2ClocatorParams%3A%21%28%28id%3ADASHBOARD_APP_LOCATOR%2Cparams%3A%28dashboardId%3A%27722b74f0-b882-11e8-a6d9-e546fe2bba5f%27%2CpreserveSavedFilters%3A%21t%2CtimeRange%3A%28from%3Anow-7d%2Cto%3Anow%29%2CuseHash%3A%21f%2CviewMode%3Aview%29%29%29%2CobjectType%3Adashboard%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.6.0-SNAPSHOT%27%29 // https://localhost:5601/api/reporting/generate/printablePdf?jobParams=(objectType:dashboard,queryString:%27_g%3D(refreshInterval%3A(display%3AOff%2Cpause%3A!!f%2Cvalue%3A0)%2Ctime%3A(from%3Anow-7d%2Cmode%3Aquick%2Cto%3Anow))%26_a%3D(description%3A%2527%2527%2Cfilters%3A!!()%2CfullScreenMode%3A!!f%2Coptions%3A(darkTheme%3A!!f)%2Cpanels%3A!!((col%3A1%2Cid%3ASystem-Navigation%2CpanelIndex%3A9%2Crow%3A1%2Csize_x%3A8%2Csize_y%3A1%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3Ac6f2ffd0-4d17-11e7-a196-69b9a7a020a9%2CpanelIndex%3A11%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3Afe064790-1b1f-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A12%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%2527855899e0-1b1c-11e7-b09e-037021c4f8df%2527%2CpanelIndex%3A13%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%25277cdb1330-4d1a-11e7-a196-69b9a7a020a9%2527%2CpanelIndex%3A14%2Crow%3A9%2Csize_x%3A12%2Csize_y%3A6%2Ctype%3Avisualization)%2C(col%3A9%2Cid%3A%2527522ee670-1b92-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A16%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A11%2Cid%3A%25271aae9140-1b93-11e7-8ada-3df93aab833e%2527%2CpanelIndex%3A17%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3A%2527825fdb80-4d1d-11e7-b5f2-2b7c1895bf32%2527%2CpanelIndex%3A18%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A5%2Cid%3Ad3166e80-1b91-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A19%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A3%2Cid%3A%252783e12df0-1b91-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A20%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A9%2Cid%3Ae9d22060-4d64-11e7-aa29-87a97a796de6%2CpanelIndex%3A21%2Crow%3A1%2Csize_x%3A4%2Csize_y%3A1%2Ctype%3Avisualization))%2Cquery%3A(language%3Alucene%2Cquery%3A(query_string%3A(analyze_wildcard%3A!!t%2Cquery%3A%2527*%2527)))%2CtimeRestore%3A!!f%2Ctitle%3A%2527Metricbeat%2Bsystem%2Boverview%2527%2CuiState%3A(P-11%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-12%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-14%3A(vis%3A(defaultColors%3A(%25270%2525%2B-%2B8.75%2525%2527%3A%2527rgb(247%2C252%2C245)%2527%2C%252717.5%2525%2B-%2B26.25%2525%2527%3A%2527rgb(116%2C196%2C118)%2527%2C%252726.25%2525%2B-%2B35%2525%2527%3A%2527rgb(35%2C139%2C69)%2527%2C%25278.75%2525%2B-%2B17.5%2525%2527%3A%2527rgb(199%2C233%2C192)%2527)))%2CP-16%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-2%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-3%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527))))%2CviewMode%3Aview)%27,savedObjectId:Metricbeat-system-overview) // https://localhost:5601/api/reporting/generate/printablePdf?jobParams=(objectType:dashboard,queryString:%27_g%3D()%26_a%3D(description%3A%2527%2527%2Cfilters%3A!!()%2CfullScreenMode%3A!!f%2Coptions%3A(darkTheme%3A!!f)%2Cpanels%3A!!((col%3A1%2Cid%3ASystem-Navigation%2CpanelIndex%3A9%2Crow%3A1%2Csize_x%3A12%2Csize_y%3A1%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3Ac6f2ffd0-4d17-11e7-a196-69b9a7a020a9%2CpanelIndex%3A11%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3Afe064790-1b1f-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A12%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%2527855899e0-1b1c-11e7-b09e-037021c4f8df%2527%2CpanelIndex%3A13%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%25277cdb1330-4d1a-11e7-a196-69b9a7a020a9%2527%2CpanelIndex%3A14%2Crow%3A9%2Csize_x%3A12%2Csize_y%3A6%2Ctype%3Avisualization)%2C(col%3A9%2Cid%3A%2527522ee670-1b92-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A16%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A11%2Cid%3A%25271aae9140-1b93-11e7-8ada-3df93aab833e%2527%2CpanelIndex%3A17%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3A%2527825fdb80-4d1d-11e7-b5f2-2b7c1895bf32%2527%2CpanelIndex%3A18%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A5%2Cid%3Ad3166e80-1b91-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A19%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A3%2Cid%3A%252783e12df0-1b91-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A20%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization))%2Cquery%3A(language%3Alucene%2Cquery%3A(query_string%3A(analyze_wildcard%3A!!t%2Cdefault_field%3A%2527*%2527%2Cquery%3A%2527*%2527)))%2CtimeRestore%3A!!f%2Ctitle%3A%2527%255BMetricbeat%2BSystem%255D%2BOverview%2527%2CuiState%3A(P-11%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-12%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-14%3A(vis%3A(defaultColors%3A(%25270%2525%2B-%2B8.75%2525%2527%3A%2527rgb(247%2C252%2C245)%2527%2C%252717.5%2525%2B-%2B26.25%2525%2527%3A%2527rgb(116%2C196%2C118)%2527%2C%252726.25%2525%2B-%2B35%2525%2527%3A%2527rgb(35%2C139%2C69)%2527%2C%25278.75%2525%2B-%2B17.5%2525%2527%3A%2527rgb(199%2C233%2C192)%2527)))%2CP-16%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-2%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-3%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527))))%2CviewMode%3Aview)%27,savedObjectId:Metricbeat-system-overview) // https://localhost:5601 @@ -40,7 +40,7 @@ export default function ({ getService, getPageObjects }) { KIBANAIP + ':' + servers.kibana.port + - '/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AEurope%2FParis%2Clayout%3A%28dimensions%3A%28height%3A2052%2Cwidth%3A2542.666748046875%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fapp%2Fdashboards%23%2Fview%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D%28filters%3A%21%21%28%29%29%26_a%3D%28description%3A%21%27Analyze%2520mock%2520eCommerce%2520orders%2520and%2520revenue%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cpanels%3A%21%21%28%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%275%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A22%29%2Cid%3A%21%2745e07720-b890-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%275%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3A%21%277%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A15%29%2Cid%3Ab80e6540-b891-11e8-a6d9-e546fe2bba5f%2CpanelIndex%3A%21%277%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A18%2Ci%3A%21%2710%21%27%2Cw%3A48%2Cx%3A0%2Cy%3A55%29%2Cid%3A%21%273ba638e0-b894-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%2710%21%27%2Ctype%3Asearch%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChiddenLayers%3A%21%21%28%29%2CisLayerTOCOpen%3A%21%21f%2CmapBuffer%3A%28maxLat%3A66.51326%2CmaxLon%3A90%2CminLat%3A0%2CminLon%3A-135%29%2CmapCenter%3A%28lat%3A45.88578%2Clon%3A-15.07605%2Czoom%3A2.11%29%2CopenTOCDetails%3A%21%21%28%29%29%2CgridData%3A%28h%3A14%2Ci%3A%21%2711%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A32%29%2Cid%3A%21%272c9c1f60-1909-11e9-919b-ffe5949a18d2%21%27%2CpanelIndex%3A%21%2711%21%27%2Ctype%3Amap%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa71cf076-6895-491c-8878-63592e429ed5%2Cw%3A18%2Cx%3A0%2Cy%3A0%29%2Cid%3Ac00d1f90-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa71cf076-6895-491c-8878-63592e429ed5%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Cw%3A30%2Cx%3A18%2Cy%3A0%29%2Cid%3Ac3378480-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A8%2Ci%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A7%29%2Cid%3Ac762b7a0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Ctitle%3A%21%27%2525%2520of%2520target%2520revenue%2520%28%2410k%29%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Cw%3A12%2Cx%3A24%2Cy%3A7%29%2Cid%3Ace02e260-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A7%29%2Cid%3Ad5f90030-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Cw%3A24%2Cx%3A0%2Cy%3A15%29%2Cid%3Adde978b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Cw%3A12%2Cx%3A24%2Cy%3A15%29%2Cid%3Ae3902840-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Cw%3A24%2Cx%3A24%2Cy%3A22%29%2Cid%3Aeddf7850-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A14%2Ci%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Cw%3A24%2Cx%3A24%2Cy%3A32%29%2Cid%3Aff6a21b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A9%2Ci%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Cw%3A24%2Cx%3A0%2Cy%3A46%29%2Cid%3A%21%2703071e90-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Ctitle%3A%21%27Top%2520products%2520this%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%2CtimeRange%3A%28from%3Anow-2w%2Cto%3Anow-1w%29%29%2CgridData%3A%28h%3A9%2Ci%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Cw%3A24%2Cx%3A24%2Cy%3A46%29%2Cid%3A%21%2706379e00-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Ctitle%3A%21%27Top%2520products%2520last%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2Ctags%3A%21%21%28%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27%255BeCommerce%255D%2520Revenue%2520Dashboard%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.0.0%27%29'; + '/api/reporting/generate/printablePdfV2?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A2024%2Cwidth%3A1920%29%2Cid%3Apreserve_layout%29%2ClocatorParams%3A%21%28%28id%3ADASHBOARD_APP_LOCATOR%2Cparams%3A%28dashboardId%3A%27722b74f0-b882-11e8-a6d9-e546fe2bba5f%27%2CpreserveSavedFilters%3A%21t%2CtimeRange%3A%28from%3Anow-7d%2Cto%3Anow%29%2CuseHash%3A%21f%2CviewMode%3Aview%29%29%29%2CobjectType%3Adashboard%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.6.0-SNAPSHOT%27%29'; const body = { trigger: { diff --git a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js index d793390488596..9b8f17c422272 100644 --- a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js +++ b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js @@ -26,6 +26,7 @@ export default ({ getService, getPageObjects }) => { describe('PNG Reporting watch', () => { let id = 'watcher_png_report-'; id = id + new Date().getTime(); // For debugging. + // http://localhost:5601/api/reporting/generate/pngV2?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A2024%2Cwidth%3A1920%29%2Cid%3Apreserve_layout%29%2ClocatorParams%3A%28id%3ADASHBOARD_APP_LOCATOR%2Cparams%3A%28dashboardId%3A%27722b74f0-b882-11e8-a6d9-e546fe2bba5f%27%2CpreserveSavedFilters%3A%21t%2CtimeRange%3A%28from%3Anow-7d%2Cto%3Anow%29%2CuseHash%3A%21f%2CviewMode%3Aview%29%29%2CobjectType%3Adashboard%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.6.0-SNAPSHOT%27%29 const watch = { id }; const reportingUrl = servers.kibana.protocol + @@ -33,7 +34,7 @@ export default ({ getService, getPageObjects }) => { KIBANAIP + ':' + servers.kibana.port + - '/api/reporting/generate/png?jobParams=%28browserTimezone%3AEurope%2FParis%2Clayout%3A%28dimensions%3A%28height%3A2052%2Cwidth%3A2542.666748046875%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fapp%2Fdashboards%23%2Fview%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D%28filters%3A%21%21%28%29%29%26_a%3D%28description%3A%21%27Analyze%2520mock%2520eCommerce%2520orders%2520and%2520revenue%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cpanels%3A%21%21%28%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%275%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A22%29%2Cid%3A%21%2745e07720-b890-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%275%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3A%21%277%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A15%29%2Cid%3Ab80e6540-b891-11e8-a6d9-e546fe2bba5f%2CpanelIndex%3A%21%277%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A18%2Ci%3A%21%2710%21%27%2Cw%3A48%2Cx%3A0%2Cy%3A55%29%2Cid%3A%21%273ba638e0-b894-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%2710%21%27%2Ctype%3Asearch%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChiddenLayers%3A%21%21%28%29%2CisLayerTOCOpen%3A%21%21f%2CmapBuffer%3A%28maxLat%3A66.51326%2CmaxLon%3A90%2CminLat%3A0%2CminLon%3A-135%29%2CmapCenter%3A%28lat%3A45.88578%2Clon%3A-15.07605%2Czoom%3A2.11%29%2CopenTOCDetails%3A%21%21%28%29%29%2CgridData%3A%28h%3A14%2Ci%3A%21%2711%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A32%29%2Cid%3A%21%272c9c1f60-1909-11e9-919b-ffe5949a18d2%21%27%2CpanelIndex%3A%21%2711%21%27%2Ctype%3Amap%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa71cf076-6895-491c-8878-63592e429ed5%2Cw%3A18%2Cx%3A0%2Cy%3A0%29%2Cid%3Ac00d1f90-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa71cf076-6895-491c-8878-63592e429ed5%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Cw%3A30%2Cx%3A18%2Cy%3A0%29%2Cid%3Ac3378480-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A8%2Ci%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A7%29%2Cid%3Ac762b7a0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Ctitle%3A%21%27%2525%2520of%2520target%2520revenue%2520%28%2410k%29%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Cw%3A12%2Cx%3A24%2Cy%3A7%29%2Cid%3Ace02e260-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A7%29%2Cid%3Ad5f90030-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Cw%3A24%2Cx%3A0%2Cy%3A15%29%2Cid%3Adde978b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Cw%3A12%2Cx%3A24%2Cy%3A15%29%2Cid%3Ae3902840-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Cw%3A24%2Cx%3A24%2Cy%3A22%29%2Cid%3Aeddf7850-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A14%2Ci%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Cw%3A24%2Cx%3A24%2Cy%3A32%29%2Cid%3Aff6a21b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A9%2Ci%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Cw%3A24%2Cx%3A0%2Cy%3A46%29%2Cid%3A%21%2703071e90-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Ctitle%3A%21%27Top%2520products%2520this%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%2CtimeRange%3A%28from%3Anow-2w%2Cto%3Anow-1w%29%29%2CgridData%3A%28h%3A9%2Ci%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Cw%3A24%2Cx%3A24%2Cy%3A46%29%2Cid%3A%21%2706379e00-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Ctitle%3A%21%27Top%2520products%2520last%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2Ctags%3A%21%21%28%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27%255BeCommerce%255D%2520Revenue%2520Dashboard%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.0.0%27%29'; + '/api/reporting/generate/pngV2?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A2024%2Cwidth%3A1920%29%2Cid%3Apreserve_layout%29%2ClocatorParams%3A%28id%3ADASHBOARD_APP_LOCATOR%2Cparams%3A%28dashboardId%3A%27722b74f0-b882-11e8-a6d9-e546fe2bba5f%27%2CpreserveSavedFilters%3A%21t%2CtimeRange%3A%28from%3Anow-7d%2Cto%3Anow%29%2CuseHash%3A%21f%2CviewMode%3Aview%29%29%2CobjectType%3Adashboard%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.6.0-SNAPSHOT%27%29'; const emails = REPORTING_TEST_EMAILS.split(','); const interval = 10; const body = { From 022e59f241049b90eb7a1e45814a290b48d5ee2d Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 6 Oct 2022 15:46:03 +0100 Subject: [PATCH 5/7] Bugfix: Refresh search results when clearing category filter (#142853) --- .../integrations/hooks/use_local_search.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx index 58d3a847d3efc..7539197e8462f 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx @@ -6,7 +6,7 @@ */ import { Search as LocalSearch, PrefixIndexStrategy } from 'js-search'; -import { useEffect, useRef } from 'react'; +import { useRef } from 'react'; import type { IntegrationCardItem } from '../../../../common/types/models'; @@ -16,13 +16,11 @@ export const fieldsToSearch = ['name', 'title']; export function useLocalSearch(packageList: IntegrationCardItem[]) { const localSearchRef = useRef(new LocalSearch(searchIdField)); - useEffect(() => { - const localSearch = new LocalSearch(searchIdField); - localSearch.indexStrategy = new PrefixIndexStrategy(); - fieldsToSearch.forEach((field) => localSearch.addIndex(field)); - localSearch.addDocuments(packageList); - localSearchRef.current = localSearch; - }, [packageList]); + const localSearch = new LocalSearch(searchIdField); + localSearch.indexStrategy = new PrefixIndexStrategy(); + fieldsToSearch.forEach((field) => localSearch.addIndex(field)); + localSearch.addDocuments(packageList); + localSearchRef.current = localSearch; return localSearchRef; } From f880edc50c0abfe7a7566ce805d62a512c508182 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 6 Oct 2022 17:47:18 +0300 Subject: [PATCH 6/7] [Cases] Fix bulk actions alignment (#142830) * Fix alignment * Remove utility bar * Fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/all_cases/table.tsx | 4 +- .../components/all_cases/utility_bar.tsx | 126 +++++++++------ .../__snapshots__/utility_bar.test.tsx.snap | 29 ---- .../utility_bar_group.test.tsx.snap | 9 -- .../utility_bar_section.test.tsx.snap | 11 -- .../utility_bar_text.test.tsx.snap | 7 - .../public/components/utility_bar/index.ts | 13 -- .../public/components/utility_bar/styles.tsx | 144 ------------------ .../utility_bar/utility_bar.test.tsx | 103 ------------- .../components/utility_bar/utility_bar.tsx | 20 --- .../utility_bar/utility_bar_action.test.tsx | 42 ----- .../utility_bar/utility_bar_action.tsx | 38 ----- .../utility_bar_bulk_actions.test.tsx | 89 ----------- .../utility_bar/utility_bar_bulk_actions.tsx | 67 -------- .../utility_bar/utility_bar_group.test.tsx | 26 ---- .../utility_bar/utility_bar_group.tsx | 20 --- .../utility_bar/utility_bar_section.test.tsx | 28 ---- .../utility_bar/utility_bar_section.tsx | 20 --- .../utility_bar/utility_bar_spacer.tsx | 20 --- .../utility_bar/utility_bar_text.test.tsx | 24 --- .../utility_bar/utility_bar_text.tsx | 21 --- x-pack/test/functional/services/cases/list.ts | 5 +- 22 files changed, 86 insertions(+), 780 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/index.ts delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/styles.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_spacer.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.tsx diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index b85f4ae1826d5..1f7382351e802 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -82,7 +82,7 @@ export const CasesTable: FunctionComponent = ({ ) : ( -
+ <> = ({ sorting={sorting} hasActions={false} /> -
+ ); }; CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index 415472574f25b..6daf9cb665116 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -6,18 +6,18 @@ */ import React, { FunctionComponent, useCallback, useState } from 'react'; -import { EuiContextMenu } from '@elastic/eui'; import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../utility_bar'; + EuiButtonEmpty, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiText, + useEuiTheme, +} from '@elastic/eui'; import * as i18n from './translations'; import { Case } from '../../../common/ui/types'; import { useRefreshCases } from './use_on_refresh_cases'; -import { UtilityBarBulkActions } from '../utility_bar/utility_bar_bulk_actions'; import { useBulkActions } from './use_bulk_actions'; import { useCasesContext } from '../cases_context/use_cases_context'; @@ -30,6 +30,7 @@ interface Props { export const CasesTableUtilityBar: FunctionComponent = React.memo( ({ isSelectorView, totalCases, selectedCases, deselectCases }) => { + const { euiTheme } = useEuiTheme(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); const closePopover = useCallback(() => setIsPopoverOpen(false), []); @@ -56,47 +57,82 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( return ( <> - - - - - {i18n.SHOWING_CASES(totalCases)} - - - + + + + {i18n.SHOWING_CASES(totalCases)} + + + + {!isSelectorView && showBulkActions && ( <> - - {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - - - - + + + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} + + + + + {i18n.BULK_ACTIONS} + + } + > + + + )} - - {i18n.REFRESH} - - - - + + + {i18n.REFRESH} + + + + + {modals} ); diff --git a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap deleted file mode 100644 index 83c8a16ea0290..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UtilityBar it renders 1`] = ` - - - - - Test text - - - - - Test action - - - - - - - Test action - - - - -`; diff --git a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap deleted file mode 100644 index 8ef7ee1cfe842..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UtilityBarGroup it renders 1`] = ` - - - Test text - - -`; diff --git a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap deleted file mode 100644 index 2fe3b8ac5c7aa..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UtilityBarSection it renders 1`] = ` - - - - Test text - - - -`; diff --git a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap deleted file mode 100644 index cf635ffa49c4c..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UtilityBarText it renders 1`] = ` - - Test text - -`; diff --git a/x-pack/plugins/cases/public/components/utility_bar/index.ts b/x-pack/plugins/cases/public/components/utility_bar/index.ts deleted file mode 100644 index 830f3cb043ba9..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { UtilityBar } from './utility_bar'; -export { UtilityBarAction } from './utility_bar_action'; -export { UtilityBarGroup } from './utility_bar_group'; -export { UtilityBarSection } from './utility_bar_section'; -export { UtilityBarSpacer } from './utility_bar_spacer'; -export { UtilityBarText } from './utility_bar_text'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/styles.tsx b/x-pack/plugins/cases/public/components/utility_bar/styles.tsx deleted file mode 100644 index 4c4427ba23471..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/styles.tsx +++ /dev/null @@ -1,144 +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 styled, { css } from 'styled-components'; - -/** - * UTILITY BAR - */ - -export interface BarProps { - border?: boolean; -} - -export interface BarSectionProps { - grow?: boolean; -} - -export interface BarGroupProps { - grow?: boolean; -} - -export const Bar = styled.aside.attrs({ - className: 'casesUtilityBar', -})` - ${({ border, theme }) => css` - ${border && - css` - border-bottom: ${theme.eui.euiBorderThin}; - padding-bottom: ${theme.eui.euiSizeS}; - `} - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.l}) { - display: flex; - justify-content: space-between; - } - `} -`; -Bar.displayName = 'Bar'; - -export const BarSection = styled.div.attrs({ - className: 'casesUtilityBar__section', -})` - ${({ grow, theme }) => css` - & + & { - margin-top: ${theme.eui.euiSizeS}; - } - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) { - display: flex; - flex-wrap: wrap; - } - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.l}) { - & + & { - margin-top: 0; - margin-left: ${theme.eui.euiSize}; - } - } - ${grow && - css` - flex: 1; - `} - `} -`; -BarSection.displayName = 'BarSection'; - -export const BarGroup = styled.div.attrs({ - className: 'casesUtilityBar__group', -})` - ${({ grow, theme }) => css` - align-items: flex-start; - display: flex; - flex-wrap: wrap; - - & + & { - margin-top: ${theme.eui.euiSizeS}; - } - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) { - border-right: ${theme.eui.euiBorderThin}; - flex-wrap: nowrap; - margin-right: ${theme.eui.euiSizeM}; - padding-right: ${theme.eui.euiSizeM}; - - & + & { - margin-top: 0; - } - - &:last-child { - border-right: none; - margin-right: 0; - padding-right: 0; - } - } - - & > * { - margin-right: ${theme.eui.euiSize}; - - &:last-child { - margin-right: 0; - } - } - ${grow && - css` - flex: 1; - `} - `} -`; -BarGroup.displayName = 'BarGroup'; - -export const BarText = styled.p.attrs({ - className: 'casesUtilityBar__text', -})` - ${({ theme }) => css` - color: ${theme.eui.euiTextSubduedColor}; - font-size: ${theme.eui.euiFontSizeXS}; - line-height: ${theme.eui.euiLineHeight}; - white-space: nowrap; - `} -`; -BarText.displayName = 'BarText'; - -export const BarAction = styled.div.attrs({ - className: 'casesUtilityBar__action', -})` - ${({ theme }) => css` - font-size: ${theme.eui.euiFontSizeXS}; - line-height: ${theme.eui.euiLineHeight}; - `} -`; -BarAction.displayName = 'BarAction'; - -export const BarSpacer = styled.div.attrs({ - className: 'casesUtilityBar__spacer', -})` - ${() => css` - flex: 1; - `} -`; -BarSpacer.displayName = 'BarSpacer'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx deleted file mode 100644 index 52486e32905db..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx +++ /dev/null @@ -1,103 +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 { euiDarkVars } from '@kbn/ui-theme'; -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { TestProviders } from '../../common/mock'; - -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '.'; - -describe('UtilityBar', () => { - test('it renders', () => { - const wrapper = shallow( - - - - - {'Test text'} - - - - {'Test action'} - - - - - - {'Test action'} - - - - - ); - - expect(wrapper.find('UtilityBar')).toMatchSnapshot(); - }); - - test('it applies border styles when border is true', () => { - const wrapper = mount( - - - - - {'Test text'} - - - - {'Test action'} - - - - - - {'Test action'} - - - - - ); - const casesUtilityBar = wrapper.find('.casesUtilityBar').first(); - - expect(casesUtilityBar).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(casesUtilityBar).toHaveStyleRule('padding-bottom', euiDarkVars.euiSizeS); - }); - - test('it DOES NOT apply border styles when border is false', () => { - const wrapper = mount( - - - - - {'Test text'} - - - - {'Test action'} - - - - - - {'Test action'} - - - - - ); - const casesUtilityBar = wrapper.find('.casesUtilityBar').first(); - - expect(casesUtilityBar).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(casesUtilityBar).not.toHaveStyleRule('padding-bottom', euiDarkVars.euiSizeS); - }); -}); diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.tsx deleted file mode 100644 index ff47459d437be..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { Bar, BarProps } from './styles'; - -interface UtilityBarProps extends BarProps { - children: React.ReactNode; -} - -export const UtilityBar = React.memo(({ border, children }) => ( - {children} -)); - -UtilityBar.displayName = 'UtilityBar'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx deleted file mode 100644 index 881f4e922bcab..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; -import { UtilityBarAction } from '.'; - -describe('UtilityBarAction', () => { - let appMockRenderer: AppMockRenderer; - const dataTestSubj = 'test-bar-action'; - - beforeEach(() => { - jest.clearAllMocks(); - appMockRenderer = createAppMockRenderer(); - }); - - test('it renders', () => { - const res = appMockRenderer.render( - - {'Test action'} - - ); - - expect(res.getByTestId(dataTestSubj)).toBeInTheDocument(); - expect(res.getByText('Test action')).toBeInTheDocument(); - }); - - test('it renders a popover', () => { - const res = appMockRenderer.render( - - {'Test action'} - - ); - - expect(res.getByTestId(`${dataTestSubj}-link-icon`)).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx deleted file mode 100644 index b0748f1dd7c9f..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { LinkIcon, LinkIconProps } from '../link_icon'; -import { BarAction } from './styles'; - -export interface UtilityBarActionProps extends LinkIconProps { - dataTestSubj?: string; -} - -export const UtilityBarAction = React.memo( - ({ dataTestSubj, children, color, disabled, href, iconSide, iconSize, iconType, onClick }) => { - return ( - - - {children} - - - ); - } -); - -UtilityBarAction.displayName = 'UtilityBarAction'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx deleted file mode 100644 index fa3372cf52331..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.test.tsx +++ /dev/null @@ -1,89 +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 userEvent from '@testing-library/user-event'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; - -import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; -import { UtilityBarBulkActions } from './utility_bar_bulk_actions'; - -describe('UtilityBarBulkActions', () => { - let appMockRenderer: AppMockRenderer; - const closePopover = jest.fn(); - const onButtonClick = jest.fn(); - const dataTestSubj = 'test-bar-action'; - - beforeEach(() => { - jest.clearAllMocks(); - appMockRenderer = createAppMockRenderer(); - }); - - it('renders', () => { - const res = appMockRenderer.render( - - - {'Test bulk actions'} - - - ); - - expect(res.getByTestId(dataTestSubj)).toBeInTheDocument(); - expect(res.getByText('button title')).toBeInTheDocument(); - }); - - it('renders a popover', async () => { - const res = appMockRenderer.render( - - - {'Test bulk actions'} - - - ); - - expect(res.getByText('Test bulk actions')).toBeInTheDocument(); - }); - - it('calls onButtonClick', async () => { - const res = appMockRenderer.render( - - - {'Test bulk actions'} - - - ); - - expect(res.getByText('Test bulk actions')).toBeInTheDocument(); - - act(() => { - userEvent.click(res.getByText('button title')); - }); - - expect(onButtonClick).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx deleted file mode 100644 index afeb93cc221ea..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_bulk_actions.tsx +++ /dev/null @@ -1,67 +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 { EuiPopover } from '@elastic/eui'; -import React from 'react'; -import { LinkIcon, LinkIconProps } from '../link_icon'; - -import { BarAction } from './styles'; - -export interface UtilityBarActionProps extends Omit { - isPopoverOpen: boolean; - buttonTitle: string; - closePopover: () => void; - onButtonClick: () => void; - dataTestSubj?: string; -} - -export const UtilityBarBulkActions = React.memo( - ({ - dataTestSubj, - children, - color, - disabled, - href, - iconSide, - iconSize, - iconType, - isPopoverOpen, - onButtonClick, - buttonTitle, - closePopover, - }) => { - return ( - - - {buttonTitle} - - } - > - {children} - - - ); - } -); - -UtilityBarBulkActions.displayName = 'UtilityBarBulkActions'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.test.tsx deleted file mode 100644 index bf7bb34ab5b64..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.test.tsx +++ /dev/null @@ -1,26 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../common/mock'; -import { UtilityBarGroup, UtilityBarText } from '.'; - -describe('UtilityBarGroup', () => { - test('it renders', () => { - const wrapper = shallow( - - - {'Test text'} - - - ); - - expect(wrapper.find('UtilityBarGroup')).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.tsx deleted file mode 100644 index ef83d6effc8a3..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_group.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { BarGroup, BarGroupProps } from './styles'; - -export interface UtilityBarGroupProps extends BarGroupProps { - children: React.ReactNode; -} - -export const UtilityBarGroup = React.memo(({ grow, children }) => ( - {children} -)); - -UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.test.tsx deleted file mode 100644 index 142cca8fc9e32..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.test.tsx +++ /dev/null @@ -1,28 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../common/mock'; -import { UtilityBarGroup, UtilityBarSection, UtilityBarText } from '.'; - -describe('UtilityBarSection', () => { - test('it renders', () => { - const wrapper = shallow( - - - - {'Test text'} - - - - ); - - expect(wrapper.find('UtilityBarSection')).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.tsx deleted file mode 100644 index c84219cc63488..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_section.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { BarSection, BarSectionProps } from './styles'; - -export interface UtilityBarSectionProps extends BarSectionProps { - children: React.ReactNode; -} - -export const UtilityBarSection = React.memo(({ grow, children }) => ( - {children} -)); - -UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_spacer.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_spacer.tsx deleted file mode 100644 index 11b3be8d656e4..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_spacer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { BarSpacer } from './styles'; - -export interface UtilityBarSpacerProps { - dataTestSubj?: string; -} - -export const UtilityBarSpacer = React.memo(({ dataTestSubj }) => ( - -)); - -UtilityBarSpacer.displayName = 'UtilityBarSpacer'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.test.tsx deleted file mode 100644 index afeae82a19e85..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.test.tsx +++ /dev/null @@ -1,24 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../common/mock'; -import { UtilityBarText } from '.'; - -describe('UtilityBarText', () => { - test('it renders', () => { - const wrapper = shallow( - - {'Test text'} - - ); - - expect(wrapper.find('UtilityBarText')).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.tsx deleted file mode 100644 index c0be3cbfbe202..0000000000000 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_text.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { BarText } from './styles'; - -export interface UtilityBarTextProps { - children: string | JSX.Element; - dataTestSubj?: string; -} - -export const UtilityBarText = React.memo(({ children, dataTestSubj }) => ( - {children} -)); - -UtilityBarText.displayName = 'UtilityBarText'; diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 15e2e40b0ca71..89b09ddde5879 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -159,7 +159,7 @@ export function CasesTableServiceProvider( }, async refreshTable() { - await testSubjects.click('all-cases-refresh'); + await testSubjects.click('all-cases-refresh-link-icon'); }, async openRowActions(index: number) { @@ -177,7 +177,8 @@ export function CasesTableServiceProvider( async selectAllCasesAndOpenBulkActions() { await testSubjects.setCheckbox('checkboxSelectAll', 'check'); - const button = await find.byCssSelector('[aria-label="Bulk actions"]'); + await testSubjects.existOrFail('case-table-bulk-actions-link-icon'); + const button = await testSubjects.find('case-table-bulk-actions-link-icon'); await button.click(); }, From c723fd825d759d9b6207573557c2de5b8d23c1c3 Mon Sep 17 00:00:00 2001 From: Adam Demjen Date: Thu, 6 Oct 2022 10:56:42 -0400 Subject: [PATCH 7/7] [8.6][ML Inference] New API to fetch ML inference errors (#142799) * Add ML inference PL creation flow * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Add exists check, clean up code a bit * Fix dest name * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Separate concerns * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Remove i18n due to linter error, fix src field ref * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Add/update unit tests * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Refactor error handling * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Add sub-pipeline to parent ML PL * Add unit tests and docs * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Refactor error handling * Wrap logic into higher level function * Add route test * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * API to fetch inference errors * Minor style changes * Add unit tests * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../get_inference_errors.test.ts | 87 +++++++++++++++++++ .../get_inference_errors.ts | 81 +++++++++++++++++ .../routes/enterprise_search/indices.test.ts | 63 ++++++++++++++ .../routes/enterprise_search/indices.ts | 26 ++++++ 4 files changed, 257 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.ts diff --git a/x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.test.ts b/x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.test.ts new file mode 100644 index 0000000000000..d9939afedaa5e --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core/server'; + +import { getMlInferenceErrors } from './get_inference_errors'; + +describe('getMlInferenceErrors', () => { + const indexName = 'my-index'; + + const mockClient = { + search: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch aggregations and transform them', async () => { + mockClient.search.mockImplementation(() => + Promise.resolve({ + aggregations: { + errors: { + buckets: [ + { + key: 'Error message 1', + doc_count: 100, + max_error_timestamp: { + value: 1664977836100, + value_as_string: '2022-10-05T13:50:36.100Z', + }, + }, + { + key: 'Error message 2', + doc_count: 200, + max_error_timestamp: { + value: 1664977836200, + value_as_string: '2022-10-05T13:50:36.200Z', + }, + }, + ], + }, + }, + }) + ); + + const actualResult = await getMlInferenceErrors( + indexName, + mockClient as unknown as ElasticsearchClient + ); + + expect(actualResult).toEqual([ + { + message: 'Error message 1', + doc_count: 100, + timestamp: '2022-10-05T13:50:36.100Z', + }, + { + message: 'Error message 2', + doc_count: 200, + timestamp: '2022-10-05T13:50:36.200Z', + }, + ]); + expect(mockClient.search).toHaveBeenCalledTimes(1); + }); + + it('should return an empty array if there are no aggregates', async () => { + mockClient.search.mockImplementation(() => + Promise.resolve({ + aggregations: { + errors: [], + }, + }) + ); + + const actualResult = await getMlInferenceErrors( + indexName, + mockClient as unknown as ElasticsearchClient + ); + + expect(actualResult).toEqual([]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.ts b/x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.ts new file mode 100644 index 0000000000000..1ced837f42f22 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/ml_inference_pipeline/get_inference_errors.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AggregationsMultiBucketAggregateBase, + AggregationsStringRareTermsBucketKeys, +} from '@elastic/elasticsearch/lib/api/types'; + +import { ElasticsearchClient } from '@kbn/core/server'; + +export interface MlInferenceError { + message: string; + doc_count: number; + timestamp: string | undefined; // Date string +} + +export interface ErrorAggregationBucket extends AggregationsStringRareTermsBucketKeys { + max_error_timestamp: { + value: number | null; + value_as_string?: string; + }; +} + +/** + * Fetches an aggregate of distinct ML inference errors from the target index, along with the most + * recent error's timestamp and affected document count for each bucket. + * @param indexName the index to get the errors from. + * @param esClient the Elasticsearch Client to use to fetch the errors. + */ +export const getMlInferenceErrors = async ( + indexName: string, + esClient: ElasticsearchClient +): Promise => { + const searchResult = await esClient.search< + unknown, + { + errors: AggregationsMultiBucketAggregateBase; + } + >({ + index: indexName, + body: { + aggs: { + errors: { + terms: { + field: '_ingest.inference_errors.message.enum', + order: { + max_error_timestamp: 'desc', + }, + size: 20, + }, + aggs: { + max_error_timestamp: { + max: { + field: '_ingest.inference_errors.timestamp', + }, + }, + }, + }, + }, + size: 0, + }, + }); + + const errorBuckets = searchResult.aggregations?.errors.buckets; + if (!errorBuckets) { + return []; + } + + // Buckets are either in an array or in a Record, we transform them to an array + const buckets = Array.isArray(errorBuckets) ? errorBuckets : Object.values(errorBuckets); + + return buckets.map((bucket) => ({ + message: bucket.key, + doc_count: bucket.doc_count, + timestamp: bucket.max_error_timestamp?.value_as_string, + })); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts index 435fb0892019f..64711a6ac20be 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts @@ -23,10 +23,14 @@ jest.mock('../../lib/indices/delete_ml_inference_pipeline', () => ({ jest.mock('../../lib/indices/exists_index', () => ({ indexOrAliasExists: jest.fn(), })); +jest.mock('../../lib/ml_inference_pipeline/get_inference_errors', () => ({ + getMlInferenceErrors: jest.fn(), +})); import { deleteMlInferencePipeline } from '../../lib/indices/delete_ml_inference_pipeline'; import { indexOrAliasExists } from '../../lib/indices/exists_index'; import { fetchMlInferencePipelineProcessors } from '../../lib/indices/fetch_ml_inference_pipeline_processors'; +import { getMlInferenceErrors } from '../../lib/ml_inference_pipeline/get_inference_errors'; import { createAndReferenceMlInferencePipeline } from '../../utils/create_ml_inference_pipeline'; import { ElasticsearchResponseError } from '../../utils/identify_exceptions'; @@ -40,6 +44,7 @@ describe('Enterprise Search Managed Indices', () => { putPipeline: jest.fn(), simulate: jest.fn(), }, + search: jest.fn(), }, }; @@ -47,6 +52,64 @@ describe('Enterprise Search Managed Indices', () => { elasticsearch: { client: mockClient }, }; + describe('GET /internal/enterprise_search/indices/{indexName}/ml_inference/errors', () => { + beforeEach(() => { + const context = { + core: Promise.resolve(mockCore), + } as unknown as jest.Mocked; + + mockRouter = new MockRouter({ + context, + method: 'get', + path: '/internal/enterprise_search/indices/{indexName}/ml_inference/errors', + }); + + registerIndexRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('fails validation without index_name', () => { + const request = { + params: {}, + }; + mockRouter.shouldThrow(request); + }); + + it('fetches ML inference errors', async () => { + const errorsResult = [ + { + message: 'Error message 1', + doc_count: 100, + timestamp: '2022-10-05T13:50:36.100Z', + }, + { + message: 'Error message 2', + doc_count: 200, + timestamp: '2022-10-05T13:50:36.200Z', + }, + ]; + + (getMlInferenceErrors as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(errorsResult); + }); + + await mockRouter.callRoute({ + params: { indexName: 'my-index-name' }, + }); + + expect(getMlInferenceErrors).toHaveBeenCalledWith('my-index-name', mockClient.asCurrentUser); + + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: { + errors: errorsResult, + }, + headers: { 'content-type': 'application/json' }, + }); + }); + }); + describe('GET /internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors', () => { beforeEach(() => { const context = { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index ef6f8131ee2c1..38aef14fdaa81 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -9,6 +9,7 @@ import { IngestPutPipelineRequest, IngestSimulateRequest, } from '@elastic/elasticsearch/lib/api/types'; + import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; @@ -27,6 +28,7 @@ import { fetchIndex } from '../../lib/indices/fetch_index'; import { fetchIndices } from '../../lib/indices/fetch_indices'; import { fetchMlInferencePipelineProcessors } from '../../lib/indices/fetch_ml_inference_pipeline_processors'; import { generateApiKey } from '../../lib/indices/generate_api_key'; +import { getMlInferenceErrors } from '../../lib/ml_inference_pipeline/get_inference_errors'; import { createIndexPipelineDefinitions } from '../../lib/pipelines/create_pipeline_definitions'; import { getCustomPipelines } from '../../lib/pipelines/get_custom_pipelines'; import { getPipeline } from '../../lib/pipelines/get_pipeline'; @@ -524,6 +526,30 @@ export function registerIndexRoutes({ }) ); + router.get( + { + path: '/internal/enterprise_search/indices/{indexName}/ml_inference/errors', + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const indexName = decodeURIComponent(request.params.indexName); + const { client } = (await context.core).elasticsearch; + + const errors = await getMlInferenceErrors(indexName, client.asCurrentUser); + + return response.ok({ + body: { + errors, + }, + headers: { 'content-type': 'application/json' }, + }); + }) + ); + router.put( { path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/{pipelineName}',