From 964f0929fc1ee1d073dc2804f4b13d90b30a4392 Mon Sep 17 00:00:00 2001 From: Claudio Procida Date: Fri, 3 Dec 2021 00:51:59 +0100 Subject: [PATCH] [RAC] Adds stats about rules to Alerts page (#119750) * Adds EuiStat to alerts page template * Adds functional tests for stat counters * Whoops - no exclusive tests * Uses triggers_actions_ui API * Toaster on error * Early review feedback * Uses new rule aggregation API * Makes tests pass * Adds logging * Whoops forgot an await * Limits triggers actions UI exports to loadAlertAggregations * Creates rules via API for functional tests * Extracts common methods to create rules via API * Removes unnecessary template strings * Cleanup * Reuses common dummy alert plugin fixture * Removes unnecessary config * Removes unnecessary config --- .../containers/alerts_page/alerts_page.tsx | 114 ++++++++++- .../triggers_actions_ui/public/index.ts | 1 + .../services/observability/alerts/common.ts | 11 + .../apps/triggers_actions_ui/alerts_list.ts | 189 ++++++++++-------- .../lib/alert_api_actions.ts | 87 ++++++++ .../apps/observability/alerts/index.ts | 68 +++++++ .../with_rac_write.config.ts | 50 ++++- 7 files changed, 432 insertions(+), 88 deletions(-) create mode 100644 x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index b19a1dbe86fe1..06040d9a186ff 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -5,15 +5,17 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; import { IndexPatternBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useRef, useState, useEffect } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { AlertStatus } from '@kbn/rule-data-utils/alerts_as_data_status'; import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { loadAlertAggregations as loadRuleAggregations } from '../../../../../../../plugins/triggers_actions_ui/public'; import { AlertStatusFilterButton } from '../../../../../common/typings'; import { ParsedTechnicalFields } from '../../../../../../rule_registry/common/parse_technical_fields'; import { ExperimentalBadge } from '../../../../components/shared/experimental_badge'; @@ -34,6 +36,12 @@ import { import './styles.scss'; import { AlertsStatusFilter, AlertsDisclaimer, AlertsSearchBar } from '../../components'; +interface RuleStatsState { + total: number; + disabled: number; + muted: number; + error: number; +} export interface TopAlert { fields: ParsedTechnicalFields; start: number; @@ -41,6 +49,12 @@ export interface TopAlert { link?: string; active: boolean; } + +const Divider = euiStyled.div` + border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + height: 100%; +`; + const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const NO_INDEX_NAMES: string[] = []; const NO_INDEX_PATTERNS: IndexPatternBase[] = []; @@ -60,6 +74,17 @@ function AlertsPage() { const timefilterService = useTimefilterService(); const { rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, workflowStatus } = useAlertsPageStateContainer(); + const { + http, + notifications: { toasts }, + } = core; + const [ruleStatsLoading, setRuleStatsLoading] = useState(false); + const [ruleStats, setRuleStats] = useState({ + total: 0, + disabled: 0, + muted: 0, + error: 0, + }); useEffect(() => { syncAlertStatusFilterStatus(kuery as string); @@ -73,6 +98,48 @@ function AlertsPage() { }, ]); + async function loadRuleStats() { + setRuleStatsLoading(true); + try { + const response = await loadRuleAggregations({ + http, + }); + // Note that the API uses the semantics of 'alerts' instead of 'rules' + const { alertExecutionStatus, ruleMutedStatus, ruleEnabledStatus } = response; + if (alertExecutionStatus && ruleMutedStatus && ruleEnabledStatus) { + const total = Object.entries(alertExecutionStatus).reduce((acc, [key, value]) => { + if (key !== 'error') { + acc = acc + value; + } + return acc; + }, 0); + const { error } = alertExecutionStatus; + const { muted } = ruleMutedStatus; + const { disabled } = ruleEnabledStatus; + setRuleStats({ + ...ruleStats, + total, + disabled, + muted, + error, + }); + } + setRuleStatsLoading(false); + } catch (_e) { + toasts.addDanger({ + title: i18n.translate('xpack.observability.alerts.ruleStats.loadError', { + defaultMessage: 'Unable to load rule stats', + }), + }); + setRuleStatsLoading(false); + } + } + + useEffect(() => { + loadRuleStats(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // In a future milestone we'll have a page dedicated to rule management in // observability. For now link to the settings page. const manageRulesHref = prepend('/app/management/insightsAndAlerting/triggersActions/alerts'); @@ -198,12 +265,53 @@ function AlertsPage() { ), rightSideItems: [ + , + , + , + , + , {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', { defaultMessage: 'Manage Rules', })} , - ], + ].reverse(), }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 4c4a424b51eea..97f4d847361f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -45,6 +45,7 @@ export function plugin() { export { Plugin }; export * from './plugin'; +export { loadAlertAggregations } from './application/lib/alert_api/aggregate'; export { loadActionTypes } from './application/lib/action_connector_api/connector_types'; diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 8c6352fff9864..2f888d3d733c0 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { chunk } from 'lodash'; import { ALERT_STATUS_ACTIVE, @@ -270,6 +271,15 @@ export function ObservabilityAlertsCommonProvider({ return actionsOverflowButtons[index] || null; }; + const getAlertStatValue = async (testSubj: string) => { + const stat = await testSubjects.find(testSubj); + const title = await stat.findByCssSelector('.euiStat__title'); + const count = await title.getVisibleText(); + const value = Number.parseInt(count, 10); + expect(Number.isNaN(value)).to.be(false); + return value; + }; + return { getQueryBar, clearQueryBar, @@ -307,5 +317,6 @@ export function ObservabilityAlertsCommonProvider({ viewRuleDetailsButtonClick, viewRuleDetailsLinkClick, getAlertsFlyoutViewRuleDetailsLinkOrFail, + getAlertStatValue, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 04b9b1b45b633..0a0e409ef53a9 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -7,8 +7,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + createAction, + createAlert, + createAlertManualCleanup, + createFailingAlert, + disableAlert, + muteAlert, +} from '../../lib/alert_api_actions'; import { ObjectRemover } from '../../lib/object_remover'; -import { generateUniqueKey, getTestAlertData, getTestActionData } from '../../lib/get_test_data'; +import { generateUniqueKey } from '../../lib/get_test_data'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); @@ -18,52 +26,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const objectRemover = new ObjectRemover(supertest); - async function createAlertManualCleanup(overwrites: Record = {}) { - const { body: createdAlert } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestAlertData(overwrites)) - .expect(200); - return createdAlert; - } - - async function createFailingAlert() { - return await createAlert({ - rule_type_id: 'test.failing', - schedule: { interval: '30s' }, - }); - } - - async function createAlert(overwrites: Record = {}) { - const createdAlert = await createAlertManualCleanup(overwrites); - objectRemover.add(createdAlert.id, 'alert', 'alerts'); - return createdAlert; - } - - async function createAction(overwrites: Record = {}) { - const { body: createdAction } = await supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send(getTestActionData(overwrites)) - .expect(200); - objectRemover.add(createdAction.id, 'action', 'actions'); - return createdAction; - } - - async function muteAlert(alertId: string) { - const { body: alert } = await supertest - .post(`/api/alerting/rule/${alertId}/_mute_all`) - .set('kbn-xsrf', 'foo'); - return alert; - } - - async function disableAlert(alertId: string) { - const { body: alert } = await supertest - .post(`/api/alerting/rule/${alertId}/_disable`) - .set('kbn-xsrf', 'foo'); - return alert; - } - async function refreshAlertsList() { await testSubjects.click('rulesTab'); } @@ -80,9 +42,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should display alerts in alphabetical order', async () => { const uniqueKey = generateUniqueKey(); - await createAlert({ name: 'b', tags: [uniqueKey] }); - await createAlert({ name: 'c', tags: [uniqueKey] }); - await createAlert({ name: 'a', tags: [uniqueKey] }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'b', tags: [uniqueKey] }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'c', tags: [uniqueKey] }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'a', tags: [uniqueKey] }, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(uniqueKey); @@ -95,7 +69,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should search for alert', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ + supertest, + objectRemover, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -108,8 +85,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should update alert list on the search clear button click', async () => { - await createAlert({ name: 'b' }); - await createAlert({ name: 'c', tags: [] }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'b' }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'c', tags: [] }, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts('b'); @@ -138,7 +123,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should search for tags', async () => { - const createdAlert = await createAlert({ tags: ['tag', 'tagtag', 'taggity tag'] }); + const createdAlert = await createAlert({ + supertest, + objectRemover, + overwrites: { tags: ['tag', 'tagtag', 'taggity tag'] }, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(`${createdAlert.name} tag`); @@ -151,7 +140,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should display an empty list when search did not return any alerts', async () => { - await createAlert(); + await createAlert({ + supertest, + objectRemover, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(`An Alert That For Sure Doesn't Exist!`); @@ -159,7 +151,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should disable single alert', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ + supertest, + objectRemover, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -175,8 +170,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should re-enable single alert', async () => { - const createdAlert = await createAlert(); - await disableAlert(createdAlert.id); + const createdAlert = await createAlert({ + supertest, + objectRemover, + }); + await disableAlert({ supertest, alertId: createdAlert.id }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -191,7 +189,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should mute single alert', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ + supertest, + objectRemover, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -207,7 +208,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should be able to mute the rule with non "alerts" consumer from a non editable context', async () => { - const createdAlert = await createAlert({ consumer: 'siem' }); + const createdAlert = await createAlert({ + supertest, + objectRemover, + overwrites: { consumer: 'siem' }, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -223,8 +228,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should unmute single alert', async () => { - const createdAlert = await createAlert(); - await muteAlert(createdAlert.id); + const createdAlert = await createAlert({ + supertest, + objectRemover, + }); + await muteAlert({ supertest, alertId: createdAlert.id }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -239,8 +247,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should delete single alert', async () => { - await createAlert(); - const secondAlert = await createAlertManualCleanup(); + await createAlert({ + supertest, + objectRemover, + }); + const secondAlert = await createAlertManualCleanup({ supertest }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(secondAlert.name); @@ -262,7 +273,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should mute all selection', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ supertest, objectRemover }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -285,7 +296,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should unmute all selection', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ supertest, objectRemover }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -307,7 +318,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should disable all selection', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ supertest, objectRemover }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -328,7 +339,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should enable all selection', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ supertest, objectRemover }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -352,7 +363,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should delete all selection', async () => { const namePrefix = generateUniqueKey(); - const createdAlert = await createAlertManualCleanup({ name: `${namePrefix}-1` }); + const createdAlert = await createAlertManualCleanup({ + supertest, + overwrites: { name: `${namePrefix}-1` }, + }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(namePrefix); @@ -376,8 +390,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should filter alerts by the status', async () => { - await createAlert(); - const failingAlert = await createFailingAlert(); + await createAlert({ supertest, objectRemover }); + const failingAlert = await createFailingAlert({ supertest, objectRemover }); // initialy alert get Pending status, so we need to retry refresh list logic to get the post execution statuses await retry.try(async () => { await refreshAlertsList(); @@ -398,7 +412,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should display total alerts by status and error banner only when exists alerts with status error', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ supertest, objectRemover }); await retry.try(async () => { await refreshAlertsList(); const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); @@ -414,7 +428,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); expect(alertsErrorBannerWhenNoErrors).to.have.length(0); - await createFailingAlert(); + await createFailingAlert({ supertest, objectRemover }); await retry.try(async () => { await refreshAlertsList(); const alertsErrorBannerExistErrors = await find.allByCssSelector( @@ -438,8 +452,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should filter alerts by the alert type', async () => { - await createAlert(); - const failingAlert = await createFailingAlert(); + await createAlert({ supertest, objectRemover }); + const failingAlert = await createFailingAlert({ supertest, objectRemover }); await refreshAlertsList(); await testSubjects.click('alertTypeFilterButton'); expect(await (await testSubjects.find('alertType0Group')).getVisibleText()).to.eql('Alerts'); @@ -455,16 +469,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should filter alerts by the action type', async () => { - await createAlert(); - const action = await createAction(); + await createAlert({ + supertest, + objectRemover, + }); + const action = await createAction({ supertest, objectRemover }); const noopAlertWithAction = await createAlert({ - actions: [ - { - id: action.id, - group: 'default', - params: { level: 'info', message: 'gfghfhg' }, - }, - ], + supertest, + objectRemover, + overwrites: { + actions: [ + { + id: action.id, + group: 'default', + params: { level: 'info', message: 'gfghfhg' }, + }, + ], + }, }); await refreshAlertsList(); await testSubjects.click('actionTypeFilterButton'); diff --git a/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts b/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts new file mode 100644 index 0000000000000..40e567c299826 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ObjectRemover } from './object_remover'; +import { getTestAlertData, getTestActionData } from './get_test_data'; + +export async function createAlertManualCleanup({ + supertest, + overwrites = {}, +}: { + supertest: any; + overwrites?: Record; +}) { + const { body: createdAlert } = await supertest + .post('/api/alerting/rule') + .set('kbn-xsrf', 'foo') + .send(getTestAlertData(overwrites)) + .expect(200); + return createdAlert; +} + +export async function createFailingAlert({ + supertest, + objectRemover, +}: { + supertest: any; + objectRemover: ObjectRemover; +}) { + return await createAlert({ + supertest, + overwrites: { + rule_type_id: 'test.failing', + schedule: { interval: '30s' }, + }, + objectRemover, + }); +} + +export async function createAlert({ + supertest, + objectRemover, + overwrites = {}, +}: { + supertest: any; + objectRemover: ObjectRemover; + overwrites?: Record; +}) { + const createdAlert = await createAlertManualCleanup({ supertest, overwrites }); + objectRemover.add(createdAlert.id, 'alert', 'alerts'); + return createdAlert; +} + +export async function createAction({ + supertest, + objectRemover, + overwrites = {}, +}: { + supertest: any; + objectRemover: ObjectRemover; + overwrites?: Record; +}) { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send(getTestActionData(overwrites)) + .expect(200); + objectRemover.add(createdAction.id, 'action', 'actions'); + return createdAction; +} + +export async function muteAlert({ supertest, alertId }: { supertest: any; alertId: string }) { + const { body: alert } = await supertest + .post(`/api/alerting/rule/${alertId}/_mute_all`) + .set('kbn-xsrf', 'foo'); + return alert; +} + +export async function disableAlert({ supertest, alertId }: { supertest: any; alertId: string }) { + const { body: alert } = await supertest + .post(`/api/alerting/rule/${alertId}/_disable`) + .set('kbn-xsrf', 'foo'); + return alert; +} diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index 3abf04ed29e67..12a83f19ca258 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -7,6 +7,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ObjectRemover } from '../../../../functional_with_es_ssl/lib/object_remover'; +import { + createAlert, + disableAlert, + muteAlert, +} from '../../../../functional_with_es_ssl/lib/alert_api_actions'; +import { generateUniqueKey } from '../../../../functional_with_es_ssl/lib/get_test_data'; async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { for (let index = 0; index < array.length; index++) { @@ -21,6 +28,8 @@ const TOTAL_ALERTS_CELL_COUNT = 165; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const find = getService('find'); + const supertest = getService('supertest'); + const objectRemover = new ObjectRemover(supertest); describe('Observability alerts', function () { this.tags('includeFirefox'); @@ -236,6 +245,65 @@ export default ({ getService }: FtrProviderContext) => { expect(await find.existsByCssSelector('[title="Rules and Connectors"]')).to.eql(true); }); }); + + describe('Stat counters', () => { + beforeEach(async () => { + const uniqueKey = generateUniqueKey(); + + const alertToDisable = await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'b', tags: [uniqueKey] }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'c', tags: [uniqueKey] }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'a', tags: [uniqueKey] }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'd', tags: [uniqueKey] }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'e', tags: [uniqueKey] }, + }); + const alertToMute = await createAlert({ + supertest, + objectRemover, + overwrites: { name: 'f', tags: [uniqueKey] }, + }); + + await disableAlert({ supertest, alertId: alertToDisable.id }); + await muteAlert({ supertest, alertId: alertToMute.id }); + + await observability.alerts.common.navigateToTimeWithData(); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + }); + + it('Exist and display expected values', async () => { + const subjToValueMap: { [key: string]: number } = { + statRuleCount: 6, + statDisabled: 1, + statMuted: 1, + statErrors: 0, + }; + await asyncForEach(Object.keys(subjToValueMap), async (subject: string) => { + const value = await observability.alerts.common.getAlertStatValue(subject); + expect(value).to.be(subjToValueMap[subject]); + }); + }); + }); }); }); }; diff --git a/x-pack/test/observability_functional/with_rac_write.config.ts b/x-pack/test/observability_functional/with_rac_write.config.ts index dcf6b121d6258..71a1de1df6a77 100644 --- a/x-pack/test/observability_functional/with_rac_write.config.ts +++ b/x-pack/test/observability_functional/with_rac_write.config.ts @@ -6,10 +6,26 @@ */ import { readFileSync } from 'fs'; -import { resolve } from 'path'; +import { resolve, join } from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test'; +// .server-log is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.swimlane', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); @@ -36,6 +52,38 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + `--plugin-path=${join( + __dirname, + '..', + 'functional_with_es_ssl', + 'fixtures', + 'plugins', + 'alerts' + )}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + `--xpack.actions.preconfiguredAlertHistoryEsIndex=false`, + `--xpack.actions.preconfigured=${JSON.stringify({ + 'my-slack1': { + actionTypeId: '.slack', + name: 'Slack#xyztest', + secrets: { + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', + }, + }, + 'my-server-log': { + actionTypeId: '.server-log', + name: 'Serverlog#xyz', + }, + 'my-email-connector': { + actionTypeId: '.email', + name: 'Email#test-preconfigured-email', + config: { + from: 'me@example.com', + host: 'localhost', + port: '1025', + }, + }, + })}`, ], }, uiSettings: {