[RAM] Remove isSnoozeUntil and calculate on the fly for get/find (#136148)

* refactor to avoid multiple call to the SO and avoid some kind of occ

* simplify logic

* formatting + test

* Remove isSnoozeUntil has a saved object attributes

* update rule list to use rule has snooze as a filter

* fix check type

* update functional test + review I

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Xavier Mouligneau 2022-07-26 16:23:37 -04:00 committed by GitHub
parent e8a9eb0a2e
commit cc31f24af5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 249 additions and 221 deletions

View file

@ -54,6 +54,13 @@ const termSchema = s.object({
term: s.recordOf(s.string(), s.oneOf([s.string(), s.boolean(), s.number()])),
});
const existsSchema = s.object({
exists: s.maybe(
s.object({
field: s.string(),
})
),
});
// TODO: it would be great if we could recursively build the schema since the aggregation have be nested
// For more details see how the types are defined in the elasticsearch javascript client:
// https://github.com/elastic/elasticsearch-js/blob/4ad5daeaf401ce8ebb28b940075e0a67e56ff9ce/src/api/typesWithBodyKey.ts#L5295
@ -134,7 +141,7 @@ export const bucketAggsSchemas: Record<string, ObjectType> = {
format: s.string(),
ranges: s.arrayOf(s.object({ from: s.maybe(s.string()), to: s.maybe(s.string()) })),
}),
filter: termSchema,
filter: s.oneOf([termSchema, existsSchema]) as unknown as ObjectType,
filters: s.object({
filters: s.recordOf(s.string(), s.oneOf([termSchema, boolSchema])),
}),

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import { first } from 'lodash';
import { first, isEmpty } from 'lodash';
import { SanitizedRule, RuleTypeParams } from '../../common/rule';
import { isSnoozeActive } from './snooze/is_snooze_active';
type RuleSnoozeProps = Pick<SanitizedRule<RuleTypeParams>, 'muteAll' | 'snoozeSchedule'>;
type ActiveSnoozes = Array<{ snoozeEndTime: Date; id: string; lastOccurrence?: Date }>;
function getActiveSnoozes(rule: Pick<RuleSnoozeProps, 'snoozeSchedule'>): ActiveSnoozes | null {
if (rule.snoozeSchedule == null) {
export function getActiveSnoozes(rule: RuleSnoozeProps): ActiveSnoozes | null {
if (rule.snoozeSchedule == null || isEmpty(rule.snoozeSchedule)) {
return null;
}
@ -26,9 +26,7 @@ function getActiveSnoozes(rule: Pick<RuleSnoozeProps, 'snoozeSchedule'>): Active
);
}
export function getActiveScheduledSnoozes(
rule: Pick<RuleSnoozeProps, 'snoozeSchedule'>
): ActiveSnoozes | null {
export function getActiveScheduledSnoozes(rule: RuleSnoozeProps): ActiveSnoozes | null {
return getActiveSnoozes(rule)?.filter((r) => Boolean(r.id)) ?? null;
}

View file

@ -88,7 +88,11 @@ const buildGetRuleRoute = ({
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const { id } = req.params;
const rule = await rulesClient.get({ id, excludeFromPublicApi });
const rule = await rulesClient.get({
id,
excludeFromPublicApi,
includeSnoozeData: true,
});
return res.ok({
body: rewriteBodyRes(rule),
});

View file

@ -35,7 +35,7 @@ const createRulesClientMock = () => {
bulkEdit: jest.fn(),
snooze: jest.fn(),
unsnooze: jest.fn(),
updateSnoozedUntilTime: jest.fn(),
calculateIsSnoozedUntil: jest.fn(),
clearExpiredSnoozes: jest.fn(),
};
return mocked;

View file

@ -8,7 +8,18 @@
import Semver from 'semver';
import pMap from 'p-map';
import Boom from '@hapi/boom';
import { omit, isEqual, map, uniq, pick, truncate, trim, mapValues, cloneDeep } from 'lodash';
import {
omit,
isEqual,
map,
uniq,
pick,
truncate,
trim,
mapValues,
cloneDeep,
isEmpty,
} from 'lodash';
import { i18n } from '@kbn/i18n';
import { fromKueryExpression, KueryNode, nodeBuilder } from '@kbn/es-query';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
@ -148,11 +159,9 @@ export interface RuleAggregation {
}>;
};
snoozed: {
buckets: Array<{
key: number;
key_as_string: string;
count: {
doc_count: number;
}>;
};
};
tags: {
buckets: Array<{
@ -524,7 +533,6 @@ export class RulesClient {
updatedBy: username,
createdAt: new Date(createTime).toISOString(),
updatedAt: new Date(createTime).toISOString(),
isSnoozedUntil: null,
snoozeSchedule: [],
params: updatedParams as RawRule['params'],
muteAll: false,
@ -617,10 +625,12 @@ export class RulesClient {
public async get<Params extends RuleTypeParams = never>({
id,
includeLegacyId = false,
includeSnoozeData = false,
excludeFromPublicApi = false,
}: {
id: string;
includeLegacyId?: boolean;
includeSnoozeData?: boolean;
excludeFromPublicApi?: boolean;
}): Promise<SanitizedRule<Params> | SanitizedRuleWithLegacyId<Params>> {
const result = await this.unsecuredSavedObjectsClient.get<RawRule>('alert', id);
@ -653,7 +663,8 @@ export class RulesClient {
result.attributes,
result.references,
includeLegacyId,
excludeFromPublicApi
excludeFromPublicApi,
includeSnoozeData
);
}
@ -1100,8 +1111,17 @@ export class RulesClient {
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } },
},
snoozed: {
terms: {
field: 'alert.attributes.isSnoozedUntil',
nested: {
path: 'alert.attributes.snoozeSchedule',
},
aggs: {
count: {
filter: {
exists: {
field: 'alert.attributes.snoozeSchedule.duration',
},
},
},
},
},
},
@ -1161,9 +1181,8 @@ export class RulesClient {
unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
};
const snoozedBuckets = resp.aggregations.snoozed.buckets;
ret.ruleSnoozedStatus = {
snoozed: snoozedBuckets.reduce((acc, bucket) => acc + bucket.doc_count, 0),
snoozed: resp.aggregations.snoozed?.count?.doc_count ?? 0,
};
const tagsBuckets = resp.aggregations.tags?.buckets || [];
@ -2263,7 +2282,7 @@ export class RulesClient {
id,
updateAttributes,
updateOptions
).then(() => this.updateSnoozedUntilTime({ id }));
);
}
public async unsnooze({
@ -2317,11 +2336,12 @@ export class RulesClient {
);
this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
const snoozeSchedule = scheduleIds
? clearScheduledSnoozesById(attributes, scheduleIds)
: clearCurrentActiveSnooze(attributes);
const updateAttributes = this.updateMeta({
snoozeSchedule: scheduleIds
? clearScheduledSnoozesById(attributes, scheduleIds)
: clearCurrentActiveSnooze(attributes),
snoozeSchedule,
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
...(!scheduleIds ? { muteAll: false } : {}),
@ -2333,31 +2353,15 @@ export class RulesClient {
id,
updateAttributes,
updateOptions
).then(() => this.updateSnoozedUntilTime({ id }));
);
}
public async updateSnoozedUntilTime({ id }: { id: string }): Promise<void> {
const { attributes, version } = await this.unsecuredSavedObjectsClient.get<RawRule>(
'alert',
id
);
const isSnoozedUntil = getRuleSnoozeEndTime(attributes);
if (!isSnoozedUntil && !attributes.isSnoozedUntil) return;
const updateAttributes = this.updateMeta({
isSnoozedUntil: isSnoozedUntil ? isSnoozedUntil.toISOString() : null,
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
await partiallyUpdateAlert(
this.unsecuredSavedObjectsClient,
id,
updateAttributes,
updateOptions
);
public calculateIsSnoozedUntil(rule: {
muteAll: boolean;
snoozeSchedule?: RuleSnooze;
}): string | null {
const isSnoozedUntil = getRuleSnoozeEndTime(rule);
return isSnoozedUntil ? isSnoozedUntil.toISOString() : null;
}
public async clearExpiredSnoozes({ id }: { id: string }): Promise<void> {
@ -2391,7 +2395,7 @@ export class RulesClient {
id,
updateAttributes,
updateOptions
).then(() => this.updateSnoozedUntilTime({ id }));
);
}
public async muteAll({ id }: { id: string }): Promise<void> {
@ -2757,7 +2761,6 @@ export class RulesClient {
schedule,
actions,
snoozeSchedule,
isSnoozedUntil,
...partialRawRule
}: Partial<RawRule>,
references: SavedObjectReference[] | undefined,
@ -2773,8 +2776,14 @@ export class RulesClient {
...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}),
},
}));
const includeSnoozeSchedule = snoozeSchedule !== undefined && !excludeFromPublicApi;
const includeSnoozeSchedule =
snoozeSchedule !== undefined && !isEmpty(snoozeSchedule) && !excludeFromPublicApi;
const isSnoozedUntil = includeSnoozeSchedule
? this.calculateIsSnoozedUntil({
muteAll: partialRawRule.muteAll ?? false,
snoozeSchedule,
})
: null;
const rule = {
id,
notifyWhen,
@ -2784,15 +2793,18 @@ export class RulesClient {
schedule: schedule as IntervalSchedule,
actions: actions ? this.injectReferencesIntoActions(id, actions, references || []) : [],
params: this.injectReferencesIntoParams(id, ruleType, params, references || []) as Params,
...(includeSnoozeSchedule ? { snoozeSchedule: snoozeScheduleDates } : {}),
...(includeSnoozeData && includeSnoozeSchedule
...(excludeFromPublicApi ? {} : { snoozeSchedule: snoozeScheduleDates ?? [] }),
...(includeSnoozeData && !excludeFromPublicApi
? {
activeSnoozes: getActiveScheduledSnoozes({ snoozeSchedule })?.map((s) => s.id),
activeSnoozes: getActiveScheduledSnoozes({
snoozeSchedule,
muteAll: partialRawRule.muteAll ?? false,
})?.map((s) => s.id),
isSnoozedUntil,
}
: {}),
...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}),
...(createdAt ? { createdAt: new Date(createdAt) } : {}),
...(isSnoozedUntil ? { isSnoozedUntil: new Date(isSnoozedUntil) } : {}),
...(scheduledTaskId ? { scheduledTaskId } : {}),
...(executionStatus
? { executionStatus: ruleExecutionStatusFromRaw(this.logger, id, executionStatus) }

View file

@ -103,15 +103,10 @@ describe('aggregate()', () => {
],
},
snoozed: {
buckets: [
{
key: '2022-03-21T20:22:01.501Z-*',
format: 'strict_date_time',
from: 1.647894121501e12,
from_as_string: '2022-03-21T20:22:01.501Z',
doc_count: 2,
},
],
doc_count: 0,
count: {
doc_count: 0,
},
},
tags: {
buckets: [
@ -175,7 +170,7 @@ describe('aggregate()', () => {
"unmuted": 27,
},
"ruleSnoozedStatus": Object {
"snoozed": 2,
"snoozed": 0,
},
"ruleTags": Array [
"a",
@ -203,7 +198,18 @@ describe('aggregate()', () => {
terms: { field: 'alert.attributes.muteAll' },
},
snoozed: {
terms: { field: 'alert.attributes.isSnoozedUntil' },
aggs: {
count: {
filter: {
exists: {
field: 'alert.attributes.snoozeSchedule.duration',
},
},
},
},
nested: {
path: 'alert.attributes.snoozeSchedule',
},
},
tags: {
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } },
@ -247,7 +253,18 @@ describe('aggregate()', () => {
terms: { field: 'alert.attributes.muteAll' },
},
snoozed: {
terms: { field: 'alert.attributes.isSnoozedUntil' },
aggs: {
count: {
filter: {
exists: {
field: 'alert.attributes.snoozeSchedule.duration',
},
},
},
},
nested: {
path: 'alert.attributes.snoozeSchedule',
},
},
tags: {
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } },

View file

@ -412,7 +412,6 @@ describe('create()', () => {
"status": "pending",
"warning": null,
},
"isSnoozedUntil": null,
"legacyId": null,
"meta": Object {
"versionApiKeyLastmodified": "v8.0.0",
@ -619,7 +618,6 @@ describe('create()', () => {
"status": "pending",
"warning": null,
},
"isSnoozedUntil": null,
"legacyId": "123",
"meta": Object {
"versionApiKeyLastmodified": "v7.10.0",
@ -1046,7 +1044,6 @@ describe('create()', () => {
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
isSnoozedUntil: null,
legacyId: null,
executionStatus: {
error: null,
@ -1246,7 +1243,6 @@ describe('create()', () => {
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
isSnoozedUntil: null,
legacyId: null,
executionStatus: {
error: null,
@ -1411,7 +1407,6 @@ describe('create()', () => {
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
isSnoozedUntil: null,
legacyId: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
@ -1576,7 +1571,6 @@ describe('create()', () => {
alertTypeId: '123',
consumer: 'bar',
name: 'abc',
isSnoozedUntil: null,
legacyId: null,
params: { bar: true },
apiKey: null,
@ -1706,7 +1700,6 @@ describe('create()', () => {
params: { foo: true },
},
],
isSnoozedUntil: null,
legacyId: null,
alertTypeId: '123',
consumer: 'bar',
@ -1839,7 +1832,6 @@ describe('create()', () => {
params: { foo: true },
},
],
isSnoozedUntil: null,
legacyId: null,
alertTypeId: '123',
consumer: 'bar',
@ -2001,7 +1993,6 @@ describe('create()', () => {
],
apiKeyOwner: null,
apiKey: null,
isSnoozedUntil: null,
legacyId: null,
createdBy: 'elastic',
updatedBy: 'elastic',
@ -2354,7 +2345,6 @@ describe('create()', () => {
alertTypeId: '123',
consumer: 'bar',
name: 'abc',
isSnoozedUntil: null,
legacyId: null,
params: { bar: true },
apiKey: Buffer.from('123:abc').toString('base64'),
@ -2454,7 +2444,6 @@ describe('create()', () => {
params: { foo: true },
},
],
isSnoozedUntil: null,
legacyId: null,
alertTypeId: '123',
consumer: 'bar',

View file

@ -162,6 +162,7 @@ describe('find()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
@ -262,6 +263,7 @@ describe('find()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
@ -460,58 +462,60 @@ describe('find()', () => {
);
expect(result).toMatchInlineSnapshot(`
Object {
"data": Array [
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
Object {
"data": Array [
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
],
"alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
},
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
},
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "2",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "9",
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "2",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "9",
"schedule": Object {
"interval": "20s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
},
"schedule": Object {
"interval": "20s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
"page": 1,
"perPage": 10,
"total": 2,
}
],
"page": 1,
"perPage": 10,
"total": 2,
}
`);
});
@ -712,6 +716,7 @@ describe('find()', () => {
"notifyWhen": undefined,
"params": undefined,
"schedule": undefined,
"snoozeSchedule": Array [],
"tags": Array [
"myTag",
],

View file

@ -107,6 +107,7 @@ describe('get()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
@ -187,6 +188,7 @@ describe('get()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
@ -287,6 +289,7 @@ describe('get()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);

View file

@ -113,6 +113,7 @@ describe('resolve()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
@ -187,6 +188,7 @@ describe('resolve()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
@ -293,6 +295,7 @@ describe('resolve()', () => {
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);

View file

@ -255,9 +255,5 @@ export const alertMappings: SavedObjectsTypeMappingDefinition = {
},
},
},
isSnoozedUntil: {
type: 'date',
format: 'strict_date_time',
},
},
};

View file

@ -452,10 +452,6 @@ export class TaskRunner<
await rulesClient.clearExpiredSnoozes({ id: rule.id });
const ruleIsSnoozed = isRuleSnoozed(rule);
if (ruleIsSnoozed) {
await this.markRuleAsSnoozed(rule.id, rulesClient);
}
if (!ruleIsSnoozed && this.shouldLogAndScheduleActionsForAlerts()) {
const mutedAlertIdsSet = new Set(mutedInstanceIds);
@ -533,10 +529,6 @@ export class TaskRunner<
};
}
private async markRuleAsSnoozed(id: string, rulesClient: RulesClientApi) {
await rulesClient.updateSnoozedUntilTime({ id });
}
private async loadRuleAttributesAndRun(): Promise<Resultable<RuleRunResult, Error>> {
const {
params: { alertId: ruleId, spaceId },

View file

@ -30663,7 +30663,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "Dernière réponse",
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "Actif",
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "Récupéré",
"xpack.triggersActionsUI.sections.ruleDetails.ruleStatusFilterButton": "Afficher",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastMessage": "Cette règle possède un intervalle défini au-dessous de l'intervalle minimal configuré. Cela peut avoir un impact sur les performances.",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastMessageButton": "Modifier la règle",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastTitle": "Paramètres de configuration",

View file

@ -30742,7 +30742,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "前回の応答",
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "アクティブ",
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "回復済み",
"xpack.triggersActionsUI.sections.ruleDetails.ruleStatusFilterButton": "表示",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastMessage": "このルールには、構成した最小間隔を下回る間隔が設定されています。これはパフォーマンスに影響する場合があります。",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastMessageButton": "ルールを編集",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastTitle": "構成設定",

View file

@ -30770,7 +30770,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "上次响应",
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "活动",
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "已恢复",
"xpack.triggersActionsUI.sections.ruleDetails.ruleStatusFilterButton": "查看",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastMessage": "此规则设置的时间间隔低于配置的最小时间间隔。这可能会影响性能。",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastMessageButton": "编辑规则",
"xpack.triggersActionsUI.sections.ruleDetails.scheduleIntervalToastTitle": "配置设置",

View file

@ -243,7 +243,7 @@ describe('loadRuleAggregations', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now))",
"filter": "alert.attributes.enabled: true",
"search": undefined,
"search_fields": undefined,
},
@ -262,7 +262,7 @@ describe('loadRuleAggregations', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)",
"filter": "alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })",
"search": undefined,
"search_fields": undefined,
},
@ -281,7 +281,7 @@ describe('loadRuleAggregations', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)",
"filter": "alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })",
"search": undefined,
"search_fields": undefined,
},

View file

@ -45,9 +45,7 @@ describe('mapFiltersToKql', () => {
mapFiltersToKql({
ruleStatusesFilter: ['enabled'],
})
).toEqual([
'(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now))',
]);
).toEqual(['alert.attributes.enabled: true']);
expect(
mapFiltersToKql({
@ -60,7 +58,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['snoozed'],
})
).toEqual([
'((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)',
'(alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })',
]);
expect(
@ -68,7 +66,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['enabled', 'snoozed'],
})
).toEqual([
'(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)',
'alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })',
]);
expect(
@ -76,7 +74,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['disabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled: false or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)',
'alert.attributes.enabled: false and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })',
]);
expect(
@ -84,7 +82,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'],
})
).toEqual([
'(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or alert.attributes.enabled: false or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)',
'alert.attributes.enabled: true and alert.attributes.enabled: false and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })',
]);
});

View file

@ -43,8 +43,8 @@ export const mapFiltersToKql = ({
}
if (ruleStatusesFilter && ruleStatusesFilter.length) {
const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)`;
const enabledFilter = `(alert.attributes.enabled: true AND NOT ${snoozedFilter})`;
const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })`;
const enabledFilter = `alert.attributes.enabled: true`;
const disabledFilter = `alert.attributes.enabled: false`;
const result = [];
@ -58,10 +58,10 @@ export const mapFiltersToKql = ({
}
if (ruleStatusesFilter.includes('snoozed')) {
result.push(`(${snoozedFilter} AND NOT ${disabledFilter})`);
result.push(`${snoozedFilter}`);
}
filters.push(result.join(' or '));
filters.push(result.join(' and '));
}
if (tagsFilter && tagsFilter.length) {

View file

@ -64,13 +64,7 @@ describe('mapFiltersToKueryNode', () => {
ruleStatusesFilter: ['enabled'],
}) as KueryNode
)
).toEqual(
toElasticsearchQuery(
fromKueryExpression(
'(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now))'
)
)
);
).toEqual(toElasticsearchQuery(fromKueryExpression('alert.attributes.enabled: true')));
expect(
toElasticsearchQuery(
@ -89,7 +83,7 @@ describe('mapFiltersToKueryNode', () => {
).toEqual(
toElasticsearchQuery(
fromKueryExpression(
'((alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)'
'(alert.attributes.muteAll: true OR alert.attributes.snoozeSchedule: { duration > 0 })'
)
)
);
@ -103,8 +97,8 @@ describe('mapFiltersToKueryNode', () => {
).toEqual(
toElasticsearchQuery(
fromKueryExpression(
`(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now)) or
((alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)`
`alert.attributes.enabled: true and
(alert.attributes.muteAll: true OR alert.attributes.snoozeSchedule: { duration > 0 })`
)
)
);
@ -118,8 +112,8 @@ describe('mapFiltersToKueryNode', () => {
).toEqual(
toElasticsearchQuery(
fromKueryExpression(
`alert.attributes.enabled: false or
((alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)`
`alert.attributes.enabled: false and
(alert.attributes.muteAll: true OR alert.attributes.snoozeSchedule: { duration > 0 })`
)
)
);
@ -133,9 +127,9 @@ describe('mapFiltersToKueryNode', () => {
).toEqual(
toElasticsearchQuery(
fromKueryExpression(
`(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now)) or
alert.attributes.enabled: false or
((alert.attributes.muteAll: true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)`
`alert.attributes.enabled: true and
alert.attributes.enabled: false and
(alert.attributes.muteAll: true OR alert.attributes.snoozeSchedule: { duration > 0 })`
)
)
);

View file

@ -54,12 +54,9 @@ export const mapFiltersToKueryNode = ({
if (ruleStatusesFilter && ruleStatusesFilter.length) {
const snoozedFilter = nodeBuilder.or([
fromKueryExpression('alert.attributes.muteAll: true'),
nodeTypes.function.buildNode('range', 'alert.attributes.isSnoozedUntil', 'gt', 'now'),
]);
const enabledFilter = nodeBuilder.and([
fromKueryExpression('alert.attributes.enabled: true'),
nodeTypes.function.buildNode('not', snoozedFilter),
fromKueryExpression('alert.attributes.snoozeSchedule:{ duration > 0 }'),
]);
const enabledFilter = fromKueryExpression('alert.attributes.enabled: true');
const disabledFilter = fromKueryExpression('alert.attributes.enabled: false');
const ruleStatusesFilterKueryNode = [];
@ -73,11 +70,9 @@ export const mapFiltersToKueryNode = ({
}
if (ruleStatusesFilter.includes('snoozed')) {
ruleStatusesFilterKueryNode.push(
nodeBuilder.and([snoozedFilter, nodeTypes.function.buildNode('not', disabledFilter)])
);
ruleStatusesFilterKueryNode.push(snoozedFilter);
}
filterKueryNode.push(nodeBuilder.or(ruleStatusesFilterKueryNode));
filterKueryNode.push(nodeBuilder.and(ruleStatusesFilterKueryNode));
}
if (tagsFilter && tagsFilter.length) {

View file

@ -266,7 +266,7 @@ describe('loadRules', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)",
"filter": "alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })",
"page": 1,
"per_page": 10,
"search": undefined,
@ -324,7 +324,7 @@ describe('loadRules', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or alert.attributes.enabled: false or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)",
"filter": "alert.attributes.enabled: true and alert.attributes.enabled: false and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })",
"page": 1,
"per_page": 10,
"search": undefined,

View file

@ -166,7 +166,7 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.muteAllTitle"
defaultMessage="Mute"
defaultMessage="Snooze indefinitely"
/>
</EuiButtonEmpty>
</EuiFlexItem>
@ -181,7 +181,7 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.unmuteAllTitle"
defaultMessage="Unmute"
defaultMessage="Cancel snooze"
/>
</EuiButtonEmpty>
</EuiFlexItem>

View file

@ -6,21 +6,11 @@
*/
import React, { useState, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFilterButton,
EuiPopover,
EuiFilterGroup,
EuiSelectableListItem,
EuiButtonEmpty,
} from '@elastic/eui';
import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiSelectableListItem } from '@elastic/eui';
import { RuleStatus } from '../../../../types';
const statuses: RuleStatus[] = ['enabled', 'disabled', 'snoozed'];
const optionStyles = {
textTransform: 'capitalize' as const,
};
const getOptionDataTestSubj = (status: RuleStatus) => `ruleStatusFilterOption-${status}`;
export interface RuleStatusFilterProps {
@ -59,22 +49,29 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => {
setIsPopoverOpen((prevIsOpen) => !prevIsOpen);
}, [setIsPopoverOpen]);
const renderClearAll = () => {
return (
<div>
<EuiButtonEmpty
style={{
width: '100%',
}}
size="xs"
iconType="crossInACircleFilled"
color="danger"
onClick={() => onChange([])}
>
Clear all
</EuiButtonEmpty>
</div>
);
const renderRuleStateOptions = (status: 'enabled' | 'disabled' | 'snoozed') => {
if (status === 'enabled') {
return (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText"
defaultMessage="Rule is enabled"
/>
);
} else if (status === 'disabled') {
return (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText"
defaultMessage="Rule is disabled"
/>
);
} else if (status === 'snoozed') {
return (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText"
defaultMessage="Rule has snoozed"
/>
);
}
};
return (
@ -92,8 +89,8 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => {
onClick={onClick}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.ruleStatusFilterButton"
defaultMessage="View"
id="xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilterButton"
defaultMessage="Rule state"
/>
</EuiFilterButton>
}
@ -103,16 +100,14 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => {
return (
<EuiSelectableListItem
key={status}
style={optionStyles}
data-test-subj={optionDataTestSubj(status)}
onClick={onFilterItemClick(status)}
checked={selectedStatuses.includes(status) ? 'on' : undefined}
>
{status}
{renderRuleStateOptions(status)}
</EuiSelectableListItem>
);
})}
{renderClearAll()}
</div>
</EuiPopover>
</EuiFilterGroup>

View file

@ -61,6 +61,8 @@ const findTestUtils = (
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const match = response.body.data.find((obj: any) => obj.id === createdAlert.id);
const activeSnoozes = match.active_snoozes;
const hasActiveSnoozes = !!(activeSnoozes || []).filter((obj: any) => obj).length;
expect(match).to.eql({
id: createdAlert.id,
name: 'abc',
@ -86,7 +88,7 @@ const findTestUtils = (
? {
monitoring: match.monitoring,
snooze_schedule: match.snooze_schedule,
active_snoozes: match.active_snoozes,
...(hasActiveSnoozes && { active_snoozes: activeSnoozes }),
}
: {}),
});
@ -260,6 +262,8 @@ const findTestUtils = (
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const match = response.body.data.find((obj: any) => obj.id === createdAlert.id);
const activeSnoozes = match.active_snoozes;
const hasActiveSnoozes = !!(activeSnoozes || []).filter((obj: any) => obj).length;
expect(match).to.eql({
id: createdAlert.id,
name: 'abc',
@ -291,7 +295,7 @@ const findTestUtils = (
? {
monitoring: match.monitoring,
snooze_schedule: match.snooze_schedule,
active_snoozes: match.active_snoozes,
...(hasActiveSnoozes && { active_snoozes: activeSnoozes }),
}
: {}),
});
@ -368,11 +372,17 @@ const findTestUtils = (
id: createdAlert.id,
actions: [],
tags: [myTag],
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
expect(omit(matchSecond, 'updatedAt')).to.eql({
id: createdSecondAlert.id,
actions: [],
tags: [myTag],
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
break;
default:
@ -446,12 +456,18 @@ const findTestUtils = (
actions: [],
tags: [myTag],
execution_status: matchFirst.execution_status,
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
expect(omit(matchSecond, 'updatedAt')).to.eql({
id: createdSecondAlert.id,
actions: [],
tags: [myTag],
execution_status: matchSecond.execution_status,
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
break;
default:

View file

@ -85,6 +85,7 @@ const getTestUtils = (
? {
monitoring: response.body.monitoring,
snooze_schedule: response.body.snooze_schedule,
is_snoozed_until: response.body.is_snoozed_until,
}
: {}),
});

View file

@ -51,6 +51,8 @@ const findTestUtils = (
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const match = response.body.data.find((obj: any) => obj.id === createdAlert.id);
const activeSnoozes = match.active_snoozes;
const hasActiveSnoozes = !!(activeSnoozes || []).filter((obj: any) => obj).length;
expect(match).to.eql({
id: createdAlert.id,
name: 'abc',
@ -76,7 +78,7 @@ const findTestUtils = (
? {
monitoring: match.monitoring,
snooze_schedule: match.snooze_schedule,
active_snoozes: match.active_snoozes,
...(hasActiveSnoozes && { active_snoozes: activeSnoozes }),
}
: {}),
});

View file

@ -55,7 +55,11 @@ const getTestUtils = (
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
...(describeType === 'internal'
? { monitoring: response.body.monitoring, snooze_schedule: response.body.snooze_schedule }
? {
monitoring: response.body.monitoring,
snooze_schedule: response.body.snooze_schedule,
is_snoozed_until: response.body.is_snoozed_until,
}
: {}),
});
expect(Date.parse(response.body.created_at)).to.be.greaterThan(0);

View file

@ -612,7 +612,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('ruleStatusFilterButton');
await testSubjects.click('ruleStatusFilterOption-enabled');
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(1);
await assertRulesLength(2);
// Select disabled
await testSubjects.click('ruleStatusFilterOption-enabled');
@ -624,17 +624,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('ruleStatusFilterOption-disabled');
await testSubjects.click('ruleStatusFilterOption-snoozed');
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(1);
await assertRulesLength(2);
// Select disabled and snoozed
await testSubjects.click('ruleStatusFilterOption-disabled');
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(3);
await assertRulesLength(1);
// Select all 4
await testSubjects.click('ruleStatusFilterOption-enabled');
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(4);
await assertRulesLength(0);
});
it('should filter alerts by the tag', async () => {