From 06f142d586653db33e1b1b52aa71d95a91b341c3 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 23 Jul 2020 08:06:16 -0400 Subject: [PATCH 01/20] [Ingest Manager] Fix config rollout move to limit concurrent config change instead of config per second (#72931) --- .../ingest_manager/common/types/index.ts | 3 +- x-pack/plugins/ingest_manager/server/index.ts | 3 +- .../agents/checkin/rxjs_utils.test.ts | 45 ++++++++++++++++++ .../services/agents/checkin/rxjs_utils.ts | 47 +++++++------------ .../server/services/agents/checkin/state.ts | 17 +++++-- .../agents/checkin/state_new_actions.ts | 10 ++-- 6 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index d7edc04a3579..7acef263f973 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -22,8 +22,7 @@ export interface IngestManagerConfigType { host?: string; ca_sha256?: string; }; - agentConfigRollupRateLimitIntervalMs: number; - agentConfigRollupRateLimitRequestPerInterval: number; + agentConfigRolloutConcurrency: number; }; } diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 6c72218abc53..40e0153a2658 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -35,8 +35,7 @@ export const config = { host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), }), - agentConfigRollupRateLimitIntervalMs: schema.number({ defaultValue: 5000 }), - agentConfigRollupRateLimitRequestPerInterval: schema.number({ defaultValue: 50 }), + agentConfigRolloutConcurrency: schema.number({ defaultValue: 10 }), }), }), }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts new file mode 100644 index 000000000000..70207dcf325c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Rx from 'rxjs'; +import { share } from 'rxjs/operators'; +import { createSubscriberConcurrencyLimiter } from './rxjs_utils'; + +function createSpyObserver(o: Rx.Observable): [Rx.Subscription, jest.Mock] { + const spy = jest.fn(); + const observer = o.subscribe(spy); + return [observer, spy]; +} + +describe('createSubscriberConcurrencyLimiter', () => { + it('should not publish to more than n concurrent subscriber', async () => { + const subject = new Rx.Subject(); + const sharedObservable = subject.pipe(share()); + + const limiter = createSubscriberConcurrencyLimiter(2); + + const [observer1, spy1] = createSpyObserver(sharedObservable.pipe(limiter())); + const [observer2, spy2] = createSpyObserver(sharedObservable.pipe(limiter())); + const [observer3, spy3] = createSpyObserver(sharedObservable.pipe(limiter())); + const [observer4, spy4] = createSpyObserver(sharedObservable.pipe(limiter())); + subject.next('test1'); + + expect(spy1).toBeCalled(); + expect(spy2).toBeCalled(); + expect(spy3).not.toBeCalled(); + expect(spy4).not.toBeCalled(); + + observer1.unsubscribe(); + expect(spy3).toBeCalled(); + expect(spy4).not.toBeCalled(); + + observer2.unsubscribe(); + expect(spy4).toBeCalled(); + + observer3.unsubscribe(); + observer4.unsubscribe(); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts index a806169019a1..dc0ed35207e4 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts @@ -43,34 +43,23 @@ export const toPromiseAbortable = ( } }); -export function createLimiter(ratelimitIntervalMs: number, ratelimitRequestPerInterval: number) { - function createCurrentInterval() { - return { - startedAt: Rx.asyncScheduler.now(), - numRequests: 0, - }; - } - - let currentInterval: { startedAt: number; numRequests: number } = createCurrentInterval(); +export function createSubscriberConcurrencyLimiter(maxConcurrency: number) { let observers: Array<[Rx.Subscriber, any]> = []; - let timerSubscription: Rx.Subscription | undefined; + let activeObservers: Array> = []; - function createTimeout() { - if (timerSubscription) { + function processNext() { + if (activeObservers.length >= maxConcurrency) { return; } - timerSubscription = Rx.asyncScheduler.schedule(() => { - timerSubscription = undefined; - currentInterval = createCurrentInterval(); - for (const [waitingObserver, value] of observers) { - if (currentInterval.numRequests >= ratelimitRequestPerInterval) { - createTimeout(); - continue; - } - currentInterval.numRequests++; - waitingObserver.next(value); - } - }, ratelimitIntervalMs); + const observerValuePair = observers.shift(); + + if (!observerValuePair) { + return; + } + + const [observer, value] = observerValuePair; + activeObservers.push(observer); + observer.next(value); } return function limit(): Rx.MonoTypeOperatorFunction { @@ -78,14 +67,8 @@ export function createLimiter(ratelimitIntervalMs: number, ratelimitRequestPerIn new Rx.Observable((observer) => { const subscription = observable.subscribe({ next(value) { - if (currentInterval.numRequests < ratelimitRequestPerInterval) { - currentInterval.numRequests++; - observer.next(value); - return; - } - observers = [...observers, [observer, value]]; - createTimeout(); + processNext(); }, error(err) { observer.error(err); @@ -96,8 +79,10 @@ export function createLimiter(ratelimitIntervalMs: number, ratelimitRequestPerIn }); return () => { + activeObservers = activeObservers.filter((o) => o !== observer); observers = observers.filter((o) => o[0] !== observer); subscription.unsubscribe(); + processNext(); }; }); }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state.ts index 69d61171b21f..63f22b82611c 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state.ts @@ -13,9 +13,11 @@ import { AGENT_UPDATE_LAST_CHECKIN_INTERVAL_MS } from '../../../constants'; function agentCheckinStateFactory() { const agentConnected = agentCheckinStateConnectedAgentsFactory(); - const newActions = agentCheckinStateNewActionsFactory(); + let newActions: ReturnType; let interval: NodeJS.Timeout; + function start() { + newActions = agentCheckinStateNewActionsFactory(); interval = setInterval(async () => { try { await agentConnected.updateLastCheckinAt(); @@ -31,15 +33,20 @@ function agentCheckinStateFactory() { } } return { - subscribeToNewActions: ( + subscribeToNewActions: async ( soClient: SavedObjectsClientContract, agent: Agent, options?: { signal: AbortSignal } - ) => - agentConnected.wrapPromise( + ) => { + if (!newActions) { + throw new Error('Agent checkin state not initialized'); + } + + return agentConnected.wrapPromise( agent.id, newActions.subscribeToNewActions(soClient, agent, options) - ), + ); + }, start, stop, }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 5ceb774a1946..53270afe453c 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -28,7 +28,7 @@ import * as APIKeysService from '../../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_UPDATE_ACTIONS_INTERVAL_MS } from '../../../constants'; import { createAgentAction, getNewActionsSince } from '../actions'; import { appContextService } from '../../app_context'; -import { toPromiseAbortable, AbortError, createLimiter } from './rxjs_utils'; +import { toPromiseAbortable, AbortError, createSubscriberConcurrencyLimiter } from './rxjs_utils'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -134,9 +134,8 @@ export function agentCheckinStateNewActionsFactory() { const agentConfigs$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators - const rateLimiter = createLimiter( - appContextService.getConfig()?.fleet.agentConfigRollupRateLimitIntervalMs || 5000, - appContextService.getConfig()?.fleet.agentConfigRollupRateLimitRequestPerInterval || 50 + const concurrencyLimiter = createSubscriberConcurrencyLimiter( + appContextService.getConfig()?.fleet.agentConfigRolloutConcurrency ?? 10 ); async function subscribeToNewActions( @@ -155,10 +154,11 @@ export function agentCheckinStateNewActionsFactory() { if (!agentConfig$) { throw new Error(`Invalid state no observable for config ${configId}`); } + const stream$ = agentConfig$.pipe( timeout(appContextService.getConfig()?.fleet.pollingRequestTimeout || 0), filter((config) => shouldCreateAgentConfigAction(agent, config)), - rateLimiter(), + concurrencyLimiter(), mergeMap((config) => createAgentActionFromConfig(soClient, agent, config)), merge(newActions$), mergeMap(async (data) => { From 49782f93480b8d016ebf36bda18a6d855be5aff2 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 23 Jul 2020 14:48:13 +0200 Subject: [PATCH 02/20] delete legacy apm_oss plugin (#73016) --- src/legacy/core_plugins/apm_oss/index.d.ts | 22 ------- src/legacy/core_plugins/apm_oss/index.js | 60 -------------------- src/legacy/core_plugins/apm_oss/package.json | 4 -- src/legacy/server/kbn_server.d.ts | 2 - 4 files changed, 88 deletions(-) delete mode 100644 src/legacy/core_plugins/apm_oss/index.d.ts delete mode 100644 src/legacy/core_plugins/apm_oss/index.js delete mode 100644 src/legacy/core_plugins/apm_oss/package.json diff --git a/src/legacy/core_plugins/apm_oss/index.d.ts b/src/legacy/core_plugins/apm_oss/index.d.ts deleted file mode 100644 index 86fe4e0350dc..000000000000 --- a/src/legacy/core_plugins/apm_oss/index.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface ApmOssPlugin { - indexPatterns: string[]; -} diff --git a/src/legacy/core_plugins/apm_oss/index.js b/src/legacy/core_plugins/apm_oss/index.js deleted file mode 100644 index b7ab6797c0de..000000000000 --- a/src/legacy/core_plugins/apm_oss/index.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -export default function apmOss(kibana) { - return new kibana.Plugin({ - id: 'apm_oss', - - config(Joi) { - return Joi.object({ - // enable plugin - enabled: Joi.boolean().default(true), - - // Kibana Index pattern - indexPattern: Joi.string().default('apm-*'), - - // ES Indices - sourcemapIndices: Joi.string().default('apm-*'), - errorIndices: Joi.string().default('apm-*'), - transactionIndices: Joi.string().default('apm-*'), - spanIndices: Joi.string().default('apm-*'), - metricsIndices: Joi.string().default('apm-*'), - onboardingIndices: Joi.string().default('apm-*'), - }).default(); - }, - - init(server) { - server.expose( - 'indexPatterns', - _.uniq( - [ - 'sourcemapIndices', - 'errorIndices', - 'transactionIndices', - 'spanIndices', - 'metricsIndices', - 'onboardingIndices', - ].map((type) => server.config().get(`apm_oss.${type}`)) - ) - ); - }, - }); -} diff --git a/src/legacy/core_plugins/apm_oss/package.json b/src/legacy/core_plugins/apm_oss/package.json deleted file mode 100644 index 4ca161f293e7..000000000000 --- a/src/legacy/core_plugins/apm_oss/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "apm_oss", - "version": "kibana" -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 40996500bfbe..9bb091383ab1 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -43,7 +43,6 @@ import { import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; -import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; @@ -62,7 +61,6 @@ declare module 'hapi' { elasticsearch: ElasticsearchPlugin; kibana: any; spaces: any; - apm_oss: ApmOssPlugin; // add new plugin types here } From 304445f007899681572a888cb45d35ef7e102d7c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 23 Jul 2020 06:27:06 -0700 Subject: [PATCH 03/20] =?UTF-8?q?fix:=20=F0=9F=90=9B=20don't=20show=20acti?= =?UTF-8?q?ons=20if=20Discover=20app=20is=20disabled=20(#73017)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 don't show actions if Discover app is disabled * style: collapse ifs --- .../explore_data/abstract_explore_data_action.ts | 6 +++++- .../explore_data/explore_data_chart_action.test.ts | 13 +++++++++++++ .../explore_data_context_menu_action.test.ts | 13 +++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 3aec0ce238c3..434d38c76d42 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -49,12 +49,16 @@ export abstract class AbstractExploreDataAction { if (!embeddable) return false; + const { core, plugins } = this.params.start(); + const { capabilities } = core.application; + + if (capabilities.discover && !capabilities.discover.show) return false; + if (!plugins.discover.urlGenerator) return false; const isDashboardOnlyMode = !!this.params .start() .plugins.kibanaLegacy?.dashboardConfig.getHideWriteControls(); if (isDashboardOnlyMode) return false; - if (!this.params.start().plugins.discover.urlGenerator) return false; if (!shared.hasExactlyOneIndexPattern(embeddable)) return false; if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; return true; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 6c3ed7a2fe77..14cd48ae1f50 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -196,6 +196,19 @@ describe('"Explore underlying data" panel action', () => { expect(isCompatible).toBe(false); }); + + test('returns false if Discover app is disabled', async () => { + const { action, context, core } = setup(); + + core.application.capabilities = { ...core.application.capabilities }; + (core.application.capabilities as any).discover = { + show: false, + }; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); }); describe('getHref()', () => { diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index 1422cc871cde..68253655af89 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -179,6 +179,19 @@ describe('"Explore underlying data" panel action', () => { expect(isCompatible).toBe(false); }); + + test('returns false if Discover app is disabled', async () => { + const { action, context, core } = setup(); + + core.application.capabilities = { ...core.application.capabilities }; + (core.application.capabilities as any).discover = { + show: false, + }; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); }); describe('getHref()', () => { From 7280b69e9942866489da7f2f03376f7508bc74c3 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Thu, 23 Jul 2020 09:54:08 -0400 Subject: [PATCH 04/20] [Security Solution][Exceptions] Preserve rule exceptions when updating rule (#72977) * Send exceptions_list with rule edit * Handle exceptions list checkbox * whoops * Don't lose data when associating with endpoint list * syntax * Filter out the endpoint lists when disassociating * Add tests * Refactor per PR suggestions Co-authored-by: Elastic Machine --- .../rules/all/__mocks__/mock.ts | 7 +++ .../rules/create/helpers.test.ts | 57 +++++++++++++++++++ .../detection_engine/rules/create/helpers.ts | 22 +++++-- .../detection_engine/rules/edit/index.tsx | 3 +- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 10d969ae7e6e..14cf476e6656 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -6,6 +6,7 @@ import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; +import { List } from '../../../../../../../common/detection_engine/schemas/types'; import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; import { FieldValueQueryBar } from '../../../../../components/rules/query_bar'; @@ -240,3 +241,9 @@ export const mockRules: Rule[] = [ mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), ]; + +export const mockExceptionsList: List = { + namespace_type: 'single', + id: '75cd4380-cc5e-11ea-9101-5b34f44aeb44', + type: 'detection', +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 745518b90df0..6458d2faa246 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { List } from '../../../../../../common/detection_engine/schemas/types'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { NewRule } from '../../../../containers/detection_engine/rules'; + import { DefineStepRuleJson, ScheduleStepRuleJson, @@ -26,12 +29,19 @@ import { } from './helpers'; import { mockDefineStepRule, + mockExceptionsList, mockQueryBar, mockScheduleStepRule, mockAboutStepRule, mockActionsStepRule, } from '../all/__mocks__/mock'; +const ENDPOINT_LIST = { + id: ENDPOINT_LIST_ID, + namespace_type: 'agnostic', + type: 'endpoint', +} as List; + describe('helpers', () => { describe('getTimeTypeValue', () => { test('returns timeObj with value 0 if no time value found', () => { @@ -373,6 +383,53 @@ describe('helpers', () => { expect(result).toEqual(expected); }); + test('returns formatted object with endpoint exceptions_list', () => { + const result: AboutStepRuleJson = formatAboutStepData( + { + ...mockData, + isAssociatedToEndpointList: true, + }, + [] + ); + expect(result.exceptions_list).toEqual([ + { id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' }, + ]); + }); + + test('returns formatted object with detections exceptions_list', () => { + const result: AboutStepRuleJson = formatAboutStepData(mockData, [mockExceptionsList]); + expect(result.exceptions_list).toEqual([mockExceptionsList]); + }); + + test('returns formatted object with both exceptions_lists', () => { + const result: AboutStepRuleJson = formatAboutStepData( + { + ...mockData, + isAssociatedToEndpointList: true, + }, + [mockExceptionsList] + ); + expect(result.exceptions_list).toEqual([ENDPOINT_LIST, mockExceptionsList]); + }); + + test('returns formatted object with pre-existing exceptions lists', () => { + const exceptionsLists: List[] = [ENDPOINT_LIST, mockExceptionsList]; + const result: AboutStepRuleJson = formatAboutStepData( + { + ...mockData, + isAssociatedToEndpointList: true, + }, + exceptionsLists + ); + expect(result.exceptions_list).toEqual(exceptionsLists); + }); + + test('returns formatted object with pre-existing endpoint exceptions list disabled', () => { + const exceptionsLists: List[] = [ENDPOINT_LIST, mockExceptionsList]; + const result: AboutStepRuleJson = formatAboutStepData(mockData, exceptionsLists); + expect(result.exceptions_list).toEqual([mockExceptionsList]); + }); + test('returns formatted object with empty falsePositive and references filtered out', () => { const mockStepData = { ...mockData, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 38f7836f678f..a972afbd8c0c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -12,8 +12,9 @@ import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/const import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { List } from '../../../../../../common/detection_engine/schemas/types'; import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; -import { NewRule } from '../../../../containers/detection_engine/rules'; +import { NewRule, Rule } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -146,7 +147,10 @@ export const formatScheduleStepData = (scheduleData: ScheduleStepRule): Schedule }; }; -export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { +export const formatAboutStepData = ( + aboutStepData: AboutStepRule, + exceptionsList?: List[] +): AboutStepRuleJson => { const { author, falsePositives, @@ -162,6 +166,10 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule timestampOverride, ...rest } = aboutStepData; + + const detectionExceptionLists = + exceptionsList != null ? exceptionsList.filter((list) => list.type !== 'endpoint') : []; + const resp = { author: author.filter((item) => !isEmpty(item)), ...(isBuildingBlock ? { building_block_type: 'default' } : {}), @@ -169,8 +177,13 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule ? { exceptions_list: [ { id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' }, + ...detectionExceptionLists, ] as AboutStepRuleJson['exceptions_list'], } + : exceptionsList != null + ? { + exceptions_list: [...detectionExceptionLists], + } : {}), false_positives: falsePositives.filter((item) => !isEmpty(item)), references: references.filter((item) => !isEmpty(item)), @@ -218,11 +231,12 @@ export const formatRule = ( defineStepData: DefineStepRule, aboutStepData: AboutStepRule, scheduleData: ScheduleStepRule, - actionsData: ActionsStepRule + actionsData: ActionsStepRule, + rule?: Rule | null ): NewRule => deepmerge.all([ formatDefineStepData(defineStepData), - formatAboutStepData(aboutStepData), + formatAboutStepData(aboutStepData, rule?.exceptions_list), formatScheduleStepData(scheduleData), formatActionsStepData(actionsData), ]) as NewRule; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 0900cdb8f478..3cc874b85ecf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -273,7 +273,8 @@ const EditRulePageComponent: FC = () => { : myScheduleRuleForm.data) as ScheduleStepRule, (activeFormId === RuleStep.ruleActions ? activeForm.data - : myActionsRuleForm.data) as ActionsStepRule + : myActionsRuleForm.data) as ActionsStepRule, + rule ), ...(ruleId ? { id: ruleId } : {}), }); From 1ee3cdb03db5a94636b90d5ffefb4ef868a7bb81 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 23 Jul 2020 17:00:16 +0300 Subject: [PATCH 05/20] [Functional Tests] Unskip tsvb timeseries test (#73011) * [Functional Tests] Unskip tsvb timeseries test * Add retry to dropdown selection when element is not found to headless mode --- test/functional/apps/visualize/_tsvb_time_series.ts | 2 +- test/functional/page_objects/visual_builder_page.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index e0d512c1f486..c048755fc5fb 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -107,7 +107,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(actualCount).to.be(expectedLegendValue); }); - it.skip('should show the correct count in the legend with "Human readable" duration formatter', async () => { + it('should show the correct count in the legend with "Human readable" duration formatter', async () => { await visualBuilder.clickSeriesOption(); await visualBuilder.changeDataFormatter('Duration'); await visualBuilder.setDurationFormatterSettings({ to: 'Human readable' }); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 4a4beca95954..0db8cac0f075 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -279,8 +279,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro decimalPlaces?: string; }) { if (from) { - const fromCombobox = await find.byCssSelector('[id$="from-row"] .euiComboBox'); - await comboBox.setElement(fromCombobox, from, { clickWithMouse: true }); + await retry.try(async () => { + const fromCombobox = await find.byCssSelector('[id$="from-row"] .euiComboBox'); + await comboBox.setElement(fromCombobox, from, { clickWithMouse: true }); + }); } if (to) { const toCombobox = await find.byCssSelector('[id$="to-row"] .euiComboBox'); From 2cf37a53266c8407e6993f7085e8f204f1ad4780 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 23 Jul 2020 09:34:56 -0500 Subject: [PATCH 06/20] Don't skip index pattern creation test (#73032) --- .../functional/apps/management/_create_index_pattern_wizard.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 160b052e70d3..976052737140 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -25,8 +25,7 @@ export default function ({ getService, getPageObjects }) { const es = getService('legacyEs'); const PageObjects = getPageObjects(['settings', 'common']); - // Flaky: https://github.com/elastic/kibana/issues/71501 - describe.skip('"Create Index Pattern" wizard', function () { + describe('"Create Index Pattern" wizard', function () { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); From 15ccdc36cae57f34aa070818776fc453fdbdcb68 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 23 Jul 2020 07:50:30 -0700 Subject: [PATCH 07/20] [test] Skips flaky uptime test Signed-off-by: Tyler Smalley --- x-pack/test/functional/apps/uptime/settings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 1286a9940c02..a258cccffbd8 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -16,8 +16,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // Flaky https://github.com/elastic/kibana/issues/60866 - describe('uptime settings page', () => { + // Flaky https://github.com/elastic/kibana/issues/72994 + describe.skip('uptime settings page', () => { beforeEach('navigate to clean app root', async () => { // make 10 checks await makeChecks(es, 'myMonitor', 1, 1, 1); From 2d9eaf013bae31adc72578065982833ea6934f0e Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 23 Jul 2020 18:00:52 +0300 Subject: [PATCH 08/20] Fix view saved search through a visualization (#73040) --- .../components/sidebar/sidebar_title.tsx | 9 ++++++-- .../apps/visualize/_linked_saved_searches.ts | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx index 6713c2ce2391..11ceb5885dd3 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx @@ -65,7 +65,7 @@ export function LinkedSearch({ savedSearch, eventEmitter }: LinkedSearchProps) { }, [eventEmitter]); const onClickViewInDiscover = useCallback(() => { application.navigateToApp('discover', { - path: `#/${savedSearch.id}`, + path: `#/view/${savedSearch.id}`, }); }, [application, savedSearch.id]); @@ -128,7 +128,12 @@ export function LinkedSearch({ savedSearch, eventEmitter }: LinkedSearchProps) {

- + { const savedSearchName = 'vis_saved_search'; + let discoverSavedSearchUrlPath: string; before(async () => { await PageObjects.common.navigateToApp('discover'); await filterBar.addFilter('extension.raw', 'is', 'jpg'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.saveSearch(savedSearchName); + discoverSavedSearchUrlPath = (await browser.getCurrentUrl()).split('?')[0]; }); it('should create a visualization from a saved search', async () => { @@ -54,6 +58,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + it('should have a valid link to the saved search from the visualization', async () => { + await testSubjects.click('showUnlinkSavedSearchPopover'); + await testSubjects.click('viewSavedSearch'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.waitFor('wait discover load its breadcrumbs', async () => { + const discoverBreadcrumb = await PageObjects.discover.getCurrentQueryName(); + return discoverBreadcrumb === savedSearchName; + }); + + const discoverURLPath = (await browser.getCurrentUrl()).split('?')[0]; + expect(discoverURLPath).to.equal(discoverSavedSearchUrlPath); + + // go back to visualize + await browser.goBack(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + it('should respect the time filter when linked to a saved search', async () => { await PageObjects.timePicker.setAbsoluteRange( 'Sep 19, 2015 @ 06:31:44.000', From 18df677da7efa30dec36e8a63b175dc5cd71e6be Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 23 Jul 2020 16:11:15 +0100 Subject: [PATCH 09/20] [ML] Fixing file import, module creation and results viewing permission checks (#72825) * [ML] Fixing file import and module creation permission checks * correcting searches on results index * fixing test * removing unnecessary index * updating apidoc * fixing test Co-authored-by: Elastic Machine --- .../plugins/ml/common/types/capabilities.ts | 13 +- .../components/bottom_bar/bottom_bar.tsx | 2 +- .../components/import_view/import_view.js | 59 +++++----- .../components/custom_url_editor/utils.js | 11 +- .../new_job/common/results_loader/searches.ts | 111 +++++++++--------- .../application/services/forecast_service.js | 84 +++++++------ .../services/ml_api_service/results.ts | 18 +++ .../results_service/result_service_rx.ts | 12 +- .../results_service/results_service.js | 41 +++---- .../ml/server/lib/check_annotations/index.ts | 8 +- .../annotation_service/annotation.test.ts | 22 ++-- .../models/annotation_service/annotation.ts | 8 +- .../analytics_audit_messages.ts | 4 +- .../job_audit_messages/job_audit_messages.js | 6 +- .../ml/server/models/job_service/jobs.ts | 4 +- .../new_job/categorization/top_categories.ts | 8 +- .../get_partition_fields_values.ts | 3 +- .../models/results_service/results_service.ts | 12 +- x-pack/plugins/ml/server/routes/apidoc.json | 1 + .../ml/server/routes/data_frame_analytics.ts | 2 +- .../ml/server/routes/results_service.ts | 33 ++++++ .../shared_services/providers/system.ts | 4 +- .../services/ml/data_visualizer_file_based.ts | 2 +- 23 files changed, 255 insertions(+), 213 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index f2177b0a3572..504cd28b8fa1 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -72,6 +72,7 @@ export function getPluginPrivileges() { const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); const allMlCapabilitiesKeys = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; // TODO: include ML in base privileges for the `8.0` release: https://github.com/elastic/kibana/issues/71422 + const savedObjects = ['index-pattern', 'dashboard', 'search', 'visualization']; const privilege = { app: [PLUGIN_ID, 'kibana'], excludeFromBasePrivileges: true, @@ -79,10 +80,6 @@ export function getPluginPrivileges() { insightsAndAlerting: ['jobsListLink'], }, catalogue: [PLUGIN_ID], - savedObject: { - all: [], - read: ['index-pattern', 'dashboard', 'search', 'visualization'], - }, }; return { @@ -90,11 +87,19 @@ export function getPluginPrivileges() { ...privilege, api: allMlCapabilitiesKeys.map((k) => `ml:${k}`), ui: allMlCapabilitiesKeys, + savedObject: { + all: savedObjects, + read: savedObjects, + }, }, user: { ...privilege, api: userMlCapabilitiesKeys.map((k) => `ml:${k}`), ui: userMlCapabilitiesKeys, + savedObject: { + all: [], + read: savedObjects, + }, }, }; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx index e28386093abe..8b6c16a71651 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx @@ -39,7 +39,7 @@ export const BottomBar: FC = ({ mode, onChangeMode, onCancel, di disableImport ? ( ) : null } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index 64d2e26f827f..36b77a5a25e0 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; import { importerFactory } from './importer'; import { ResultsLinks } from '../results_links'; import { FilebeatConfigFlyout } from '../filebeat_config_flyout'; @@ -66,6 +67,7 @@ const DEFAULT_STATE = { indexPatternNameError: '', timeFieldName: undefined, isFilebeatFlyoutVisible: false, + checkingValidIndex: false, }; export class ImportView extends Component { @@ -76,14 +78,12 @@ export class ImportView extends Component { } componentDidMount() { - this.loadIndexNames(); this.loadIndexPatternNames(); } clickReset = () => { const state = getDefaultState(this.state, this.props.results); this.setState(state, () => { - this.loadIndexNames(); this.loadIndexPatternNames(); }); }; @@ -326,21 +326,33 @@ export class ImportView extends Component { }; onIndexChange = (e) => { - const name = e.target.value; - const { indexNames, indexPattern, indexPatternNames } = this.state; - + const index = e.target.value; this.setState({ - index: name, - indexNameError: isIndexNameValid(name, indexNames), - // if index pattern has been altered, check that it still matches the inputted index - ...(indexPattern === '' - ? {} - : { - indexPatternNameError: isIndexPatternNameValid(indexPattern, indexPatternNames, name), - }), + index, + checkingValidIndex: true, }); + this.debounceIndexCheck(index); }; + debounceIndexCheck = debounce(async (index) => { + if (index === '') { + this.setState({ checkingValidIndex: false }); + return; + } + + const { exists } = await ml.checkIndexExists({ index }); + const indexNameError = exists ? ( + + ) : ( + isIndexNameValid(index) + ); + + this.setState({ checkingValidIndex: false, indexNameError }); + }, 500); + onIndexPatternChange = (e) => { const name = e.target.value; const { indexPatternNames, index } = this.state; @@ -396,12 +408,6 @@ export class ImportView extends Component { this.props.showBottomBar(); }; - async loadIndexNames() { - const indices = await ml.getIndices(); - const indexNames = indices.map((i) => i.name); - this.setState({ indexNames }); - } - async loadIndexPatternNames() { await loadIndexPatterns(this.props.indexPatterns); const indexPatternNames = getIndexPatternNames(); @@ -437,6 +443,7 @@ export class ImportView extends Component { indexPatternNameError, timeFieldName, isFilebeatFlyoutVisible, + checkingValidIndex, } = this.state; const createPipeline = pipelineString !== ''; @@ -459,7 +466,8 @@ export class ImportView extends Component { index === '' || indexNameError !== '' || (createIndexPattern === true && indexPatternNameError !== '') || - initialized === true; + initialized === true || + checkingValidIndex === true; return ( @@ -655,16 +663,7 @@ function getDefaultState(state, results) { }; } -function isIndexNameValid(name, indexNames) { - if (indexNames.find((i) => i === name)) { - return ( - - ); - } - +function isIndexNameValid(name) { const reg = new RegExp('[\\\\/*?"<>|\\s,#]+'); if ( name !== name.toLowerCase() || // name should be lowercase diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index 0b33efa3f9ff..87c2219f4d44 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -11,7 +11,6 @@ import url from 'url'; import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../../src/plugins/dashboard/public'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; import { getPartitioningFieldNames } from '../../../../../common/util/job_utils'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; @@ -295,11 +294,11 @@ export function getTestUrl(job, customUrl) { }; return new Promise((resolve, reject) => { - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - rest_total_hits_as_int: true, - body, - }) + ml.results + .anomalySearch({ + rest_total_hits_as_int: true, + body, + }) .then((resp) => { if (resp.hits.total > 0) { const record = resp.hits.hits[0]._source; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts index 724a6146854a..51c396518c85 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts @@ -6,7 +6,6 @@ import { get } from 'lodash'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns'; import { escapeForElasticsearchQuery } from '../../../../util/string_utils'; import { ml } from '../../../../services/ml_api_service'; @@ -53,69 +52,70 @@ export function getScoresByRecord( jobIdFilterStr += `"${String(firstSplitField.value).replace(/\\/g, '\\\\')}"`; } - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', + ml.results + .anomalySearch({ + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + }, }, - }, - { - bool: { - must: [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis', + { + bool: { + must: [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, }, }, - }, - { - query_string: { - query: jobIdFilterStr, + { + query_string: { + query: jobIdFilterStr, + }, }, - }, - ], + ], + }, }, - }, - ], - }, - }, - aggs: { - detector_index: { - terms: { - field: 'detector_index', - order: { - recordScore: 'desc', - }, + ], }, - aggs: { - recordScore: { - max: { - field: 'record_score', + }, + aggs: { + detector_index: { + terms: { + field: 'detector_index', + order: { + recordScore: 'desc', }, }, - byTime: { - date_histogram: { - field: 'timestamp', - interval, - min_doc_count: 1, - extended_bounds: { - min: earliestMs, - max: latestMs, + aggs: { + recordScore: { + max: { + field: 'record_score', }, }, - aggs: { - recordScore: { - max: { - field: 'record_score', + byTime: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 1, + extended_bounds: { + min: earliestMs, + max: latestMs, + }, + }, + aggs: { + recordScore: { + max: { + field: 'record_score', + }, }, }, }, @@ -123,8 +123,7 @@ export function getScoresByRecord( }, }, }, - }, - }) + }) .then((resp: any) => { const detectorsByIndex = get(resp, ['aggregations', 'detector_index', 'buckets'], []); detectorsByIndex.forEach((dtr: any) => { diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.js b/x-pack/plugins/ml/public/application/services/forecast_service.js index c3d593c3347d..ed5a29ff74a6 100644 --- a/x-pack/plugins/ml/public/application/services/forecast_service.js +++ b/x-pack/plugins/ml/public/application/services/forecast_service.js @@ -9,7 +9,6 @@ import _ from 'lodash'; import { map } from 'rxjs/operators'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { ml } from './ml_api_service'; // Gets a basic summary of the most recently run forecasts for the specified @@ -48,19 +47,19 @@ function getForecastsSummary(job, query, earliestMs, maxResults) { filterCriteria.push(query); } - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: maxResults, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: filterCriteria, + ml.results + .anomalySearch({ + size: maxResults, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: filterCriteria, + }, }, + sort: [{ forecast_create_timestamp: { order: 'desc' } }], }, - sort: [{ forecast_create_timestamp: { order: 'desc' } }], - }, - }) + }) .then((resp) => { if (resp.hits.total !== 0) { obj.forecasts = resp.hits.hits.map((hit) => hit._source); @@ -106,29 +105,29 @@ function getForecastDateRange(job, forecastId) { // TODO - add in criteria for detector index and entity fields (by, over, partition) // once forecasting with these parameters is supported. - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: { - earliest: { - min: { - field: 'timestamp', + ml.results + .anomalySearch({ + size: 0, + body: { + query: { + bool: { + filter: filterCriteria, }, }, - latest: { - max: { - field: 'timestamp', + aggs: { + earliest: { + min: { + field: 'timestamp', + }, + }, + latest: { + max: { + field: 'timestamp', + }, }, }, }, - }, - }) + }) .then((resp) => { obj.earliest = _.get(resp, 'aggregations.earliest.value', null); obj.latest = _.get(resp, 'aggregations.latest.value', null); @@ -243,9 +242,8 @@ function getForecastData( min: aggType.min, }; - return ml - .esSearch$({ - index: ML_RESULTS_INDEX_PATTERN, + return ml.results + .anomalySearch$({ size: 0, body: { query: { @@ -343,18 +341,18 @@ function getForecastRequestStats(job, forecastId) { }, ]; - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 1, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: filterCriteria, + ml.results + .anomalySearch({ + size: 1, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: filterCriteria, + }, }, }, - }, - }) + }) .then((resp) => { if (resp.hits.total !== 0) { obj.stats = _.first(resp.hits.hits)._source; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts index 521fd306847e..08c3853ace6f 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts @@ -96,4 +96,22 @@ export const resultsApiProvider = (httpService: HttpService) => ({ body, }); }, + + anomalySearch(obj: any) { + const body = JSON.stringify(obj); + return httpService.http({ + path: `${basePath()}/results/anomaly_search`, + method: 'POST', + body, + }); + }, + + anomalySearch$(obj: any) { + const body = JSON.stringify(obj); + return httpService.http$({ + path: `${basePath()}/results/anomaly_search`, + method: 'POST', + body, + }); + }, }); diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 1bcbd8dbcdd6..d7f016b41937 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -262,8 +262,8 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { }, ]; - return mlApiServices - .esSearch$({ + return mlApiServices.results + .anomalySearch$({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -399,8 +399,8 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { }); }); - return mlApiServices - .esSearch$({ + return mlApiServices.results + .anomalySearch$({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: maxResults !== undefined ? maxResults : 100, @@ -484,8 +484,8 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { }); } - return mlApiServices - .esSearch$({ + return mlApiServices.results + .anomalySearch$({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 55ddb1de3529..50e2d0a5a2a0 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { escapeForElasticsearchQuery } from '../../util/string_utils'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; import { ANOMALY_SWIM_LANE_HARD_LIMIT, SWIM_LANE_DEFAULT_PAGE_SIZE, @@ -66,9 +65,8 @@ export function resultsServiceProvider(mlApiServices) { }); } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: 0, body: { query: { @@ -238,9 +236,8 @@ export function resultsServiceProvider(mlApiServices) { }); } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: 0, body: { query: { @@ -378,9 +375,8 @@ export function resultsServiceProvider(mlApiServices) { }); } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: 0, body: { query: { @@ -560,9 +556,8 @@ export function resultsServiceProvider(mlApiServices) { }); } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: 0, body: { query: { @@ -721,9 +716,8 @@ export function resultsServiceProvider(mlApiServices) { }); } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: maxResults !== undefined ? maxResults : 100, rest_total_hits_as_int: true, body: { @@ -854,9 +848,8 @@ export function resultsServiceProvider(mlApiServices) { }); } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: maxResults !== undefined ? maxResults : 100, rest_total_hits_as_int: true, body: { @@ -980,9 +973,8 @@ export function resultsServiceProvider(mlApiServices) { } } - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: maxResults !== undefined ? maxResults : 100, rest_total_hits_as_int: true, body: { @@ -1307,9 +1299,8 @@ export function resultsServiceProvider(mlApiServices) { }); }); - mlApiServices - .esSearch({ - index: ML_RESULTS_INDEX_PATTERN, + mlApiServices.results + .anomalySearch({ size: 0, body: { query: { diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.ts b/x-pack/plugins/ml/server/lib/check_annotations/index.ts index fb37917c512c..de19f0ead679 100644 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.ts +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.ts @@ -18,17 +18,17 @@ import { // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present export async function isAnnotationsFeatureAvailable({ - callAsCurrentUser, + callAsInternalUser, }: ILegacyScopedClusterClient) { try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; - const annotationsIndexExists = await callAsCurrentUser('indices.exists', indexParams); + const annotationsIndexExists = await callAsInternalUser('indices.exists', indexParams); if (!annotationsIndexExists) { return false; } - const annotationsReadAliasExists = await callAsCurrentUser('indices.existsAlias', { + const annotationsReadAliasExists = await callAsInternalUser('indices.existsAlias', { index: ML_ANNOTATIONS_INDEX_ALIAS_READ, name: ML_ANNOTATIONS_INDEX_ALIAS_READ, }); @@ -37,7 +37,7 @@ export async function isAnnotationsFeatureAvailable({ return false; } - const annotationsWriteAliasExists = await callAsCurrentUser('indices.existsAlias', { + const annotationsWriteAliasExists = await callAsInternalUser('indices.existsAlias', { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, name: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, }); diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 3bf9bd0232a5..5be443266ffe 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -52,8 +52,8 @@ describe('annotation_service', () => { const response = await deleteAnnotation(annotationMockId); - expect(mockFunct.callAsCurrentUser.mock.calls[0][0]).toBe('delete'); - expect(mockFunct.callAsCurrentUser.mock.calls[0][1]).toEqual(deleteParamsMock); + expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('delete'); + expect(mockFunct.callAsInternalUser.mock.calls[0][1]).toEqual(deleteParamsMock); expect(response).toBe(acknowledgedResponseMock); done(); }); @@ -73,8 +73,8 @@ describe('annotation_service', () => { const response: GetResponse = await getAnnotations(indexAnnotationArgsMock); - expect(mockFunct.callAsCurrentUser.mock.calls[0][0]).toBe('search'); - expect(mockFunct.callAsCurrentUser.mock.calls[0][1]).toEqual(getAnnotationsRequestMock); + expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('search'); + expect(mockFunct.callAsInternalUser.mock.calls[0][1]).toEqual(getAnnotationsRequestMock); expect(Object.keys(response.annotations)).toHaveLength(1); expect(response.annotations[jobIdMock]).toHaveLength(2); expect(isAnnotations(response.annotations[jobIdMock])).toBeTruthy(); @@ -89,7 +89,7 @@ describe('annotation_service', () => { }; const mlClusterClientSpyError: any = { - callAsCurrentUser: jest.fn(() => { + callAsInternalUser: jest.fn(() => { return Promise.resolve(mockEsError); }), }; @@ -124,10 +124,10 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(mockFunct.callAsCurrentUser.mock.calls[0][0]).toBe('index'); + expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('index'); // test if the annotation has been correctly augmented - const indexParamsCheck = mockFunct.callAsCurrentUser.mock.calls[0][1]; + const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[0][1]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -154,10 +154,10 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(mockFunct.callAsCurrentUser.mock.calls[0][0]).toBe('index'); + expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('index'); // test if the annotation has been correctly augmented - const indexParamsCheck = mockFunct.callAsCurrentUser.mock.calls[0][1]; + const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[0][1]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -196,9 +196,9 @@ describe('annotation_service', () => { await indexAnnotation(annotation, modifiedUsernameMock); - expect(mockFunct.callAsCurrentUser.mock.calls[1][0]).toBe('index'); + expect(mockFunct.callAsInternalUser.mock.calls[1][0]).toBe('index'); // test if the annotation has been correctly updated - const indexParamsCheck = mockFunct.callAsCurrentUser.mock.calls[1][1]; + const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[1][1]; const modifiedAnnotation = indexParamsCheck.body; expect(modifiedAnnotation.annotation).toBe(modifiedAnnotationText); expect(modifiedAnnotation.create_username).toBe(originalUsernameMock); diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index f7353034b745..8094689abf3e 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -76,7 +76,7 @@ export interface DeleteParams { id: string; } -export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { async function indexAnnotation(annotation: Annotation, username: string) { if (isAnnotation(annotation) === false) { // No need to translate, this will not be exposed in the UI. @@ -103,7 +103,7 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl delete params.body.key; } - return await callAsCurrentUser('index', params); + return await callAsInternalUser('index', params); } async function getAnnotations({ @@ -286,7 +286,7 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl }; try { - const resp = await callAsCurrentUser('search', params); + const resp = await callAsInternalUser('search', params); if (resp.error !== undefined && resp.message !== undefined) { // No need to translate, this will not be exposed in the UI. @@ -335,7 +335,7 @@ export function annotationProvider({ callAsCurrentUser }: ILegacyScopedClusterCl refresh: 'wait_for', }; - return await callAsCurrentUser('delete', param); + return await callAsInternalUser('delete', param); } return { diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index c8471b546220..1cb0656e88a0 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -23,7 +23,7 @@ interface BoolQuery { bool: { [key: string]: any }; } -export function analyticsAuditMessagesProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function analyticsAuditMessagesProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { // search for audit messages, // analyticsId is optional. without it, all analytics will be listed. async function getAnalyticsAuditMessages(analyticsId: string) { @@ -69,7 +69,7 @@ export function analyticsAuditMessagesProvider({ callAsCurrentUser }: ILegacySco } try { - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, rest_total_hits_as_int: true, diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index dcbabd879b47..86d80c394137 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -34,7 +34,7 @@ const anomalyDetectorTypeFilter = { }, }; -export function jobAuditMessagesProvider({ callAsCurrentUser, callAsInternalUser }) { +export function jobAuditMessagesProvider({ callAsInternalUser }) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. // from is optional and should be a string formatted in ES time units. e.g. 12h, 1d, 7d @@ -100,7 +100,7 @@ export function jobAuditMessagesProvider({ callAsCurrentUser, callAsInternalUser } try { - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, rest_total_hits_as_int: true, @@ -155,7 +155,7 @@ export function jobAuditMessagesProvider({ callAsCurrentUser, callAsInternalUser levelsPerJobAggSize = jobIds.length; } - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, rest_total_hits_as_int: true, diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index e9ed2d0941d9..0aa1cfdae13c 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -48,7 +48,7 @@ interface Results { } export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsCurrentUser, callAsInternalUser } = mlClusterClient; + const { callAsInternalUser } = mlClusterClient; const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(mlClusterClient); const { getAuditMessagesSummary } = jobAuditMessagesProvider(mlClusterClient); @@ -400,7 +400,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const detailed = true; const jobIds = []; try { - const tasksList = await callAsCurrentUser('tasks.list', { actions, detailed }); + const tasksList = await callAsInternalUser('tasks.list', { actions, detailed }); Object.keys(tasksList.nodes).forEach((nodeId) => { const tasks = tasksList.nodes[nodeId].tasks; Object.keys(tasks).forEach((taskId) => { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 4f97238a4a0b..5ade86806f38 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -9,9 +9,9 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; import { CategoryId, Category } from '../../../../../common/types/categories'; -export function topCategoriesProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { async function getTotalCategories(jobId: string): Promise<{ total: number }> { - const totalResp = await callAsCurrentUser('search', { + const totalResp = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -37,7 +37,7 @@ export function topCategoriesProvider({ callAsCurrentUser }: ILegacyScopedCluste } async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { - const top: SearchResponse = await callAsCurrentUser('search', { + const top: SearchResponse = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -99,7 +99,7 @@ export function topCategoriesProvider({ callAsCurrentUser }: ILegacyScopedCluste field: 'category_id', }, }; - const result: SearchResponse = await callAsCurrentUser('search', { + const result: SearchResponse = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, size, body: { diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 663ee846571e..9c0efe259844 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -75,7 +75,6 @@ function getFieldObject(fieldType: PartitionFieldsType, aggs: any) { } export const getPartitionFieldsValuesFactory = ({ - callAsCurrentUser, callAsInternalUser, }: ILegacyScopedClusterClient) => /** @@ -102,7 +101,7 @@ export const getPartitionFieldsValuesFactory = ({ const isModelPlotEnabled = job?.model_plot_config?.enabled; - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 8e904143263d..04997e517bba 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -31,7 +31,7 @@ interface Influencer { } export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsCurrentUser } = mlClusterClient; + const { callAsInternalUser } = mlClusterClient; // Obtains data for the anomalies table, aggregating anomalies by day or hour as requested. // Return an Object with properties 'anomalies' and 'interval' (interval used to aggregate anomalies, // one of day, hour or second. Note 'auto' can be provided as the aggregationInterval in the request, @@ -134,7 +134,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); } - const resp: SearchResponse = await callAsCurrentUser('search', { + const resp: SearchResponse = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: maxRecords, @@ -288,7 +288,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }; - const resp = await callAsCurrentUser('search', query); + const resp = await callAsInternalUser('search', query); const maxScore = _.get(resp, ['aggregations', 'max_score', 'value'], null); return { maxScore }; @@ -326,7 +326,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // Size of job terms agg, consistent with maximum number of jobs supported by Java endpoints. const maxJobs = 10000; - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -370,7 +370,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // from the given index and job ID. // Returned response consists of a list of examples against category ID. async function getCategoryExamples(jobId: string, categoryIds: any, maxExamples: number) { - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, // Matches size of records in anomaly summary table. @@ -405,7 +405,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // Returned response contains four properties - categoryId, regex, examples // and terms (space delimited String of the common tokens matched in values of the category). async function getCategoryDefinition(jobId: string, categoryId: string) { - const resp = await callAsCurrentUser('search', { + const resp = await callAsInternalUser('search', { index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: 1, diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 98f7a78537c5..f360da5df539 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -48,6 +48,7 @@ "GetMaxAnomalyScore", "GetCategoryExamples", "GetPartitionFieldsValues", + "AnomalySearch", "Modules", "DataRecognizer", diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 3e6c6f5f6a2f..94feb21a6b5f 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -513,7 +513,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; - const results = await context.ml!.mlClient.callAsCurrentUser( + const results = await context.ml!.mlClient.callAsInternalUser( 'ml.updateDataFrameAnalytics', { body: request.body, diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index c7fcebd2a29a..c9370362816f 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -5,6 +5,7 @@ */ import { RequestHandlerContext } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -15,6 +16,7 @@ import { partitionFieldValuesSchema, } from './schemas/results_service_schema'; import { resultsServiceProvider } from '../models/results_service'; +import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; function getAnomaliesTableData(context: RequestHandlerContext, payload: any) { const rs = resultsServiceProvider(context.ml!.mlClient); @@ -232,4 +234,35 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) } }) ); + + /** + * @apiGroup ResultsService + * + * @api {post} /api/ml/results/anomaly_search Performs a search on the anomaly results index + * @apiName AnomalySearch + */ + router.post( + { + path: '/api/ml/results/anomaly_search', + validate: { + body: schema.maybe(schema.any()), + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + const body = { + ...request.body, + index: ML_RESULTS_INDEX_PATTERN, + }; + try { + return response.ok({ + body: await context.ml!.mlClient.callAsInternalUser('search', body), + }); + } catch (error) { + return response.customError(wrapError(error)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index ec2662014546..d292abc438a2 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -37,7 +37,7 @@ export function getMlSystemProvider( return { mlSystemProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { // const hasMlCapabilities = getHasMlCapabilities(request); - const { callAsCurrentUser, callAsInternalUser } = mlClusterClient; + const { callAsInternalUser } = mlClusterClient; return { async mlCapabilities() { isMinimumLicense(); @@ -77,7 +77,7 @@ export function getMlSystemProvider( // integration and currently alerting does not supply a request object. // await hasMlCapabilities(['canAccessML']); - return callAsCurrentUser('search', { + return callAsInternalUser('search', { ...searchParams, index: ML_RESULTS_INDEX_PATTERN, }); diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index eea0a83879ea..8c5e40dd5dbd 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -101,7 +101,7 @@ export function MachineLearningDataVisualizerFileBasedProvider( }, async startImportAndWaitForProcessing() { - await testSubjects.click('mlFileDataVisImportButton'); + await testSubjects.clickWhenNotDisabled('mlFileDataVisImportButton'); await retry.tryForTime(60 * 1000, async () => { await testSubjects.existOrFail('mlFileImportSuccessCallout'); }); From badbfa0eb5f6aee037b37ae72b9d9e77c392e210 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 23 Jul 2020 08:39:51 -0700 Subject: [PATCH 10/20] Added more {{context}} fields for Index Threshold alert type (including requested 'threshold' field). Extended action variables UX with tooltip containing variable description. (#71141) * Added more {{context}} fields for Index Threshold alert type (including requested 'threshold' field). Extended action variables UX with tooltip containing variable description. * Fixed type checks and failing tests * fixed type check * Splited params variables * Fixed tests and type checks * Fixed styles * Fixed type check * fixed styles * fixed missing type * Fixed due to comments * fixed variables description * fixed type check * Fixed due to comments * fixed typecheck * Merge remote-tracking branch upstream/master into alerting-additional-context-fields * fixed type checks and tests * fixed tests --- .../index_threshold/action_context.test.ts | 30 ++++++------ .../index_threshold/alert_type.test.ts | 46 +++++++++++++++++++ .../alert_types/index_threshold/alert_type.ts | 30 ++++++++++++ .../alerts/server/alert_type_registry.test.ts | 2 + .../alerts/server/alert_type_registry.ts | 1 + .../create_execution_handler.test.ts | 5 ++ .../task_runner/create_execution_handler.ts | 5 +- .../alerts/server/task_runner/task_runner.ts | 7 ++- .../transform_action_params.test.ts | 17 +++++++ .../task_runner/transform_action_params.ts | 5 +- x-pack/plugins/alerts/server/types.ts | 3 ++ .../rules/step_rule_actions/index.tsx | 3 +- .../pages/detection_engine/rules/helpers.tsx | 28 ++++++----- x-pack/plugins/triggers_actions_ui/README.md | 4 +- .../components/add_message_variables.scss | 1 + .../components/add_message_variables.tsx | 24 +++++++--- .../servicenow/servicenow_params.tsx | 2 +- .../json_editor_with_message_variables.tsx | 3 +- .../text_area_with_message_variables.tsx | 3 +- .../text_field_with_message_variables.tsx | 3 +- .../application/lib/action_variables.test.ts | 9 +++- .../application/lib/action_variables.ts | 3 +- .../public/application/lib/alert_api.test.ts | 1 + .../action_form.test.tsx | 5 +- .../action_connector_form/action_form.tsx | 3 +- .../components/alert_details.test.tsx | 36 +++++++-------- .../sections/alert_form/alert_add.test.tsx | 1 + .../sections/alert_form/alert_form.tsx | 4 +- .../triggers_actions_ui/public/index.ts | 1 + .../triggers_actions_ui/public/types.ts | 3 +- .../plugins/alerts/server/alert_types.ts | 1 + .../tests/alerting/list_alert_types.ts | 2 + .../tests/alerting/list_alert_types.ts | 4 ++ .../apps/triggers_actions_ui/alerts.ts | 2 +- 34 files changed, 228 insertions(+), 69 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts index a72a7343c590..3f5addb77cb3 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts @@ -9,11 +9,6 @@ import { ParamsSchema } from './alert_type_params'; describe('ActionContext', () => { it('generates expected properties if aggField is null', async () => { - const base: BaseActionContext = { - date: '2020-01-01T00:00:00.000Z', - group: '[group]', - value: 42, - }; const params = ParamsSchema.validate({ index: '[index]', timeField: '[timeField]', @@ -26,6 +21,11 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4], }); + const base: BaseActionContext = { + date: '2020-01-01T00:00:00.000Z', + group: '[group]', + value: 42, + }; const context = addMessages({ name: '[alert-name]' }, base, params); expect(context.title).toMatchInlineSnapshot( `"alert [alert-name] group [group] exceeded threshold"` @@ -36,11 +36,6 @@ describe('ActionContext', () => { }); it('generates expected properties if aggField is not null', async () => { - const base: BaseActionContext = { - date: '2020-01-01T00:00:00.000Z', - group: '[group]', - value: 42, - }; const params = ParamsSchema.validate({ index: '[index]', timeField: '[timeField]', @@ -54,6 +49,11 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4.2], }); + const base: BaseActionContext = { + date: '2020-01-01T00:00:00.000Z', + group: '[group]', + value: 42, + }; const context = addMessages({ name: '[alert-name]' }, base, params); expect(context.title).toMatchInlineSnapshot( `"alert [alert-name] group [group] exceeded threshold"` @@ -64,11 +64,6 @@ describe('ActionContext', () => { }); it('generates expected properties if comparator is between', async () => { - const base: BaseActionContext = { - date: '2020-01-01T00:00:00.000Z', - group: '[group]', - value: 4, - }; const params = ParamsSchema.validate({ index: '[index]', timeField: '[timeField]', @@ -81,6 +76,11 @@ describe('ActionContext', () => { thresholdComparator: 'between', threshold: [4, 5], }); + const base: BaseActionContext = { + date: '2020-01-01T00:00:00.000Z', + group: '[group]', + value: 4, + }; const context = addMessages({ name: '[alert-name]' }, base, params); expect(context.title).toMatchInlineSnapshot( `"alert [alert-name] group [group] exceeded threshold"` diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts index d3583fd4cdb0..e33a3e775ca9 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts @@ -47,6 +47,52 @@ describe('alertType', () => { "name": "value", }, ], + "params": Array [ + Object { + "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + "name": "threshold", + }, + Object { + "description": "A comparison function to use to determine if the threshold as been met.", + "name": "thresholdComparator", + }, + Object { + "description": "index", + "name": "index", + }, + Object { + "description": "timeField", + "name": "timeField", + }, + Object { + "description": "aggType", + "name": "aggType", + }, + Object { + "description": "aggField", + "name": "aggField", + }, + Object { + "description": "groupBy", + "name": "groupBy", + }, + Object { + "description": "termField", + "name": "termField", + }, + Object { + "description": "termSize", + "name": "termSize", + }, + Object { + "description": "timeWindowSize", + "name": "timeWindowSize", + }, + Object { + "description": "timeWindowUnit", + "name": "timeWindowUnit", + }, + ], } `); }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts index 153334cb6404..c0522c08a7b9 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts @@ -14,6 +14,7 @@ import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../common'; export const ID = '.index-threshold'; +import { CoreQueryParamsSchemaProperties } from './lib/core_query_types'; const ActionGroupId = 'threshold met'; const ComparatorFns = getComparatorFns(); export const ComparatorFnNames = new Set(ComparatorFns.keys()); @@ -67,6 +68,30 @@ export function getAlertType(service: Service): AlertType { } ); + const actionVariableContextThresholdLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextThresholdLabel', + { + defaultMessage: + "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + } + ); + + const actionVariableContextThresholdComparatorLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextThresholdComparatorLabel', + { + defaultMessage: 'A comparison function to use to determine if the threshold as been met.', + } + ); + + const alertParamsVariables = Object.keys(CoreQueryParamsSchemaProperties).map( + (propKey: string) => { + return { + name: propKey, + description: propKey, + }; + } + ); + return { id: ID, name: alertTypeName, @@ -83,6 +108,11 @@ export function getAlertType(service: Service): AlertType { { name: 'date', description: actionVariableContextDateLabel }, { name: 'value', description: actionVariableContextValueLabel }, ], + params: [ + { name: 'threshold', description: actionVariableContextThresholdLabel }, + { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, + ...alertParamsVariables, + ], }, executor, producer: BUILT_IN_ALERTS_FEATURE_ID, diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index c74039071371..229847bda183 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -208,6 +208,7 @@ describe('get()', () => { ], "actionVariables": Object { "context": Array [], + "params": Array [], "state": Array [], }, "defaultActionGroupId": "default", @@ -261,6 +262,7 @@ describe('list()', () => { ], "actionVariables": Object { "context": Array [], + "params": Array [], "state": Array [], }, "defaultActionGroupId": "testActionGroup", diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index c466d0e96382..19d3bf13bd66 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -119,5 +119,6 @@ function normalizedActionVariables(actionVariables: AlertType['actionVariables'] return { context: actionVariables?.context ?? [], state: actionVariables?.state ?? [], + params: actionVariables?.params ?? [], }; } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 3ea40fe4c308..677040d8174e 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -50,6 +50,11 @@ const createExecutionHandlerParams = { }, ], request: {} as KibanaRequest, + alertParams: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, }; beforeEach(() => { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index e1e1568d2f13..c21d81779e5e 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -5,7 +5,7 @@ */ import { map } from 'lodash'; -import { AlertAction, State, Context, AlertType } from '../types'; +import { AlertAction, State, Context, AlertType, AlertParams } from '../types'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; @@ -24,6 +24,7 @@ interface CreateExecutionHandlerOptions { logger: Logger; eventLogger: IEventLogger; request: KibanaRequest; + alertParams: AlertParams; } interface ExecutionHandlerOptions { @@ -45,6 +46,7 @@ export function createExecutionHandler({ alertType, eventLogger, request, + alertParams, }: CreateExecutionHandlerOptions) { const alertTypeActionGroups = new Set(map(alertType.actionGroups, 'id')); return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => { @@ -66,6 +68,7 @@ export function createExecutionHandler({ context, actionParams: action.params, state, + alertParams, }), }; }); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index e4d04a005c98..04fea58f250a 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -110,7 +110,8 @@ export class TaskRunner { tags: string[] | undefined, spaceId: string, apiKey: string | null, - actions: Alert['actions'] + actions: Alert['actions'], + alertParams: RawAlert['params'] ) { return createExecutionHandler({ alertId, @@ -124,6 +125,7 @@ export class TaskRunner { alertType: this.alertType, eventLogger: this.context.eventLogger, request: this.getFakeKibanaRequest(spaceId, apiKey), + alertParams, }); } @@ -261,7 +263,8 @@ export class TaskRunner { alert.tags, spaceId, apiKey, - alert.actions + alert.actions, + alert.params ); return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId); } diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts index d5c310caf3fd..ddbef8e32e70 100644 --- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts @@ -13,6 +13,7 @@ test('skips non string parameters', () => { empty1: null, empty2: undefined, date: '2019-02-12T21:01:22.479Z', + message: 'Value "{{params.foo}}" exists', }; const result = transformActionParams({ actionParams, @@ -23,6 +24,9 @@ test('skips non string parameters', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: { + foo: 'test', + }, }); expect(result).toMatchInlineSnapshot(` Object { @@ -30,6 +34,7 @@ test('skips non string parameters', () => { "date": "2019-02-12T21:01:22.479Z", "empty1": null, "empty2": undefined, + "message": "Value \\"test\\" exists", "number": 1, } `); @@ -49,6 +54,7 @@ test('missing parameters get emptied out', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -71,6 +77,7 @@ test('context parameters are passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -92,6 +99,7 @@ test('state parameters are passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -113,6 +121,7 @@ test('alertId is passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -134,6 +143,7 @@ test('alertName is passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -155,6 +165,7 @@ test('tags is passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -175,6 +186,7 @@ test('undefined tags is passed to templates', () => { alertName: 'alert-name', spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -196,6 +208,7 @@ test('empty tags is passed to templates', () => { tags: [], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -217,6 +230,7 @@ test('spaceId is passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -238,6 +252,7 @@ test('alertInstanceId is passed to templates', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -261,6 +276,7 @@ test('works recursively', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { @@ -286,6 +302,7 @@ test('works recursively with arrays', () => { tags: ['tag-A', 'tag-B'], spaceId: 'spaceId-A', alertInstanceId: '2', + alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts index fa4a0e40ddee..30f062eee370 100644 --- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts @@ -6,7 +6,7 @@ import Mustache from 'mustache'; import { isString, cloneDeepWith } from 'lodash'; -import { AlertActionParams, State, Context } from '../types'; +import { AlertActionParams, State, Context, AlertParams } from '../types'; interface TransformActionParamsOptions { alertId: string; @@ -17,6 +17,7 @@ interface TransformActionParamsOptions { actionParams: AlertActionParams; state: State; context: Context; + alertParams: AlertParams; } export function transformActionParams({ @@ -28,6 +29,7 @@ export function transformActionParams({ context, actionParams, state, + alertParams, }: TransformActionParamsOptions): AlertActionParams { const result = cloneDeepWith(actionParams, (value: unknown) => { if (!isString(value)) return; @@ -43,6 +45,7 @@ export function transformActionParams({ alertInstanceId, context, state, + params: alertParams, }; return Mustache.render(value, variables); }); diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 66eec370f2c2..154a9564518e 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -23,6 +23,8 @@ import { export type State = Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Context = Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AlertParams = Record; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; export type GetBasePathFunction = (spaceId?: string) => string; @@ -82,6 +84,7 @@ export interface AlertType { actionVariables?: { context?: ActionVariable[]; state?: ActionVariable[]; + params?: ActionVariable[]; }; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 2b842515d0b7..5b4f7677dbc3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -15,6 +15,7 @@ import { import { findIndex } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { ActionVariable } from '../../../../../../triggers_actions_ui/public'; import { RuleStep, RuleStepProps, @@ -36,7 +37,7 @@ import { APP_ID } from '../../../../../common/constants'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; - actionMessageParams: string[]; + actionMessageParams: ActionVariable[]; } const stepActionsDefaultValue = { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 11b779e71b9b..8f8967f2ff6d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -9,6 +9,7 @@ import moment from 'moment'; import memoizeOne from 'memoize-one'; import { useLocation } from 'react-router-dom'; +import { ActionVariable } from '../../../../../../triggers_actions_ui/public'; import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; @@ -326,18 +327,23 @@ export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { return ruleParamsKeys; }; -export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { - if (!ruleType) { - return []; +export const getActionMessageParams = memoizeOne( + (ruleType: RuleType | undefined): ActionVariable[] => { + if (!ruleType) { + return []; + } + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return [ + { name: 'state.signals_count', description: 'state.signals_count' }, + { name: '{context.results_link}', description: 'context.results_link' }, + ...actionMessageRuleParams.map((param) => { + const extendedParam = `context.rule.${param}`; + return { name: extendedParam, description: extendedParam }; + }), + ]; } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - - return [ - 'state.signals_count', - '{context.results_link}', - ...actionMessageRuleParams.map((param) => `context.rule.${param}`), - ]; -}); +); // typed as null not undefined as the initial state for this value is null. export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 0dd2d100401f..b8e765c9ea63 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -1294,7 +1294,7 @@ Then this dependencies will be used to embed Actions form or register your own a return ( { initialAlert.actions[index].id = id; @@ -1329,7 +1329,7 @@ interface ActionAccordionFormProps { 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' >; actionTypes?: ActionType[]; - messageVariables?: string[]; + messageVariables?: ActionVariable[]; defaultActionMessage?: string; consumer: string; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss index 996f21c4b6b0..521d0f399b19 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss @@ -1,4 +1,5 @@ .messageVariablesPanel { @include euiYScrollWithShadows; max-height: $euiSize * 20; + max-width: $euiSize * 20; } \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 655f64995d14..0742ed8a778e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -5,11 +5,18 @@ */ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { + EuiPopover, + EuiButtonIcon, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiText, +} from '@elastic/eui'; import './add_message_variables.scss'; +import { ActionVariable } from '../../types'; interface Props { - messageVariables: string[] | undefined; + messageVariables?: ActionVariable[]; paramsProperty: string; onSelectEventHandler: (variable: string) => void; } @@ -22,17 +29,22 @@ export const AddMessageVariables: React.FunctionComponent = ({ const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); const getMessageVariables = () => - messageVariables?.map((variable: string, i: number) => ( + messageVariables?.map((variable: ActionVariable, i: number) => ( { - onSelectEventHandler(variable); + onSelectEventHandler(variable.name); setIsVariablesPopoverOpen(false); }} > - {`{{${variable}}}`} + <> + {`{{${variable.name}}}`} + +

{variable.description}
+
+ )); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx index 1e0f4d1fdc57..2a29018d83ff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx @@ -61,7 +61,7 @@ const ServiceNowParamsFields: React.FunctionComponent variable === 'alertId')) { + if (!savedObjectId && messageVariables?.find((variable) => variable.name === 'alertId')) { editSubActionProperty('savedObjectId', '{{alertId}}'); } if (!urgency) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 473c0fe9609c..0b8184fc441f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -9,9 +9,10 @@ import './add_message_variables.scss'; import { useXJsonMode } from '../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; import { AddMessageVariables } from './add_message_variables'; +import { ActionVariable } from '../../types'; interface Props { - messageVariables: string[] | undefined; + messageVariables?: ActionVariable[]; paramsProperty: string; inputTargetValue: string; label: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 0b8a9349ad5f..e60785f70bff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -7,9 +7,10 @@ import React, { useState } from 'react'; import { EuiTextArea, EuiFormRow } from '@elastic/eui'; import './add_message_variables.scss'; import { AddMessageVariables } from './add_message_variables'; +import { ActionVariable } from '../../types'; interface Props { - messageVariables: string[] | undefined; + messageVariables?: ActionVariable[]; paramsProperty: string; index: number; inputTargetValue?: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index e280fd3f34e9..fc05b237ccf5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -7,9 +7,10 @@ import React, { useState } from 'react'; import { EuiFieldText } from '@elastic/eui'; import './add_message_variables.scss'; import { AddMessageVariables } from './add_message_variables'; +import { ActionVariable } from '../../types'; interface Props { - messageVariables: string[] | undefined; + messageVariables?: ActionVariable[]; paramsProperty: string; index: number; inputTargetValue?: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index ddd03df8bee6..c5009fad3294 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -12,7 +12,7 @@ beforeEach(() => jest.resetAllMocks()); describe('actionVariablesFromAlertType', () => { test('should return correct variables when no state or context provided', async () => { - const alertType = getAlertType({ context: [], state: [] }); + const alertType = getAlertType({ context: [], state: [], params: [] }); expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` Array [ Object { @@ -46,6 +46,7 @@ describe('actionVariablesFromAlertType', () => { { name: 'bar', description: 'bar-description' }, ], state: [], + params: [], }); expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` Array [ @@ -88,6 +89,7 @@ describe('actionVariablesFromAlertType', () => { { name: 'foo', description: 'foo-description' }, { name: 'bar', description: 'bar-description' }, ], + params: [], }); expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` Array [ @@ -133,6 +135,7 @@ describe('actionVariablesFromAlertType', () => { { name: 'fooS', description: 'fooS-description' }, { name: 'barS', description: 'barS-description' }, ], + params: [{ name: 'fooP', description: 'fooP-description' }], }); expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` Array [ @@ -164,6 +167,10 @@ describe('actionVariablesFromAlertType', () => { "description": "barC-description", "name": "context.barC", }, + Object { + "description": "fooP-description", + "name": "params.fooP", + }, Object { "description": "fooS-description", "name": "state.fooS", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index 714dc5210e39..8bbe34847016 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -11,9 +11,10 @@ import { AlertType, ActionVariable } from '../../types'; export function actionVariablesFromAlertType(alertType: AlertType): ActionVariable[] { const alwaysProvidedVars = getAlwaysProvidedActionVariables(); const contextVars = prefixKeys(alertType.actionVariables.context, 'context.'); + const paramsVars = prefixKeys(alertType.actionVariables.params, 'params.'); const stateVars = prefixKeys(alertType.actionVariables.state, 'state.'); - return alwaysProvidedVars.concat(contextVars, stateVars); + return alwaysProvidedVars.concat(contextVars, paramsVars, stateVars); } function prefixKeys(actionVariables: ActionVariable[], prefix: string): ActionVariable[] { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 23caf2cfb31a..fc5d301cb7cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -42,6 +42,7 @@ describe('loadAlertTypes', () => { actionVariables: { context: [{ name: 'var1', description: 'val1' }], state: [{ name: 'var2', description: 'val2' }], + params: [{ name: 'var3', description: 'val3' }], }, producer: ALERTS_FEATURE_ID, actionGroups: [{ id: 'default', name: 'Default' }], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index c21cce4cc4b6..7ee1e0d3f3fa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -217,7 +217,10 @@ describe('action_form', () => { wrapper = mountWithIntl( { initialAlert.actions[index].id = id; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index af10f583dd41..2d4507ca9307 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -38,6 +38,7 @@ import { ActionTypeIndex, ActionConnector, ActionType, + ActionVariable, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; @@ -61,7 +62,7 @@ interface ActionAccordionFormProps { >; docLinks: DocLinksStart; actionTypes?: ActionType[]; - messageVariables?: string[]; + messageVariables?: ActionVariable[]; defaultActionMessage?: string; setHasActionsDisabled?: (value: boolean) => void; capabilities: ApplicationStart['capabilities']; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index ccaa180de0ed..a620a0db4540 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -93,7 +93,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -132,7 +132,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -162,7 +162,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -216,7 +216,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -275,7 +275,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -295,7 +295,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -324,7 +324,7 @@ describe('disable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -352,7 +352,7 @@ describe('disable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -380,7 +380,7 @@ describe('disable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -417,7 +417,7 @@ describe('disable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -457,7 +457,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -486,7 +486,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -515,7 +515,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -553,7 +553,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -591,7 +591,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, @@ -641,7 +641,7 @@ describe('edit button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: 'alerting', authorizedConsumers, @@ -683,7 +683,7 @@ describe('edit button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: 'alerting', authorizedConsumers, @@ -718,7 +718,7 @@ describe('edit button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], - actionVariables: { context: [], state: [] }, + actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: 'alerting', authorizedConsumers, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 10efabd70ade..3803fcebbb92 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -68,6 +68,7 @@ describe('alert_add', () => { actionVariables: { context: [], state: [], + params: [], }, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 47ec2c436ca5..9d54baf359af 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -269,8 +269,8 @@ export const AlertForm = ({ setHasActionsDisabled={setHasActionsDisabled} messageVariables={ alertTypesIndex && alertTypesIndex.has(alert.alertTypeId) - ? actionVariablesFromAlertType(alertTypesIndex.get(alert.alertTypeId)!).map( - (av) => av.name + ? actionVariablesFromAlertType(alertTypesIndex.get(alert.alertTypeId)!).sort((a, b) => + a.name.toUpperCase().localeCompare(b.name.toUpperCase()) ) : undefined } diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 55653f49001b..1048e15eb118 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -19,6 +19,7 @@ export { ActionType, ActionTypeRegistryContract, AlertTypeParamsExpressionProps, + ActionVariable, } from './types'; export { ConnectorAddFlyout, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index dd2b070956db..a42a9f56a751 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -41,7 +41,7 @@ export interface ActionParamsProps { index: number; editAction: (property: string, value: any, index: number) => void; errors: IErrorObject; - messageVariables?: string[]; + messageVariables?: ActionVariable[]; defaultMessage?: string; docLinks: DocLinksStart; } @@ -94,6 +94,7 @@ export interface ActionVariable { export interface ActionVariables { context: ActionVariable[]; state: ActionVariable[]; + params: ActionVariable[]; } export interface AlertType { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 26010e5a2c2e..ebf639067518 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -26,6 +26,7 @@ export function defineAlertTypes( defaultActionGroupId: 'default', actionVariables: { state: [{ name: 'instanceStateValue', description: 'the instance state value' }], + params: [{ name: 'instanceParamsValue', description: 'the instance params value' }], context: [{ name: 'instanceContextValue', description: 'the instance context value' }], }, async executor(alertExecutorOptions: AlertExecutorOptions) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index c3e5af0d1f77..ad60ed6941ca 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -22,6 +22,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { actionVariables: { state: [], context: [], + params: [], }, producer: 'alertsFixture', }; @@ -34,6 +35,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { actionVariables: { state: [], context: [], + params: [], }, producer: 'alertsRestrictedFixture', }; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index dd09a14b4cb8..6fb573c7344b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -29,6 +29,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { name: 'Test: Noop', actionVariables: { state: [], + params: [], context: [], }, producer: 'alertsFixture', @@ -48,6 +49,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType.actionVariables).to.eql({ state: [{ name: 'instanceStateValue', description: 'the instance state value' }], + params: [{ name: 'instanceParamsValue', description: 'the instance params value' }], context: [{ name: 'instanceContextValue', description: 'the instance context value' }], }); }); @@ -64,6 +66,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType.actionVariables).to.eql({ state: [], + params: [], context: [{ name: 'aContextVariable', description: 'this is a context variable' }], }); }); @@ -81,6 +84,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType.actionVariables).to.eql({ state: [{ name: 'aStateVariable', description: 'this is a state variable' }], context: [], + params: [], }); }); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 09c415685450..fa714e8374ec 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -86,7 +86,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('variableMenuButton-1'); expect(await messageTextArea.getAttribute('value')).to.eql( - 'test message {{alertId}} some additional text {{alertName}}' + 'test message {{alertId}} some additional text {{alertInstanceId}}' ); await testSubjects.click('saveAlertButton'); From 6b9a598f731142657df1c729d07949293ee63fb0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 23 Jul 2020 18:43:54 +0300 Subject: [PATCH 11/20] [Security Solution][Case] Fix long tag display (#72819) --- .../public/cases/components/all_cases/columns.tsx | 10 +++++++--- .../cases/components/case_view/index.test.tsx | 8 +++++++- .../cases/components/tag_list/index.test.tsx | 6 +++--- .../public/cases/components/tag_list/index.tsx | 15 ++++++++------- .../cases/components/user_action_tree/helpers.tsx | 12 ++++++------ 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx index 162966a2df28..5c6c72477bf1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx @@ -6,6 +6,7 @@ import React, { useCallback } from 'react'; import { EuiAvatar, + EuiBadgeGroup, EuiBadge, EuiLink, EuiTableActionsColumnType, @@ -19,7 +20,6 @@ import { getEmptyTagValue } from '../../../common/components/empty_value'; import { Case } from '../../containers/types'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import { CaseDetailsLink } from '../../../common/components/links'; -import { TruncatableText } from '../../../common/components/truncatable_text'; import * as i18n from './translations'; export type CasesColumns = @@ -35,6 +35,10 @@ const Spacer = styled.span` margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; +const TagWrapper = styled(EuiBadgeGroup)` + width: 100%; +`; + const renderStringField = (field: string, dataTestSubj: string) => field != null ? {field} : getEmptyTagValue(); @@ -96,7 +100,7 @@ export const getCasesColumns = ( render: (tags: Case['tags']) => { if (tags != null && tags.length > 0) { return ( - + {tags.map((tag: string, i: number) => ( ))} - + ); } return getEmptyTagValue(); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 278b972ada97..e1d7d98ba8c5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -119,10 +119,16 @@ describe('CaseView ', () => { ); expect( wrapper - .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`) + .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag-coke"]`) .first() .text() ).toEqual(data.tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag-pepsi"]`) + .first() + .text() + ).toEqual(data.tags[1]); expect(wrapper.find(`[data-test-subj="case-view-username"]`).first().text()).toEqual( data.createdBy.username ); diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx index 939ddfde8b9d..7c3fcde68703 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx @@ -102,14 +102,14 @@ describe('TagList ', () => { ); - expect(wrapper.find(`[data-test-subj="case-tag"]`).last().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeTruthy(); wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click'); await act(async () => { - expect(wrapper.find(`[data-test-subj="case-tag"]`).last().exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeFalsy(); wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click'); await waitFor(() => { wrapper.update(); - expect(wrapper.find(`[data-test-subj="case-tag"]`).last().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx index 7bb10c743a41..b5af1934f379 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx @@ -10,6 +10,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, + EuiBadgeGroup, EuiBadge, EuiButton, EuiButtonEmpty, @@ -98,15 +99,15 @@ export const TagList = React.memo( {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} - {tags.length > 0 && - !isEditTags && - tags.map((tag, key) => ( - - + + {tags.length > 0 && + !isEditTags && + tags.map((tag, key) => ( + {tag} - - ))} + ))} + {isEditTags && ( diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx index a6286693423c..1401ac2c4652 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiBadgeGroup, EuiBadge, EuiLink } from '@elastic/eui'; import React from 'react'; import { CaseFullExternalService, Connector } from '../../../../../case/common/api'; @@ -50,14 +50,14 @@ const getTagsLabelTitle = (action: CaseUserActions) => ( {action.action === 'add' && i18n.ADDED_FIELD} {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} - {action.newValue != null && - action.newValue.split(',').map((tag) => ( - + + {action.newValue != null && + action.newValue.split(',').map((tag) => ( {tag} - - ))} + ))} + ); From 367bece39622a04c9b51409751fbb9dd31dcddf1 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 23 Jul 2020 08:49:24 -0700 Subject: [PATCH 12/20] [kbn/test/failed_test_reporter] handle cypress junit better (#72968) Co-authored-by: spalger --- .../__fixtures__/cypress_report.xml | 50 +++++++++++++ .../__fixtures__/index.ts | 1 + .../add_messages_to_report.test.ts | 71 ++++++++++++++++++- .../add_messages_to_report.ts | 10 ++- .../run_failed_tests_reporter_cli.ts | 2 + .../src/failed_tests_reporter/test_report.ts | 2 +- 6 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml new file mode 100644 index 000000000000..ed0e154552ca --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/cypress_report.xml @@ -0,0 +1,50 @@ + + + + + + + + + ...` + +You can fix this problem by: + - Passing `{force: true}` which disables all error checking + - Passing `{waitForAnimations: false}` which disables waiting on animations + - Passing `{animationDistanceThreshold: 20}` which decreases the sensitivity + +https://on.cypress.io/element-is-animating + +Because this error occurred during a `after each` hook we are skipping the remaining tests in the current suite: `timeline flyout button` + at cypressErr (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:146621:16) + at cypressErrByPath (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:146630:10) + at Object.throwErrByPath (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:146593:11) + at Object.ensureElementIsNotAnimating (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:137560:24) + at ensureNotAnimating (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:127434:13) + at runAllChecks (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:127522:9) + at retryActionability (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:127542:16) + at tryCatcher (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:9065:23) + at Function.Promise.attempt.Promise.try (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:6339:29) + at tryFn (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:140680:21) + at whenStable (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:140715:12) + at http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:140259:16 + at tryCatcher (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:9065:23) + at Promise._settlePromiseFromHandler (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:7000:31) + at Promise._settlePromise (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:7057:18) + at Promise._settlePromise0 (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:7102:10)]]> + + + diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts index 02b6b5f06421..16ebe10ad542 100644 --- a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts +++ b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/index.ts @@ -23,3 +23,4 @@ export const FTR_REPORT = Fs.readFileSync(require.resolve('./ftr_report.xml'), ' export const JEST_REPORT = Fs.readFileSync(require.resolve('./jest_report.xml'), 'utf8'); export const KARMA_REPORT = Fs.readFileSync(require.resolve('./karma_report.xml'), 'utf8'); export const MOCHA_REPORT = Fs.readFileSync(require.resolve('./mocha_report.xml'), 'utf8'); +export const CYPRESS_REPORT = Fs.readFileSync(require.resolve('./cypress_report.xml'), 'utf8'); diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts index f8f279151e07..53a74f6cc6af 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts @@ -39,7 +39,13 @@ jest.mock('fs', () => { }; }); -import { FTR_REPORT, JEST_REPORT, MOCHA_REPORT, KARMA_REPORT } from './__fixtures__'; +import { + FTR_REPORT, + JEST_REPORT, + MOCHA_REPORT, + KARMA_REPORT, + CYPRESS_REPORT, +} from './__fixtures__'; import { parseTestReport } from './test_report'; import { addMessagesToReport } from './add_messages_to_report'; @@ -270,6 +276,69 @@ it('rewrites mocha reports with minimal changes', async () => { `); }); +it('rewrites cypress reports with minimal changes', async () => { + const xml = await addMessagesToReport({ + messages: [ + { + classname: '"after each" hook for "toggles open the timeline"', + name: 'timeline flyout button "after each" hook for "toggles open the timeline"', + message: 'Some extra content\n', + }, + ], + report: await parseTestReport(CYPRESS_REPORT), + log, + reportPath: Path.resolve(__dirname, './__fixtures__/cypress_report.xml'), + }); + + expect(createPatch('cypress.xml', CYPRESS_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(` + Index: cypress.xml + =================================================================== + --- cypress.xml [object Object] + +++ cypress.xml + @@ -1,25 +1,16 @@ + -‹?xml version="1.0" encoding="UTF-8"?› + +‹?xml version="1.0" encoding="utf-8"?› + ‹testsuites name="Mocha Tests" time="16.198" tests="2" failures="1"› + - ‹testsuite name="Root Suite" timestamp="2020-07-22T15:06:26" tests="0" file="cypress/integration/timeline_flyout_button.spec.ts" failures="0" time="0"› + - ‹/testsuite› + + ‹testsuite name="Root Suite" timestamp="2020-07-22T15:06:26" tests="0" file="cypress/integration/timeline_flyout_button.spec.ts" failures="0" time="0"/› + ‹testsuite name="timeline flyout button" timestamp="2020-07-22T15:06:26" tests="2" failures="1" time="16.198"› + - ‹testcase name="timeline flyout button toggles open the timeline" time="8.099" classname="toggles open the timeline"› + - ‹/testcase› + + ‹testcase name="timeline flyout button toggles open the timeline" time="8.099" classname="toggles open the timeline"/› + ‹testcase name="timeline flyout button "after each" hook for "toggles open the timeline"" time="8.099" classname=""after each" hook for "toggles open the timeline""› + - ‹failure message="Timed out retrying: \`cy.click()\` could not be issued because this element is currently animating: + + ‹failure message="Timed out retrying: \`cy.click()\` could not be issued because this element is currently animating: \`<button class="euiButtonEmpty euiButtonEmpty--text" type="button" data-test-subj="timeline-new"›...</button›\` You can fix this problem by: - Passing \`{force: true}\` which disables all error checking - Passing \`{waitForAnimations: false}\` which disables waiting on animations - Passing \`{animationDistanceThreshold: 20}\` which decreases the sensitivity https://on.cypress.io/element-is-animating Because this error occurred during a \`after each\` hook we are skipping the remaining tests in the current suite: \`timeline flyout button\`" type="CypressError"›‹![CDATA[Failed Tests Reporter: + + - Some extra content + + -\`<button class="euiButtonEmpty euiButtonEmpty--text" type="button" data-test-subj="timeline-new">...</button>\` + + -You can fix this problem by: + - - Passing \`{force: true}\` which disables all error checking + - - Passing \`{waitForAnimations: false}\` which disables waiting on animations + - - Passing \`{animationDistanceThreshold: 20}\` which decreases the sensitivity + +CypressError: Timed out retrying: \`cy.click()\` could not be issued because this element is currently animating: + + -https://on.cypress.io/element-is-animating + - + -Because this error occurred during a \`after each\` hook we are skipping the remaining tests in the current suite: \`timeline flyout button\`" type="CypressError"›‹![CDATA[CypressError: Timed out retrying: \`cy.click()\` could not be issued because this element is currently animating: + - + \`‹button class="euiButtonEmpty euiButtonEmpty--text" type="button" data-test-subj="timeline-new"›...‹/button›\` + + You can fix this problem by: + - Passing \`{force: true}\` which disables all error checking + @@ -46,5 +37,5 @@ + at Promise._settlePromise (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:7057:18) + at Promise._settlePromise0 (http://elastic:changeme@localhost:61141/__cypress/runner/cypress_runner.js:7102:10)]]›‹/failure› + ‹/testcase› + ‹/testsuite› + -‹/testsuites› + +‹/testsuites› + \\ No newline at end of file + + `); +}); + it('rewrites karma reports with minimal changes', async () => { const xml = await addMessagesToReport({ report: await parseTestReport(KARMA_REPORT), diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts index 6bc7556db8a4..27bf8a9c7549 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts @@ -59,10 +59,14 @@ export async function addMessagesToReport(options: { log.info(`${classname} - ${name}:${messageList}`); const output = `Failed Tests Reporter:${messageList}\n\n`; - if (!testCase['system-out']) { - testCase['system-out'] = [output]; + if (typeof testCase.failure[0] === 'object' && testCase.failure[0].$.message) { + // failure with "messages" ignore the system-out on jenkins + // so we instead extend the failure message + testCase.failure[0]._ = output + testCase.failure[0]._; + } else if (!testCase['system-out']) { + testCase['system-out'] = [{ _: output }]; } else if (typeof testCase['system-out'][0] === 'string') { - testCase['system-out'][0] = output + String(testCase['system-out'][0]); + testCase['system-out'][0] = { _: output + testCase['system-out'][0] }; } else { testCase['system-out'][0]._ = output + testCase['system-out'][0]._; } diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 8a951ac96919..3dfb1ea44d9e 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -72,6 +72,7 @@ export function runFailedTestsReporterCli() { } const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS; + log.info('Searching for reports at', patterns); const reportPaths = await globby(patterns, { absolute: true, }); @@ -80,6 +81,7 @@ export function runFailedTestsReporterCli() { throw createFailError(`Unable to find any junit reports with patterns [${patterns}]`); } + log.info('found', reportPaths.length, 'junit reports', reportPaths); const newlyCreatedIssues: Array<{ failure: TestFailure; newIssue: GithubIssueMini; diff --git a/packages/kbn-test/src/failed_tests_reporter/test_report.ts b/packages/kbn-test/src/failed_tests_reporter/test_report.ts index 43d84163462d..9907ca8b89ca 100644 --- a/packages/kbn-test/src/failed_tests_reporter/test_report.ts +++ b/packages/kbn-test/src/failed_tests_reporter/test_report.ts @@ -70,7 +70,7 @@ export interface TestCase { } export interface FailedTestCase extends TestCase { - failure: Array; + failure: Array; } /** From 0103cd342439bcde987c7761c1c538394001f30b Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 23 Jul 2020 12:21:31 -0400 Subject: [PATCH 13/20] [Fix] Lose OriginatingApp Connection on Save As (#72725) Reset originatingApp in lens and visualize when return to dashboard on saving is false --- .../application/components/visualize_editor.tsx | 1 + .../application/components/visualize_top_nav.tsx | 4 ++++ .../application/utils/get_top_nav_config.tsx | 5 +++++ .../apps/dashboard/edit_embeddable_redirects.js | 12 ++++++++++++ test/functional/page_objects/visualize_page.ts | 10 ++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 15 +++++++++++---- .../apps/dashboard_mode/dashboard_empty_screen.js | 10 ++++++++++ x-pack/test/functional/page_objects/lens_page.ts | 10 ++++++++++ 8 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index c571a5fb078b..516dcacfe581 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -89,6 +89,7 @@ export const VisualizeEditor = () => { isEmbeddableRendered={isEmbeddableRendered} hasUnappliedChanges={hasUnappliedChanges} originatingApp={originatingApp} + setOriginatingApp={setOriginatingApp} savedVisInstance={savedVisInstance} stateContainer={appState} visualizationIdFromUrl={visualizationIdFromUrl} diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 2e7dba46487a..f00c26f83e1e 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -40,6 +40,7 @@ interface VisualizeTopNavProps { setHasUnsavedChanges: (value: boolean) => void; hasUnappliedChanges: boolean; originatingApp?: string; + setOriginatingApp?: (originatingApp: string | undefined) => void; savedVisInstance: SavedVisInstance; stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; @@ -53,6 +54,7 @@ const TopNav = ({ setHasUnsavedChanges, hasUnappliedChanges, originatingApp, + setOriginatingApp, savedVisInstance, stateContainer, visualizationIdFromUrl, @@ -86,6 +88,7 @@ const TopNav = ({ hasUnappliedChanges, openInspector, originatingApp, + setOriginatingApp, savedVisInstance, stateContainer, visualizationIdFromUrl, @@ -100,6 +103,7 @@ const TopNav = ({ hasUnappliedChanges, openInspector, originatingApp, + setOriginatingApp, savedVisInstance, stateContainer, visualizationIdFromUrl, diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 96f64c6478fa..392168a53008 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -39,6 +39,7 @@ interface TopNavConfigParams { setHasUnsavedChanges: (value: boolean) => void; openInspector: () => void; originatingApp?: string; + setOriginatingApp?: (originatingApp: string | undefined) => void; hasUnappliedChanges: boolean; savedVisInstance: SavedVisInstance; stateContainer: VisualizeAppStateContainer; @@ -51,6 +52,7 @@ export const getTopNavConfig = ( setHasUnsavedChanges, openInspector, originatingApp, + setOriginatingApp, hasUnappliedChanges, savedVisInstance: { embeddableHandler, savedVis, vis }, stateContainer, @@ -112,6 +114,9 @@ export const getTopNavConfig = ( application.navigateToApp(originatingApp); } } else { + if (setOriginatingApp && originatingApp && savedVis.copyOnSave) { + setOriginatingApp(undefined); + } chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs(getEditBreadcrumbs(savedVis.lastSavedTitle)); diff --git a/test/functional/apps/dashboard/edit_embeddable_redirects.js b/test/functional/apps/dashboard/edit_embeddable_redirects.js index a366e34b121d..6d3d43890a96 100644 --- a/test/functional/apps/dashboard/edit_embeddable_redirects.js +++ b/test/functional/apps/dashboard/edit_embeddable_redirects.js @@ -75,5 +75,17 @@ export default function ({ getService, getPageObjects }) { const titles = await PageObjects.dashboard.getPanelTitles(); expect(titles.indexOf(newTitle)).to.not.be(-1); }); + + it('loses originatingApp connection after save as when redirectToOrigin is false', async () => { + const newTitle = 'wowee, my title just got cooler again'; + await PageObjects.header.waitUntilLoadingHasFinished(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, { + saveAsNew: true, + redirectToOrigin: false, + }); + await PageObjects.visualize.notLinkedToOriginatingApp(); + }); }); } diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index a08598fc42d6..92692767b096 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -352,6 +352,16 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.existOrFail('visualizesaveAndReturnButton'); await testSubjects.click('visualizesaveAndReturnButton'); } + + public async linkedToOriginatingApp() { + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('visualizesaveAndReturnButton'); + } + + public async notLinkedToOriginatingApp() { + await header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('visualizesaveAndReturnButton'); + } } return new VisualizePage(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 9b8b9a8531cf..082a3afcd513 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -44,6 +44,7 @@ interface State { isLoading: boolean; isSaveModalVisible: boolean; indexPatternsForTopNav: IndexPatternInstance[]; + originatingApp?: string; persistedDoc?: Document; lastKnownDoc?: Document; @@ -97,6 +98,7 @@ export function App({ fromDate: currentRange.from, toDate: currentRange.to, }, + originatingApp, filters: [], indicateNoData: false, }; @@ -321,9 +323,14 @@ export function App({ .then(({ id }) => { // Prevents unnecessary network request and disables save button const newDoc = { ...doc, id }; + const currentOriginatingApp = state.originatingApp; setState((s) => ({ ...s, isSaveModalVisible: false, + originatingApp: + saveProps.newCopyOnSave && !saveProps.returnToOrigin + ? undefined + : currentOriginatingApp, persistedDoc: newDoc, lastKnownDoc: newDoc, })); @@ -368,7 +375,7 @@ export function App({
{ if (isSaveable && lastKnownDoc) { setState((s) => ({ ...s, isSaveModalVisible: true })); @@ -523,7 +530,7 @@ export function App({
{lastKnownDoc && state.isSaveModalVisible && ( runSave(props)} onClose={() => setState((s) => ({ ...s, isSaveModalVisible: false }))} documentInfo={{ diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index c8a8f9653c11..62e07a08d176 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -98,5 +98,15 @@ export default function ({ getPageObjects, getService }) { const titles = await PageObjects.dashboard.getPanelTitles(); expect(titles.indexOf(newTitle)).to.not.be(-1); }); + + it('loses originatingApp connection after save as when redirectToOrigin is false', async () => { + const newTitle = 'wowee, my title just got cooler again'; + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.save(newTitle, true, false); + await PageObjects.lens.notLinkedToOriginatingApp(); + await PageObjects.common.navigateToApp('dashboard'); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index d101c9754d56..79548db0e263 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -195,5 +195,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont async createLayer() { await testSubjects.click('lnsLayerAddButton'); }, + + async linkedToOriginatingApp() { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('lnsApp_saveAndReturnButton'); + }, + + async notLinkedToOriginatingApp() { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('lnsApp_saveAndReturnButton'); + }, }); } From 6d480c7f228e570b36254e8728cce02d25b494d7 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 23 Jul 2020 11:38:29 -0500 Subject: [PATCH 14/20] [ML] Add API integration tests for /filters and /calendars (#72564) Co-authored-by: Elastic Machine --- .../apis/ml/calendars/create_calendars.ts | 84 ++++++++++ .../apis/ml/calendars/delete_calendars.ts | 87 +++++++++++ .../apis/ml/calendars/get_calendars.ts | 145 ++++++++++++++++++ .../apis/ml/calendars/helpers.ts | 31 ++++ .../apis/ml/calendars/index.ts | 16 ++ .../apis/ml/calendars/update_calendars.ts | 104 +++++++++++++ .../apis/ml/filters/create_filters.ts | 127 +++++++++++++++ .../apis/ml/filters/delete_filters.ts | 95 ++++++++++++ .../apis/ml/filters/get_filters.ts | 98 ++++++++++++ .../api_integration/apis/ml/filters/index.ts | 16 ++ .../apis/ml/filters/update_filters.ts | 118 ++++++++++++++ x-pack/test/api_integration/apis/ml/index.ts | 2 + x-pack/test/functional/services/ml/api.ts | 139 +++++++++++++++-- 13 files changed, 1052 insertions(+), 10 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/helpers.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/create_filters.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/delete_filters.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/get_filters.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/filters/update_filters.ts diff --git a/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts new file mode 100644 index 000000000000..f163df0109ff --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/create_calendars.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('create_calendars', function () { + const calendarId = `test_create_calendar`; + + const requestBody = { + calendarId, + job_ids: ['test_job_1', 'test_job_2'], + description: 'Test calendar', + events: [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ], + }; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should successfully create calendar by id', async () => { + await supertest + .put(`/api/ml/calendars`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + const results = await ml.api.getCalendar(requestBody.calendarId); + const createdCalendar = results.body.calendars[0]; + + expect(createdCalendar.calendar_id).to.eql(requestBody.calendarId); + expect(createdCalendar.description).to.eql(requestBody.description); + expect(createdCalendar.job_ids).to.eql(requestBody.job_ids); + + await ml.api.waitForEventsToExistInCalendar(calendarId, requestBody.events); + }); + + it('should not create new calendar for user without required permission', async () => { + const { body } = await supertest + .put(`/api/ml/calendars`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForCalendarNotToExist(calendarId); + }); + + it('should not create new calendar for unauthorized user', async () => { + const { body } = await supertest + .put(`/api/ml/calendars`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForCalendarNotToExist(calendarId); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.ts new file mode 100644 index 000000000000..5c5d5a3c432f --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/delete_calendars.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('delete_calendars', function () { + const calendarId = `test_delete_cal`; + const testCalendar = { + calendar_id: calendarId, + job_ids: ['test_job_1', 'test_job_2'], + description: `Test calendar`, + }; + const testEvents = [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ]; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + beforeEach(async () => { + await ml.api.createCalendar(calendarId, testCalendar); + await ml.api.createCalendarEvents(calendarId, testEvents); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should delete calendar by id', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.acknowledged).to.eql(true); + await ml.api.waitForCalendarNotToExist(calendarId); + }); + + it('should not delete calendar for user without required permission', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForCalendarToExist(calendarId); + }); + + it('should not delete calendar for unauthorized user', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForCalendarToExist(calendarId); + }); + + it('should return 404 if invalid calendarId', async () => { + const { body } = await supertest + .delete(`/api/ml/calendars/calendar_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts new file mode 100644 index 000000000000..e115986b2f09 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/get_calendars.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_calendars', function () { + const testEvents = [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ]; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + describe('get multiple calendars', function () { + const testCalendars = [1, 2, 3].map((num) => ({ + calendar_id: `test_get_cal_${num}`, + job_ids: ['test_job_1', 'test_job_2'], + description: `Test calendar ${num}`, + })); + + beforeEach(async () => { + for (const testCalendar of testCalendars) { + await ml.api.createCalendar(testCalendar.calendar_id, testCalendar); + await ml.api.createCalendarEvents(testCalendar.calendar_id, testEvents); + } + }); + + afterEach(async () => { + for (const testCalendar of testCalendars) { + await ml.api.deleteCalendar(testCalendar.calendar_id); + } + }); + + it('should fetch all calendars', async () => { + const { body } = await supertest + .get(`/api/ml/calendars`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body).to.have.length(testCalendars.length); + expect(body[0].events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body[0]); + }); + + it('should fetch all calendars for user with view permission', async () => { + const { body } = await supertest + .get(`/api/ml/calendars`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body).to.have.length(testCalendars.length); + expect(body[0].events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body[0]); + }); + + it('should not fetch calendars for unauthorized user', async () => { + const { body } = await supertest + .get(`/api/ml/calendars`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + }); + }); + + describe('get calendar by id', function () { + const calendarId = `test_get_cal`; + const testCalendar = { + calendar_id: calendarId, + job_ids: ['test_job_1', 'test_job_2'], + description: `Test calendar`, + }; + + beforeEach(async () => { + await ml.api.createCalendar(calendarId, testCalendar); + await ml.api.createCalendarEvents(calendarId, testEvents); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should fetch calendar & associated events by id', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.job_ids).to.eql(testCalendar.job_ids); + expect(body.description).to.eql(testCalendar.description); + expect(body.events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body); + }); + + it('should fetch calendar & associated events by id for user with view permission', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.job_ids).to.eql(testCalendar.job_ids); + expect(body.description).to.eql(testCalendar.description); + expect(body.events).to.have.length(testEvents.length); + ml.api.assertAllEventsExistInCalendar(testEvents, body); + }); + + it('should not fetch calendars for unauthorized user', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + }); + }); + + it('should return 404 if invalid calendar id', async () => { + const { body } = await supertest + .get(`/api/ml/calendars/calendar_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/helpers.ts b/x-pack/test/api_integration/apis/ml/calendars/helpers.ts new file mode 100644 index 000000000000..5d143d9b451f --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/helpers.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Calendar, CalendarEvent } from '../../../../../plugins/ml/server/models/calendar'; + +export const assertAllEventsExistInCalendar = ( + eventsToCheck: CalendarEvent[], + calendar: Calendar +): boolean => { + const updatedCalendarEvents = calendar.events as CalendarEvent[]; + let allEventsAreUpdated = true; + for (const eventToCheck of eventsToCheck) { + // if at least one of the events that we need to check is not in the updated events + // no need to continue + if ( + updatedCalendarEvents.findIndex( + (updatedEvent) => + updatedEvent.description === eventToCheck.description && + updatedEvent.start_time === eventToCheck.start_time && + updatedEvent.end_time === eventToCheck.end_time + ) < 0 + ) { + allEventsAreUpdated = false; + break; + } + } + return allEventsAreUpdated; +}; diff --git a/x-pack/test/api_integration/apis/ml/calendars/index.ts b/x-pack/test/api_integration/apis/ml/calendars/index.ts new file mode 100644 index 000000000000..e7d824205e6c --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('calendars', function () { + loadTestFile(require.resolve('./create_calendars')); + loadTestFile(require.resolve('./get_calendars')); + loadTestFile(require.resolve('./delete_calendars')); + loadTestFile(require.resolve('./update_calendars')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts b/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts new file mode 100644 index 000000000000..5194370b19e6 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/calendars/update_calendars.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('update_calendars', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + const calendarId = `test_update_cal`; + const originalCalendar = { + calendar_id: calendarId, + job_ids: ['test_job_1'], + description: `Test calendar`, + }; + const originalEvents = [ + { description: 'event 1', start_time: 1513641600000, end_time: 1513728000000 }, + ]; + + const updateCalendarRequestBody = { + calendarId, + job_ids: ['test_updated_job_1', 'test_updated_job_2'], + description: 'Updated calendar #1', + events: [ + { description: 'updated event 2', start_time: 1513814400000, end_time: 1513900800000 }, + { description: 'updated event 3', start_time: 1514160000000, end_time: 1514246400000 }, + ], + }; + + beforeEach(async () => { + await ml.api.createCalendar(calendarId, originalCalendar); + await ml.api.createCalendarEvents(calendarId, originalEvents); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('should update calendar by id with new settings', async () => { + await supertest + .put(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(200); + + await ml.api.waitForCalendarToExist(calendarId); + + const getCalendarResult = await ml.api.getCalendar(calendarId); + const getEventsResult = await ml.api.getCalendarEvents(calendarId); + + const updatedCalendar = getCalendarResult.body.calendars[0]; + const updatedEvents = getEventsResult.body.events; + + expect(updatedCalendar.calendar_id).to.eql(updateCalendarRequestBody.calendarId); + expect(updatedCalendar.job_ids).to.have.length(updateCalendarRequestBody.job_ids.length); + expect(updatedEvents).to.have.length(updateCalendarRequestBody.events.length); + await ml.api.waitForEventsToExistInCalendar( + updatedCalendar.calendar_id, + updateCalendarRequestBody.events + ); + }); + + it('should not allow to update calendar for user without required permission ', async () => { + await supertest + .put(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(404); + }); + + it('should not allow to update calendar for unauthorized user', async () => { + await supertest + .put(`/api/ml/calendars/${calendarId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(404); + }); + + it('should return error if invalid calendarId ', async () => { + await supertest + .put(`/api/ml/calendars/calendar_id_dne`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateCalendarRequestBody) + .expect(404); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts new file mode 100644 index 000000000000..c175d3a9a3d9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'should successfully create new filter', + user: USER.ML_POWERUSER, + requestBody: { filterId: 'safe_ip_addresses', description: '', items: ['104.236.210.185'] }, + expected: { + responseCode: 200, + responseBody: { + filter_id: 'safe_ip_addresses', + description: '', + items: ['104.236.210.185'], + }, + }, + }, + { + testTitle: 'should not create new filter for user without required permission', + user: USER.ML_VIEWER, + requestBody: { + filterId: 'safe_ip_addresses_view_only', + + description: '', + items: ['104.236.210.185'], + }, + expected: { + responseCode: 404, + responseBody: { + error: 'Not Found', + message: 'Not Found', + }, + }, + }, + { + testTitle: 'should not create new filter for unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: { + filterId: 'safe_ip_addresses_unauthorized', + description: '', + items: ['104.236.210.185'], + }, + expected: { + responseCode: 404, + responseBody: { + error: 'Not Found', + message: 'Not Found', + }, + }, + }, + { + testTitle: 'should return 400 bad request if invalid filterId', + user: USER.ML_POWERUSER, + requestBody: { + filterId: '@invalid_filter_id', + description: '', + items: ['104.236.210.185'], + }, + expected: { + responseCode: 400, + responseBody: { + error: 'Bad Request', + message: 'Invalid filter_id', + }, + }, + }, + { + testTitle: 'should return 400 bad request if invalid items', + user: USER.ML_POWERUSER, + requestBody: { filterId: 'valid_filter', description: '' }, + expected: { + responseCode: 400, + responseBody: { + error: 'Bad Request', + message: 'expected value of type [array] but got [undefined]', + }, + }, + }, + ]; + + describe('create_filters', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + for (const testData of testDataList) { + const { filterId } = testData.requestBody; + await ml.api.deleteFilter(filterId); + } + }); + + for (const testData of testDataList) { + const { testTitle, user, requestBody, expected } = testData; + it(`${testTitle}`, async () => { + const { body } = await supertest + .put(`/api/ml/filters`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(expected.responseCode); + if (body.error === undefined) { + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body).to.eql(expectedResponse); + } else { + expect(body.error).to.contain(expected.responseBody.error); + expect(body.message).to.contain(expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/delete_filters.ts b/x-pack/test/api_integration/apis/ml/filters/delete_filters.ts new file mode 100644 index 000000000000..bb83a7f72069 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/delete_filters.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const items = ['104.236.210.185']; + const validFilters = [ + { + filterId: 'filter_power', + requestBody: { description: 'Test delete filter #1', items }, + }, + { + filterId: 'filter_viewer', + requestBody: { description: 'Test delete filter (viewer)', items }, + }, + { + filterId: 'filter_unauthorized', + requestBody: { description: 'Test delete filter (unauthorized)', items }, + }, + ]; + + describe('delete_filters', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + for (const filter of validFilters) { + const { filterId, requestBody } = filter; + await ml.api.createFilter(filterId, requestBody); + } + }); + + after(async () => { + for (const filter of validFilters) { + const { filterId } = filter; + await ml.api.deleteFilter(filterId); + } + }); + + it(`should delete filter by id`, async () => { + const { filterId } = validFilters[0]; + const { body } = await supertest + .delete(`/api/ml/filters/${filterId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.acknowledged).to.eql(true); + await ml.api.waitForFilterToNotExist(filterId); + }); + + it(`should not delete filter for user without required permission`, async () => { + const { filterId } = validFilters[1]; + const { body } = await supertest + .delete(`/api/ml/filters/${filterId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForFilterToExist(filterId); + }); + + it(`should not delete filter for unauthorized user`, async () => { + const { filterId } = validFilters[2]; + const { body } = await supertest + .delete(`/api/ml/filters/${filterId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + await ml.api.waitForFilterToExist(filterId); + }); + + it(`should not allow user to delete filter if invalid filterId`, async () => { + const { body } = await supertest + .delete(`/api/ml/filters/filter_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts new file mode 100644 index 000000000000..3dd6093a9917 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const validFilters = [ + { + filterId: 'filter_1', + requestBody: { description: 'Valid filter #1', items: ['104.236.210.185'] }, + }, + { + filterId: 'filter_2', + requestBody: { description: 'Valid filter #2', items: ['104.236.210.185'] }, + }, + ]; + + describe('get_filters', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + for (const filter of validFilters) { + const { filterId, requestBody } = filter; + await ml.api.createFilter(filterId, requestBody); + } + }); + + after(async () => { + for (const filter of validFilters) { + const { filterId } = filter; + await ml.api.deleteFilter(filterId); + } + }); + it(`should fetch all filters`, async () => { + const { body } = await supertest + .get(`/api/ml/filters`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body).to.have.length(validFilters.length); + }); + + it(`should not allow to retrieve filters for user without required permission`, async () => { + const { body } = await supertest + .get(`/api/ml/filters`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + + it(`should not allow to retrieve filters for unauthorized user`, async () => { + const { body } = await supertest + .get(`/api/ml/filters`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + + it(`should fetch single filter by id`, async () => { + const { filterId, requestBody } = validFilters[0]; + const { body } = await supertest + .get(`/api/ml/filters/${filterId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.filter_id).to.eql(filterId); + expect(body.description).to.eql(requestBody.description); + expect(body.items).to.eql(requestBody.items); + }); + + it(`should return 400 if filterId does not exist`, async () => { + const { body } = await supertest + .get(`/api/ml/filters/filter_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(400); + expect(body.error).to.eql('Bad Request'); + expect(body.message).to.contain('Unable to find filter'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/filters/index.ts b/x-pack/test/api_integration/apis/ml/filters/index.ts new file mode 100644 index 000000000000..0c0bc4eab29e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('filters', function () { + loadTestFile(require.resolve('./create_filters')); + loadTestFile(require.resolve('./get_filters')); + loadTestFile(require.resolve('./delete_filters')); + loadTestFile(require.resolve('./update_filters')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts new file mode 100644 index 000000000000..eb58d545093c --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const items = ['104.236.210.185']; + const validFilters = [ + { + filterId: 'filter_power', + requestBody: { description: 'Test update filter #1', items }, + }, + { + filterId: 'filter_viewer', + requestBody: { description: 'Test update filter (viewer)', items }, + }, + { + filterId: 'filter_unauthorized', + requestBody: { description: 'Test update filter (unauthorized)', items }, + }, + ]; + + describe('update_filters', function () { + const updateFilterRequestBody = { + description: 'Updated filter #1', + removeItems: items, + addItems: ['my_new_items_1', 'my_new_items_2'], + }; + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + for (const filter of validFilters) { + const { filterId, requestBody } = filter; + await ml.api.createFilter(filterId, requestBody); + } + }); + + after(async () => { + for (const filter of validFilters) { + const { filterId } = filter; + await ml.api.deleteFilter(filterId); + } + }); + + it(`should update filter by id`, async () => { + const { filterId } = validFilters[0]; + const { body } = await supertest + .put(`/api/ml/filters/${filterId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(200); + + expect(body.filter_id).to.eql(filterId); + expect(body.description).to.eql(updateFilterRequestBody.description); + expect(body.items).to.eql(updateFilterRequestBody.addItems); + }); + + it(`should not allow to update filter for user without required permission`, async () => { + const { filterId, requestBody: oldFilterRequest } = validFilters[1]; + const { body } = await supertest + .put(`/api/ml/filters/${filterId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(404); + + // response should return not found + expect(body.error).to.eql('Not Found'); + + // and the filter should not be updated + const response = await ml.api.getFilter(filterId); + const updatedFilter = response.body.filters[0]; + expect(updatedFilter.filter_id).to.eql(filterId); + expect(updatedFilter.description).to.eql(oldFilterRequest.description); + expect(updatedFilter.items).to.eql(oldFilterRequest.items); + }); + + it(`should not allow to update filter for unauthorized user`, async () => { + const { filterId, requestBody: oldFilterRequest } = validFilters[2]; + const { body } = await supertest + .put(`/api/ml/filters/${filterId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + + const response = await ml.api.getFilter(filterId); + const updatedFilter = response.body.filters[0]; + expect(updatedFilter.filter_id).to.eql(filterId); + expect(updatedFilter.description).to.eql(oldFilterRequest.description); + expect(updatedFilter.items).to.eql(oldFilterRequest.items); + }); + + it(`should return appropriate error if invalid filterId`, async () => { + const { body } = await supertest + .put(`/api/ml/filters/filter_id_dne`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(updateFilterRequestBody) + .expect(400); + + expect(body.message).to.contain('No filter with id'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 5c2e7a6c4b2f..b29bc47b5039 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -58,5 +58,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./jobs')); loadTestFile(require.resolve('./results')); loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./filters')); + loadTestFile(require.resolve('./calendars')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index a48159cd7515..9dfec3a17dec 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -5,14 +5,12 @@ */ import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test/types/ftr'; +import { Calendar, CalendarEvent } from '../../../../plugins/ml/server/models/calendar/index'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; - import { FtrProviderContext } from '../../ftr_provider_context'; - import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; - export type MlApi = ProvidedType; export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { @@ -325,19 +323,102 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, - async getCalendar(calendarId: string) { - return await esSupertest.get(`/_ml/calendars/${calendarId}`).expect(200); + async getCalendar(calendarId: string, expectedCode = 200) { + return await esSupertest.get(`/_ml/calendars/${calendarId}`).expect(expectedCode); }, - async createCalendar(calendarId: string, body = { description: '', job_ids: [] }) { + async createCalendar( + calendarId: string, + requestBody: Partial = { description: '', job_ids: [] } + ) { log.debug(`Creating calendar with id '${calendarId}'...`); - await esSupertest.put(`/_ml/calendars/${calendarId}`).send(body).expect(200); + await esSupertest.put(`/_ml/calendars/${calendarId}`).send(requestBody).expect(200); + await this.waitForCalendarToExist(calendarId); + }, + + async deleteCalendar(calendarId: string) { + log.debug(`Deleting calendar with id '${calendarId}'...`); + await esSupertest.delete(`/_ml/calendars/${calendarId}`); + + await this.waitForCalendarNotToExist(calendarId); + }, + + async waitForCalendarToExist(calendarId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${calendarId}' to exist`, 5 * 1000, async () => { + if (await this.getCalendar(calendarId, 200)) { + return true; + } else { + throw new Error(errorMsg || `expected calendar '${calendarId}' to exist`); + } + }); + }, - await retry.waitForWithTimeout(`'${calendarId}' to be created`, 30 * 1000, async () => { - if (await this.getCalendar(calendarId)) { + async waitForCalendarNotToExist(calendarId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${calendarId}' to not exist`, 5 * 1000, async () => { + if (await this.getCalendar(calendarId, 404)) { return true; } else { - throw new Error(`expected calendar '${calendarId}' to be created`); + throw new Error(errorMsg || `expected calendar '${calendarId}' to not exist`); + } + }); + }, + + async createCalendarEvents(calendarId: string, events: CalendarEvent[]) { + log.debug(`Creating events for calendar with id '${calendarId}'...`); + await esSupertest.post(`/_ml/calendars/${calendarId}/events`).send({ events }).expect(200); + await this.waitForEventsToExistInCalendar(calendarId, events); + }, + + async getCalendarEvents(calendarId: string, expectedCode = 200) { + return await esSupertest.get(`/_ml/calendars/${calendarId}/events`).expect(expectedCode); + }, + + assertAllEventsExistInCalendar: ( + eventsToCheck: CalendarEvent[], + calendar: Calendar + ): boolean => { + const updatedCalendarEvents = calendar.events as CalendarEvent[]; + let allEventsAreUpdated = true; + for (const eventToCheck of eventsToCheck) { + // if at least one of the events that we need to check is not in the updated events + // no need to continue + if ( + updatedCalendarEvents.findIndex( + (updatedEvent) => + updatedEvent.description === eventToCheck.description && + updatedEvent.start_time === eventToCheck.start_time && + updatedEvent.end_time === eventToCheck.end_time + ) < 0 + ) { + allEventsAreUpdated = false; + break; + } + } + expect(allEventsAreUpdated).to.eql( + true, + `Expected calendar ${calendar.calendar_id} to contain events ${JSON.stringify( + eventsToCheck + )}` + ); + return true; + }, + + async waitForEventsToExistInCalendar( + calendarId: string, + eventsToCheck: CalendarEvent[], + errorMsg?: string + ) { + await retry.waitForWithTimeout(`'${calendarId}' events to exist`, 5 * 1000, async () => { + // validate if calendar events have been updated with the requested events + const { body } = await this.getCalendarEvents(calendarId, 200); + + if (this.assertAllEventsExistInCalendar(eventsToCheck, body)) { + return true; + } else { + throw new Error( + errorMsg || + `expected events for calendar '${calendarId}' to have been updated correctly` + ); } }); }, @@ -515,5 +596,43 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } ); }, + + async getFilter(filterId: string, expectedCode = 200) { + return await esSupertest.get(`/_ml/filters/${filterId}`).expect(expectedCode); + }, + + async createFilter(filterId: string, requestBody: object) { + log.debug(`Creating filter with id '${filterId}'...`); + await esSupertest.put(`/_ml/filters/${filterId}`).send(requestBody).expect(200); + + await this.waitForFilterToExist(filterId, `expected filter '${filterId}' to be created`); + }, + + async deleteFilter(filterId: string) { + log.debug(`Deleting filter with id '${filterId}'...`); + await esSupertest.delete(`/_ml/filters/${filterId}`); + + await this.waitForFilterToNotExist(filterId, `expected filter '${filterId}' to be deleted`); + }, + + async waitForFilterToExist(filterId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${filterId}' to exist`, 5 * 1000, async () => { + if (await this.getFilter(filterId, 200)) { + return true; + } else { + throw new Error(errorMsg || `expected filter '${filterId}' to exist`); + } + }); + }, + + async waitForFilterToNotExist(filterId: string, errorMsg?: string) { + await retry.waitForWithTimeout(`'${filterId}' to not exist`, 5 * 1000, async () => { + if (await this.getFilter(filterId, 404)) { + return true; + } else { + throw new Error(errorMsg || `expected filter '${filterId}' to not exist`); + } + }); + }, }; } From 2932b169a27b45eadfc03aaa9bd5a57cab2d3750 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 23 Jul 2020 11:45:38 -0500 Subject: [PATCH 15/20] [Ingest Manager] Integration test install/uninstall a package (#72957) * integration test for initial installation of package * add all services to integration config * rename files, test removing package * remove import from merge conflict * rename es_assets to all_assets package * move install package to before clause and update test descriptions * fix typo * update ilm policy name * use skipIfNoDockerRegistry helper --- .../epm/{install.ts => install_overrides.ts} | 0 .../apis/epm/install_remove_assets.ts | 197 ++++++++++++++++++ .../apis/epm/list.ts | 2 +- .../elasticsearch/ilm_policy/all_assets.json | 15 ++ .../elasticsearch/ingest_pipeline/default.yml | 7 + .../0.1.0/dataset/test_logs/fields/fields.yml | 16 ++ .../0.1.0/dataset/test_logs/manifest.yml | 9 + .../dataset/test_metrics/fields/fields.yml | 16 ++ .../0.1.0/dataset/test_metrics/manifest.yml | 3 + .../all_assets/0.1.0/docs/README.md | 3 + .../0.1.0/img/logo_overrides_64_color.svg | 7 + .../kibana/dashboard/sample_dashboard.json | 16 ++ .../kibana/dashboard/sample_dashboard2.json | 16 ++ .../0.1.0/kibana/search/sample_search.json | 24 +++ .../visualization/sample_visualization.json | 11 + .../all_assets/0.1.0/manifest.yml | 20 ++ .../apis/index.js | 3 +- .../ingest_manager_api_integration/config.ts | 5 +- .../ingest_manager_api_integration/helpers.ts | 2 +- 19 files changed, 365 insertions(+), 7 deletions(-) rename x-pack/test/ingest_manager_api_integration/apis/epm/{install.ts => install_overrides.ts} (100%) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/search/sample_search.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/manifest.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_overrides.ts similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/epm/install.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/install_overrides.ts diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts new file mode 100644 index 000000000000..9ca8ebf13607 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + const es = getService('es'); + const pkgName = 'all_assets'; + const pkgVersion = '0.1.0'; + const pkgKey = `${pkgName}-${pkgVersion}`; + const logsTemplateName = `logs-${pkgName}.test_logs`; + const metricsTemplateName = `metrics-${pkgName}.test_metrics`; + + const uninstallPackage = async (pkg: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + const installPackage = async (pkg: string) => { + await supertest.post(`/api/ingest_manager/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('installs and uninstalls all assets', async () => { + describe('installs all assets when installing a package for the first time', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await installPackage(pkgKey); + }); + it('should have installed the ILM policy', async function () { + const resPolicy = await es.transport.request({ + method: 'GET', + path: `/_ilm/policy/all_assets`, + }); + expect(resPolicy.statusCode).equal(200); + }); + it('should have installed the index templates', async function () { + const resLogsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${logsTemplateName}`, + }); + expect(resLogsTemplate.statusCode).equal(200); + + const resMetricsTemplate = await es.transport.request({ + method: 'GET', + path: `/_index_template/${metricsTemplateName}`, + }); + expect(resMetricsTemplate.statusCode).equal(200); + }); + it('should have installed the pipelines', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, + }); + expect(res.statusCode).equal(200); + }); + it('should have installed the template components', async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}-mappings`, + }); + expect(res.statusCode).equal(200); + const resSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}-settings`, + }); + expect(resSettings.statusCode).equal(200); + }); + it('should have installed the kibana assets', async function () { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + expect(resIndexPatternLogs.id).equal('logs-*'); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + expect(resIndexPatternMetrics.id).equal('metrics-*'); + const resIndexPatternEvents = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'events-*', + }); + expect(resIndexPatternEvents.id).equal('events-*'); + const resDashboard = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard', + }); + expect(resDashboard.id).equal('sample_dashboard'); + const resDashboard2 = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard2', + }); + expect(resDashboard2.id).equal('sample_dashboard2'); + const resVis = await kibanaServer.savedObjects.get({ + type: 'visualization', + id: 'sample_visualization', + }); + expect(resVis.id).equal('sample_visualization'); + const resSearch = await kibanaServer.savedObjects.get({ + type: 'search', + id: 'sample_search', + }); + expect(resSearch.id).equal('sample_search'); + }); + }); + + describe('uninstalls all assets when uninstalling a package', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await uninstallPackage(pkgKey); + }); + it('should have uninstalled the index templates', async function () { + const resLogsTemplate = await es.transport.request( + { + method: 'GET', + path: `/_index_template/${logsTemplateName}`, + }, + { + ignore: [404], + } + ); + expect(resLogsTemplate.statusCode).equal(404); + + const resMetricsTemplate = await es.transport.request( + { + method: 'GET', + path: `/_index_template/${metricsTemplateName}`, + }, + { + ignore: [404], + } + ); + expect(resMetricsTemplate.statusCode).equal(404); + }); + it('should have uninstalled the pipelines', async function () { + const res = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, + }, + { + ignore: [404], + } + ); + expect(res.statusCode).equal(404); + }); + it('should have uninstalled the kibana assets', async function () { + let resDashboard; + try { + resDashboard = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard', + }); + } catch (err) { + resDashboard = err; + } + expect(resDashboard.response.data.statusCode).equal(404); + let resDashboard2; + try { + resDashboard2 = await kibanaServer.savedObjects.get({ + type: 'dashboard', + id: 'sample_dashboard2', + }); + } catch (err) { + resDashboard2 = err; + } + expect(resDashboard2.response.data.statusCode).equal(404); + let resVis; + try { + resVis = await kibanaServer.savedObjects.get({ + type: 'visualization', + id: 'sample_visualization', + }); + } catch (err) { + resVis = err; + } + expect(resVis.response.data.statusCode).equal(404); + let resSearch; + try { + resVis = await kibanaServer.savedObjects.get({ + type: 'search', + id: 'sample_search', + }); + } catch (err) { + resSearch = err; + } + expect(resSearch.response.data.statusCode).equal(404); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index 74aaf48d1567..2fbda8f2d3c8 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(5); + expect(listResponse.response.length).to.be(6); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json new file mode 100644 index 000000000000..7cf62e890f86 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ilm_policy/all_assets.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb", + "max_age": "30d" + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 000000000000..580db049d0d5 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,7 @@ +--- +description: Pipeline for parsing test logs + plugins. +processors: +- set: + field: error.message + value: '{{ _ingest.on_failure_message }}' \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/fields.yml new file mode 100644 index 000000000000..12a9a03c1337 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/manifest.yml new file mode 100644 index 000000000000..8cd522e2845b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/fields.yml new file mode 100644 index 000000000000..12a9a03c1337 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/manifest.yml new file mode 100644 index 000000000000..6bc20442bd43 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/docs/README.md new file mode 100644 index 000000000000..2617f1fcabe1 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing that a package installs its elasticsearch assets when installed for the first time (not updating) and removing the package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 000000000000..b03007a76ffc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json new file mode 100644 index 000000000000..ef08d6932421 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "description": "Sample dashboard", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlightAll\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false}", + "panelsJSON": "[{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"panelRefName\":\"panel_0\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"kafka.log.class\",\"kafka.log.trace.class\",\"kafka.log.trace.full\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":12,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"panelRefName\":\"panel_1\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"log.level\",\"kafka.log.component\",\"message\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":20,\"i\":\"3\",\"w\":48,\"x\":0,\"y\":20},\"panelIndex\":\"3\",\"panelRefName\":\"panel_2\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"4\",\"w\":48,\"x\":0,\"y\":12},\"panelIndex\":\"4\",\"panelRefName\":\"panel_3\",\"version\":\"7.3.0\"}]", + "timeRestore": false, + "title": "[Logs Sample] Overview ECS", + "version": 1 + }, + "id": "sample_dashboard", + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json new file mode 100644 index 000000000000..7ea63c5d444b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "description": "Sample dashboard 2", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlightAll\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false}", + "panelsJSON": "[{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"panelRefName\":\"panel_0\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"kafka.log.class\",\"kafka.log.trace.class\",\"kafka.log.trace.full\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":12,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"panelRefName\":\"panel_1\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"log.level\",\"kafka.log.component\",\"message\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":20,\"i\":\"3\",\"w\":48,\"x\":0,\"y\":20},\"panelIndex\":\"3\",\"panelRefName\":\"panel_2\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"4\",\"w\":48,\"x\":0,\"y\":12},\"panelIndex\":\"4\",\"panelRefName\":\"panel_3\",\"version\":\"7.3.0\"}]", + "timeRestore": false, + "title": "[Logs Sample2] Overview ECS", + "version": 1 + }, + "id": "sample_dashboard2", + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/search/sample_search.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/search/sample_search.json new file mode 100644 index 000000000000..28185affabef --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/search/sample_search.json @@ -0,0 +1,24 @@ +{ + "attributes": { + "columns": [ + "log.level", + "kafka.log.component", + "message" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\",\"key\":\"dataset.name\",\"negate\":false,\"params\":{\"query\":\"kafka.log\",\"type\":\"phrase\"},\"type\":\"phrase\",\"value\":\"log\"},\"query\":{\"match\":{\"dataset.name\":{\"query\":\"kafka.log\",\"type\":\"phrase\"}}}}],\"highlightAll\":true,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "All logs [Logs Kafka] ECS", + "version": 1 + }, + "id": "sample_search", + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json new file mode 100644 index 000000000000..e814b83bbf32 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/visualization/sample_visualization.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "description": "sample visualization", + "title": "sample vis title", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "sample_visualization", + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/manifest.yml new file mode 100644 index 000000000000..3c11b5103fbe --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: all_assets +title: All Assets Installed/Uninstalled Test +description: This is a test package for testing that all assets were installed when installing a package for the first time and removing the assets during package uninstall +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index c0c8ce3ff082..1045ff5d82d1 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -16,7 +16,8 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./epm/file')); //loadTestFile(require.resolve('./epm/template')); loadTestFile(require.resolve('./epm/ilm')); - loadTestFile(require.resolve('./epm/install')); + loadTestFile(require.resolve('./epm/install_overrides')); + loadTestFile(require.resolve('./epm/install_remove_assets')); // Package configs loadTestFile(require.resolve('./package_config/create')); diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index 6f5d8eed4351..2aa2e62a4b9e 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -8,7 +8,6 @@ import path from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { defineDockerServersConfig } from '@kbn/test'; -import { services } from '../api_integration/services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); @@ -49,9 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }), esArchiver: xPackAPITestsConfig.get('esArchiver'), services: { - ...services, - supertest: xPackAPITestsConfig.get('services.supertest'), - es: xPackAPITestsConfig.get('services.es'), + ...xPackAPITestsConfig.get('services'), }, junit: { reportName: 'X-Pack EPM API Integration Tests', diff --git a/x-pack/test/ingest_manager_api_integration/helpers.ts b/x-pack/test/ingest_manager_api_integration/helpers.ts index b1755e30f61f..a5ffc4e7adc2 100644 --- a/x-pack/test/ingest_manager_api_integration/helpers.ts +++ b/x-pack/test/ingest_manager_api_integration/helpers.ts @@ -22,7 +22,7 @@ export function skipIfNoDockerRegistry(providerContext: FtrProviderContext) { const server = dockerServers.get('registry'); const log = getService('log'); - beforeEach(function beforeSetupWithDockerRegistyry() { + beforeEach(function beforeSetupWithDockerRegistry() { if (!server.enabled) { warnAndSkipTest(this, log); } From 52f3cc311d0a73792e2b38a21ea894f60aaa867e Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 23 Jul 2020 09:51:40 -0700 Subject: [PATCH 16/20] fix skip of flaky test (#72994) --- x-pack/test/accessibility/apps/uptime.ts | 3 ++- x-pack/test/functional/apps/uptime/settings.ts | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index ebd120fa0fee..e6ef1cfe8cfe 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('uptime', () => { + // FLAKY: https://github.com/elastic/kibana/issues/72994 + describe.skip('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index a258cccffbd8..744b9120028d 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -16,8 +16,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // Flaky https://github.com/elastic/kibana/issues/72994 - describe.skip('uptime settings page', () => { + describe('uptime settings page', () => { beforeEach('navigate to clean app root', async () => { // make 10 checks await makeChecks(es, 'myMonitor', 1, 1, 1); From 7d51b978068fa04317cc96ae1ea07dc9281f9cf3 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 23 Jul 2020 12:01:18 -0500 Subject: [PATCH 17/20] [Security Solution][Detections] Fix display of exceptions after creation on Rule Details (#72951) * Refresh rule details when exception list modal modifies the rule This addresses a bug where, when opening the exceptions modal for the first time and creating exceptions, the details page does not reflect these created exceptions until a full refresh. This is due to the hook performing the refresh being dependent on the rule's exceptions_list attribute, which is not populated until after opening the modal. Because the UI is not informed of the rule update, it did not know to refresh the rule. This adds the machinery necessary to make the above work. It: * adds a new hook for fetching/refreshing a rule * Adds an onRuleChange callback to both the ExceptionsViewer and the mutating AddExceptionModal * passes the refresh function in as the onRuleChange callback There's currently a gross intermediate state here where the loading screen is displayed while the rule refreshes in the background; I'll be fixing that shortly. * Do not show loading/blank state while refreshing rule On Rule Details, when the Add Exceptions modal creates the rule's exception list, we refresh quietly in the background by setting our rule from null -> ruleA -> ruleB instead of null -> ruleA -> null -> ruleB. This also simplifies the loading logic in a few places now that we're using our new rule: we mainly care whether or not our rule is populated. * Display toast error if rule fetch fails This should now have feature parity with useRule, while additionally providing a function to refresh the rule. * Refactor tests to leverage existing helpers * Add return type to our callback function Co-authored-by: Elastic Machine --- x-pack/plugins/lists/public/shared_exports.ts | 2 + .../exceptions/add_exception_modal/index.tsx | 11 +++++ ...tch_or_create_rule_exception_list.test.tsx | 29 +++++++++++ ...se_fetch_or_create_rule_exception_list.tsx | 7 ++- .../components/exceptions/viewer/index.tsx | 3 ++ .../containers/detection_engine/rules/api.ts | 19 +++++++- .../detection_engine/rules/use_rule_async.tsx | 48 +++++++++++++++++++ .../detection_engine/rules/details/index.tsx | 23 ++++++--- .../public/shared_imports.ts | 2 + 9 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index 56341035f839..16026a436f15 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -5,7 +5,9 @@ */ // Exports to be shared with plugins +export { withOptionalSignal } from './common/with_optional_signal'; export { useIsMounted } from './common/hooks/use_is_mounted'; +export { useAsync } from './common/hooks/use_async'; export { useApi } from './exceptions/hooks/use_api'; export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 2abbaee5187a..0d93a1ea8871 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -61,6 +61,7 @@ export interface AddExceptionModalBaseProps { export interface AddExceptionModalProps extends AddExceptionModalBaseProps { onCancel: () => void; onConfirm: (didCloseAlert: boolean) => void; + onRuleChange?: () => void; alertStatus?: Status; } @@ -99,6 +100,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ alertData, onCancel, onConfirm, + onRuleChange, alertStatus, }: AddExceptionModalProps) { const { http } = useKibana().services; @@ -152,6 +154,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({ [setExceptionItemsToAdd] ); + const handleRuleChange = useCallback( + (ruleChanged: boolean): void => { + if (ruleChanged && onRuleChange) { + onRuleChange(); + } + }, + [onRuleChange] + ); const onFetchOrCreateExceptionListError = useCallback( (error: Error) => { setFetchOrCreateListError(true); @@ -163,6 +173,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ruleId, exceptionListType, onError: onFetchOrCreateExceptionListError, + onSuccess: handleRuleChange, }); const initialExceptionItems = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 7bef771d367f..6dbf5922e0a9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -38,6 +38,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { ReturnUseFetchOrCreateRuleExceptionList >; const onError = jest.fn(); + const onSuccess = jest.fn(); const error = new Error('Something went wrong'); const ruleId = 'myRuleId'; const abortCtrl = new AbortController(); @@ -94,6 +95,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { ruleId, exceptionListType: listType, onError, + onSuccess, }) ); }); @@ -168,6 +170,15 @@ describe('useFetchOrCreateRuleExceptionList', () => { expect(patchRule).toHaveBeenCalledTimes(1); }); }); + it('invokes onSuccess indicating that the rule changed', async () => { + await act(async () => { + const { waitForNextUpdate } = render(); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(onSuccess).toHaveBeenCalledWith(true); + }); + }); }); describe("when the rule has exception list references and 'detection' is passed in", () => { @@ -207,6 +218,15 @@ describe('useFetchOrCreateRuleExceptionList', () => { expect(result.current[1]).toEqual(detectionExceptionList); }); }); + it('invokes onSuccess indicating that the rule did not change', async () => { + await act(async () => { + const { waitForNextUpdate } = render(); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(onSuccess).toHaveBeenCalledWith(false); + }); + }); describe("but the rule does not have a reference to 'detection' type exception list", () => { beforeEach(() => { @@ -362,5 +382,14 @@ describe('useFetchOrCreateRuleExceptionList', () => { expect(onError).toHaveBeenCalledWith(error); }); }); + + it('does not call onSuccess', async () => { + await act(async () => { + const { waitForNextUpdate } = render(); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(onSuccess).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index b238e25f6de5..2a5ef7b21b51 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -31,6 +31,7 @@ export interface UseFetchOrCreateRuleExceptionListProps { ruleId: Rule['id']; exceptionListType: ExceptionListSchema['type']; onError: (arg: Error) => void; + onSuccess?: (ruleWasChanged: boolean) => void; } /** @@ -47,6 +48,7 @@ export const useFetchOrCreateRuleExceptionList = ({ ruleId, exceptionListType, onError, + onSuccess, }: UseFetchOrCreateRuleExceptionListProps): ReturnUseFetchOrCreateRuleExceptionList => { const [isLoading, setIsLoading] = useState(false); const [exceptionList, setExceptionList] = useState(null); @@ -168,6 +170,9 @@ export const useFetchOrCreateRuleExceptionList = ({ if (isSubscribed) { setExceptionList(exceptionListToUse); setIsLoading(false); + if (onSuccess) { + onSuccess(matchingList == null); + } } } catch (error) { if (isSubscribed) { @@ -183,7 +188,7 @@ export const useFetchOrCreateRuleExceptionList = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [http, ruleId, exceptionListType, onError]); + }, [http, ruleId, exceptionListType, onError, onSuccess]); return [isLoading, exceptionList]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 9cc73d449114..34dc47b9cd41 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -57,6 +57,7 @@ interface ExceptionsViewerProps { exceptionListsMeta: ExceptionIdentifiers[]; availableListTypes: ExceptionListTypeEnum[]; commentsAccordionId: string; + onRuleChange?: () => void; } const ExceptionsViewerComponent = ({ @@ -66,6 +67,7 @@ const ExceptionsViewerComponent = ({ exceptionListsMeta, availableListTypes, commentsAccordionId, + onRuleChange, }: ExceptionsViewerProps): JSX.Element => { const { services } = useKibana(); const [, dispatchToaster] = useStateToaster(); @@ -275,6 +277,7 @@ const ExceptionsViewerComponent = ({ exceptionListType={exceptionListTypeToEdit} onCancel={handleOnCancelExceptionModal} onConfirm={handleOnConfirmExceptionModal} + onRuleChange={onRuleChange} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 66be5397c72c..08d564230b85 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_PREPACKAGED_URL, @@ -126,7 +127,23 @@ export const fetchRules = async ({ * @throws An error if response is not OK */ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + pureFetchRuleById({ id, http: KibanaServices.get().http, signal }); + +/** + * Fetch a Rule by providing a Rule ID + * + * @param id Rule ID's (not rule_id) + * @param http Kibana http service + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const pureFetchRuleById = async ({ + id, + http, + signal, +}: FetchRuleProps & { http: HttpStart }): Promise => + http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'GET', query: { id }, signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx new file mode 100644 index 000000000000..fbca46097dcd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_async.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useCallback } from 'react'; + +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; +import { useHttp } from '../../../../common/lib/kibana'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { pureFetchRuleById } from './api'; +import { Rule } from './types'; +import * as i18n from './translations'; + +export interface UseRuleAsync { + error: unknown; + loading: boolean; + refresh: () => void; + rule: Rule | null; +} + +const _fetchRule = withOptionalSignal(pureFetchRuleById); +const _useRuleAsync = () => useAsync(_fetchRule); + +export const useRuleAsync = (ruleId: string): UseRuleAsync => { + const { start, loading, result, error } = _useRuleAsync(); + const http = useHttp(); + const { addError } = useAppToasts(); + + const fetch = useCallback(() => { + start({ id: ruleId, http }); + }, [http, ruleId, start]); + + // initial fetch + useEffect(() => { + fetch(); + }, [fetch]); + + // toast on error + useEffect(() => { + if (error != null) { + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); + } + }, [addError, error]); + + return { error, loading, refresh: fetch, rule: result ?? null }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 5832f0713493..9c130a7d351f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -37,7 +37,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; -import { useRule, Rule } from '../../../../containers/detection_engine/rules'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { useWithSource } from '../../../../../common/containers/source'; @@ -84,7 +84,7 @@ import { ExceptionsViewer } from '../../../../../common/components/exceptions/vi import { DEFAULT_INDEX_PATTERN, FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; -import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../lists_plugin_deps'; +import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../shared_imports'; import { getEventsViewerBodyHeight, MIN_EVENTS_VIEWER_BODY_HEIGHT, @@ -92,6 +92,7 @@ import { import { footerHeight } from '../../../../../timelines/components/timeline/footer'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; +import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../../../timelines/store/timeline'; import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults'; @@ -146,7 +147,9 @@ export const RuleDetailsPageComponent: FC = ({ } = useListsConfig(); const loading = userInfoLoading || listsConfigLoading; const { detailName: ruleId } = useParams(); - const [isLoading, rule] = useRule(ruleId); + const { rule: maybeRule, refresh: refreshRule, loading: ruleLoading } = useRuleAsync(ruleId); + const [rule, setRule] = useState(null); + const isLoading = ruleLoading && rule == null; // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); @@ -172,10 +175,17 @@ export const RuleDetailsPageComponent: FC = ({ mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); const ruleDetailTabs = getRuleDetailsTabs(rule); - const title = isLoading === true || rule === null ? : rule.name; + // persist rule until refresh is complete + useEffect(() => { + if (maybeRule != null) { + setRule(maybeRule); + } + }, [maybeRule]); + + const title = rule?.name ?? ; const subTitle = useMemo( () => - isLoading === true || rule === null ? ( + rule == null ? ( ) : ( [ @@ -211,7 +221,7 @@ export const RuleDetailsPageComponent: FC = ({ ), ] ), - [isLoading, rule] + [rule] ); // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts @@ -524,6 +534,7 @@ export const RuleDetailsPageComponent: FC = ({ availableListTypes={exceptionLists.allowedExceptionListTypes} commentsAccordionId={'ruleDetailsTabExceptions'} exceptionListsMeta={exceptionLists.lists} + onRuleChange={refreshRule} /> )} {ruleDetailTab === RuleDetailTabs.failures && } diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 9939345324f1..b2c7319b9457 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -32,6 +32,7 @@ export { useIsMounted, useCursor, useApi, + useAsync, useExceptionList, usePersistExceptionItem, usePersistExceptionList, @@ -50,4 +51,5 @@ export { Pagination, UseExceptionListSuccess, addEndpointExceptionList, + withOptionalSignal, } from '../../lists/public'; From e1a3dccf034863278cd51a534acf47d403767619 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 23 Jul 2020 11:01:59 -0600 Subject: [PATCH 18/20] [Maps] fix cloned clustered documents layer returns error (#72975) * [Maps] fix cloned clustered documents layer returns error * tslint --- .../blended_vector_layer.test.tsx | 150 ++++++++++++++++++ .../blended_vector_layer.ts | 16 +- 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx new file mode 100644 index 000000000000..5d234f5be44a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; +import { BlendedVectorLayer } from './blended_vector_layer'; +// @ts-expect-error +import { ESSearchSource } from '../../sources/es_search_source'; +import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; + +jest.mock('../../../kibana_services', () => { + return { + getIsDarkMode() { + return false; + }, + }; +}); + +const mapColors: string[] = []; + +const notClusteredDataRequest = { + data: { isSyncClustered: false }, + dataId: 'ACTIVE_COUNT_DATA_ID', +}; + +const clusteredDataRequest = { + data: { isSyncClustered: true }, + dataId: 'ACTIVE_COUNT_DATA_ID', +}; + +const documentSourceDescriptor = ESSearchSource.createDescriptor({ + geoField: 'myGeoField', + indexPatternId: 'myIndexPattern', + scalingType: SCALING_TYPES.CLUSTERS, +}); + +describe('getSource', () => { + describe('isClustered: true', () => { + test('should return cluster source', async () => { + const blendedVectorLayer = new BlendedVectorLayer({ + source: new ESSearchSource(documentSourceDescriptor), + layerDescriptor: BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: documentSourceDescriptor, + __dataRequests: [clusteredDataRequest], + }, + mapColors + ), + }); + + const source = blendedVectorLayer.getSource(); + expect(source.cloneDescriptor().type).toBe(SOURCE_TYPES.ES_GEO_GRID); + }); + + test('cluster source applyGlobalQuery should be true when document source applyGlobalQuery is true', async () => { + const blendedVectorLayer = new BlendedVectorLayer({ + source: new ESSearchSource(documentSourceDescriptor), + layerDescriptor: BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: documentSourceDescriptor, + __dataRequests: [clusteredDataRequest], + }, + mapColors + ), + }); + + const source = blendedVectorLayer.getSource(); + expect((source.cloneDescriptor() as ESGeoGridSourceDescriptor).applyGlobalQuery).toBe(true); + }); + + test('cluster source applyGlobalQuery should be false when document source applyGlobalQuery is false', async () => { + const blendedVectorLayer = new BlendedVectorLayer({ + source: new ESSearchSource({ + ...documentSourceDescriptor, + applyGlobalQuery: false, + }), + layerDescriptor: BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: documentSourceDescriptor, + __dataRequests: [clusteredDataRequest], + }, + mapColors + ), + }); + + const source = blendedVectorLayer.getSource(); + expect((source.cloneDescriptor() as ESGeoGridSourceDescriptor).applyGlobalQuery).toBe(false); + }); + }); + + describe('isClustered: false', () => { + test('should return document source', async () => { + const blendedVectorLayer = new BlendedVectorLayer({ + source: new ESSearchSource(documentSourceDescriptor), + layerDescriptor: BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: documentSourceDescriptor, + __dataRequests: [notClusteredDataRequest], + }, + mapColors + ), + }); + + const source = blendedVectorLayer.getSource(); + expect(source.cloneDescriptor().type).toBe(SOURCE_TYPES.ES_SEARCH); + }); + }); +}); + +describe('cloneDescriptor', () => { + describe('isClustered: true', () => { + test('Cloned layer descriptor sourceDescriptor should be document source', async () => { + const blendedVectorLayer = new BlendedVectorLayer({ + source: new ESSearchSource(documentSourceDescriptor), + layerDescriptor: BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: documentSourceDescriptor, + __dataRequests: [clusteredDataRequest], + }, + mapColors + ), + }); + + const clonedLayerDescriptor = await blendedVectorLayer.cloneDescriptor(); + expect(clonedLayerDescriptor.sourceDescriptor!.type).toBe(SOURCE_TYPES.ES_SEARCH); + expect(clonedLayerDescriptor.label).toBe('Clone of myIndexPattern'); + }); + }); + + describe('isClustered: false', () => { + test('Cloned layer descriptor sourceDescriptor should be document source', async () => { + const blendedVectorLayer = new BlendedVectorLayer({ + source: new ESSearchSource(documentSourceDescriptor), + layerDescriptor: BlendedVectorLayer.createDescriptor( + { + sourceDescriptor: documentSourceDescriptor, + __dataRequests: [notClusteredDataRequest], + }, + mapColors + ), + }); + + const clonedLayerDescriptor = await blendedVectorLayer.cloneDescriptor(); + expect(clonedLayerDescriptor.sourceDescriptor!.type).toBe(SOURCE_TYPES.ES_SEARCH); + expect(clonedLayerDescriptor.label).toBe('Clone of myIndexPattern'); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index da28574189e6..950d9890a3c6 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -34,6 +34,7 @@ import { SizeDynamicOptions, DynamicStylePropertyOptions, StylePropertyOptions, + LayerDescriptor, VectorLayerDescriptor, } from '../../../../common/descriptor_types'; import { IStyle } from '../../styles/style'; @@ -216,7 +217,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { } } - async getDisplayName(source: ISource) { + async getDisplayName(source?: ISource) { const displayName = await super.getDisplayName(source); return this._isClustered ? i18n.translate('xpack.maps.blendedVectorLayer.clusteredLayerName', { @@ -242,6 +243,19 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { return false; } + async cloneDescriptor(): Promise { + const clonedDescriptor = await super.cloneDescriptor(); + + // Use super getDisplayName instead of instance getDisplayName to avoid getting 'Clustered Clone of Clustered' + const displayName = await super.getDisplayName(); + clonedDescriptor.label = `Clone of ${displayName}`; + + // sourceDescriptor must be document source descriptor + clonedDescriptor.sourceDescriptor = this._documentSource.cloneDescriptor(); + + return clonedDescriptor; + } + getSource() { return this._isClustered ? this._clusterSource : this._documentSource; } From 4b7c16c2ba2ed61b003540a52b0f75baacd1a7e6 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 23 Jul 2020 13:04:11 -0400 Subject: [PATCH 19/20] [Security Solution] [Resolver] Select origin node on load (#72946) --- .../security_solution/public/resolver/store/reducer.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index 028c28d94a41..d0f9701fe944 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -18,7 +18,14 @@ const uiReducer: Reducer = ( }, action ) => { - if (action.type === 'userFocusedOnResolverNode') { + if (action.type === 'serverReturnedResolverData') { + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: action.payload.result.entityID, + selectedNode: action.payload.result.entityID, + }; + return next; + } else if (action.type === 'userFocusedOnResolverNode') { const next: ResolverUIState = { ...state, ariaActiveDescendant: action.payload, From cb48e6e98ecdadbffbaeee5d998c0f25d6af8f6a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 23 Jul 2020 11:14:54 -0600 Subject: [PATCH 20/20] [maps] fix un-hiding layer not syncing data (#73039) * [maps] fix un-hiding layer not syncing data * revert syncDataForLayer to syncDataForLayerId * remove unused method --- .../maps/public/actions/layer_actions.ts | 15 +++------ x-pack/test/functional/apps/maps/index.js | 1 + .../functional/apps/maps/layer_visibility.js | 33 +++++++++++++++++++ .../es_archives/maps/kibana/data.json | 31 +++++++++++++++++ 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 x-pack/test/functional/apps/maps/layer_visibility.js diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index a0d2152e8866..208f6dc6c6f8 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -35,12 +35,7 @@ import { UPDATE_LAYER_STYLE, UPDATE_SOURCE_PROP, } from './map_action_constants'; -import { - clearDataRequests, - syncDataForLayerId, - syncDataForLayer, - updateStyleMeta, -} from './data_request_actions'; +import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_request_actions'; import { cleanTooltipStateForLayer } from './tooltip_actions'; import { JoinDescriptor, LayerDescriptor, StyleDescriptor } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; @@ -175,7 +170,7 @@ export function promotePreviewLayers() { } export function setLayerVisibility(layerId: string, makeVisible: boolean) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return (dispatch: Dispatch, getState: () => MapStoreState) => { // if the current-state is invisible, we also want to sync data // e.g. if a layer was invisible at start-up, it won't have any data loaded const layer = getLayerById(layerId, getState()); @@ -189,19 +184,19 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { dispatch(cleanTooltipStateForLayer(layerId)); } - await dispatch({ + dispatch({ type: SET_LAYER_VISIBILITY, layerId, visibility: makeVisible, }); if (makeVisible) { - dispatch(syncDataForLayer(layer)); + dispatch(syncDataForLayerId(layerId)); } }; } export function toggleLayerVisible(layerId: string) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { + return (dispatch: Dispatch, getState: () => MapStoreState) => { const layer = getLayerById(layerId, getState()); if (!layer) { return; diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index d0735aecda78..4bbe38367d0a 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -35,6 +35,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./saved_object_management')); loadTestFile(require.resolve('./sample_data')); loadTestFile(require.resolve('./auto_fit_to_bounds')); + loadTestFile(require.resolve('./layer_visibility')); loadTestFile(require.resolve('./feature_controls/maps_security')); loadTestFile(require.resolve('./feature_controls/maps_spaces')); loadTestFile(require.resolve('./full_screen_mode')); diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/layer_visibility.js new file mode 100644 index 000000000000..22cff6de416c --- /dev/null +++ b/x-pack/test/functional/apps/maps/layer_visibility.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + + describe('layer visibility', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('document example hidden'); + }); + + afterEach(async () => { + await inspector.close(); + }); + + it('should not make any requests when layer is hidden', async () => { + const noRequests = await PageObjects.maps.doesInspectorHaveRequests(); + expect(noRequests).to.equal(true); + }); + + it('should fetch layer data when layer is made visible', async () => { + await PageObjects.maps.toggleLayerVisibility('logstash'); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('6'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index d2206009d9e6..7690c9258931 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -446,6 +446,37 @@ } } +{ + "type": "doc", + "value": { + "id": "map:2de4de10-cc82-11ea-9b0a-eb2886fc84af", + "index": ".kibana", + "source": { + "map": { + "title" : "document example hidden", + "description" : "", + "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":false,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "name" : "layer_1_source_index_pattern", + "type" : "index-pattern", + "id" : "c698b940-e149-11e8-a35a-370a8516603a" + } + ], + "migrationVersion" : { + "map" : "7.9.0" + }, + "updated_at" : "2020-07-23T01:16:47.600Z" + } + } +} + + + { "type": "doc", "value": {