[RAM] Add data model for scheduled and recurring snoozes (#131019)

* [RAM] Add data model for scheduled and recurring snoozes

* Update migration tests

* Make snoozeIndefinitely required

* Fix remaining muteAlls

* Replace snoozeEndTime with snoozeSchedule

* Fix typecheck

* Fix typecheck

* Revert muteAll => snoozeIndefinitely rename

* Revert more snoozeIndefinitely refs

* Revert README

* Restore updated taskrunner test

* Fix RuleStatusDropdown test

* Add timeZone to SO

* Update timezone usage

* Implement RRule

* Fix task runner test

* Add rrule types

* Push snoozeEndTime from server and fix unsnooze

* Fix Jest Tests 5

* Fix rulestatusdropdown test

* Fix jest tests 1

* Fix snooze_end_time refs in functional tests

* Fix snooze API integration tests

* Move isRuleSnoozed to server

* Update x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts

Co-authored-by: Patrick Mueller <pmuellr@gmail.com>

* Require timeZone in rulesnooze

* Add automatic isSnoozedUntil savedobject flag

* Check isSnoozedUntil against now

* Fix jest

* Fix typecheck

* Fix jest

* Fix snoozedUntil date parsing

* Fix rewriterule

* Add error handling for RRule

* Fix re-snoozing

* Add comments to rulesnoozetype

* Restructure data model to use rRule for everything

* Fix functional tests

* Fix jest

* Fix functional tests

* Fix functional tests

* Fix functional tests

* Clarify isRuleSnoozed

Co-authored-by: Patrick Mueller <pmuellr@gmail.com>
This commit is contained in:
Zacqary Adam Xeper 2022-05-18 12:07:54 -05:00 committed by GitHub
parent fa7df7983c
commit 64689c0f9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 878 additions and 144 deletions

View file

@ -220,6 +220,7 @@
"@types/mapbox__vector-tile": "1.3.0",
"@types/moment-duration-format": "^2.2.3",
"@types/react-is": "^16.7.1",
"@types/rrule": "^2.2.9",
"JSONStream": "1.3.5",
"abort-controller": "^3.0.0",
"antlr4ts": "^0.5.0-alpha.3",
@ -309,6 +310,7 @@
"loader-utils": "^1.2.3",
"lodash": "^4.17.21",
"lru-cache": "^4.1.5",
"luxon": "^2.3.2",
"lz-string": "^1.4.4",
"mapbox-gl-draw-rectangle-mode": "1.0.4",
"maplibre-gl": "2.1.9",
@ -405,6 +407,7 @@
"reselect": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",
"rison-node": "1.0.2",
"rrule": "2.6.4",
"rxjs": "^7.5.5",
"safe-squel": "^5.12.5",
"seedrandom": "^3.0.5",

View file

@ -21,6 +21,7 @@ export * from './disabled_action_groups';
export * from './rule_notify_when_type';
export * from './parse_duration';
export * from './execution_log_types';
export * from './rule_snooze_type';
export interface AlertingFrameworkHealth {
isSufficientlySecure: boolean;

View file

@ -12,6 +12,7 @@ import {
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '@kbn/core/server';
import { RuleNotifyWhenType } from './rule_notify_when_type';
import { RuleSnooze } from './rule_snooze_type';
export type RuleTypeState = Record<string, unknown>;
export type RuleTypeParams = Record<string, unknown>;
@ -104,12 +105,13 @@ export interface Rule<Params extends RuleTypeParams = never> {
apiKey: string | null;
apiKeyOwner: string | null;
throttle: string | null;
notifyWhen: RuleNotifyWhenType | null;
muteAll: boolean;
notifyWhen: RuleNotifyWhenType | null;
mutedInstanceIds: string[];
executionStatus: RuleExecutionStatus;
monitoring?: RuleMonitoring;
snoozeEndTime?: Date | null; // Remove ? when this parameter is made available in the public API
snoozeSchedule?: RuleSnooze; // Remove ? when this parameter is made available in the public API
isSnoozedUntil?: Date | null;
}
export type SanitizedRule<Params extends RuleTypeParams = never> = Omit<Rule<Params>, 'apiKey'>;

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { WeekdayStr } from 'rrule';
export type RuleSnooze = Array<{
duration: number;
rRule: Partial<RRuleRecord> & Pick<RRuleRecord, 'dtstart' | 'tzid'>;
// For scheduled/recurring snoozes, `id` uniquely identifies them so that they can be displayed, modified, and deleted individually
id?: string;
}>;
// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec
export interface RRuleRecord {
dtstart: string;
tzid: string;
freq?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
until?: string;
count?: number;
interval?: number;
wkst?: WeekdayStr;
byweekday?: Array<string | number>;
bymonth?: number[];
bysetpos?: number[];
bymonthday: number;
byyearday: number[];
byweekno: number[];
byhour: number[];
byminute: number[];
bysecond: number[];
}

View file

@ -27,4 +27,5 @@ export {
} from './rule_execution_status';
export { getRecoveredAlerts } from './get_recovered_alerts';
export { createWrappedScopedClusterClientFactory } from './wrap_scoped_cluster_client';
export { isRuleSnoozed, getRuleSnoozeEndTime } from './is_rule_snoozed';
export { convertRuleIdsToKueryNode } from './convert_rule_ids_to_kuery_node';

View file

@ -0,0 +1,319 @@
/*
* 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 sinon from 'sinon';
import { RRule } from 'rrule';
import { isRuleSnoozed } from './is_rule_snoozed';
import { RRuleRecord } from '../types';
const DATE_9999 = '9999-12-31T12:34:56.789Z';
const DATE_1970 = '1970-01-01T00:00:00.000Z';
const DATE_2019 = '2019-01-01T00:00:00.000Z';
const DATE_2019_PLUS_6_HOURS = '2019-01-01T06:00:00.000Z';
const DATE_2020 = '2020-01-01T00:00:00.000Z';
const DATE_2020_MINUS_1_HOUR = '2019-12-31T23:00:00.000Z';
const DATE_2020_MINUS_1_MONTH = '2019-12-01T00:00:00.000Z';
const NOW = DATE_2020;
let fakeTimer: sinon.SinonFakeTimers;
describe('isRuleSnoozed', () => {
beforeAll(() => {
fakeTimer = sinon.useFakeTimers(new Date(NOW));
});
afterAll(() => fakeTimer.restore());
test('returns false when snooze has not yet started', () => {
const snoozeSchedule = [
{
duration: 100000000,
rRule: {
dtstart: DATE_9999,
tzid: 'UTC',
count: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule, muteAll: false })).toBe(false);
});
test('returns true when snooze has started', () => {
const snoozeSchedule = [
{
duration: 100000000,
rRule: {
dtstart: NOW,
tzid: 'UTC',
count: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule, muteAll: false })).toBe(true);
});
test('returns false when snooze has ended', () => {
const snoozeSchedule = [
{
duration: 100000000,
rRule: {
dtstart: DATE_2019,
tzid: 'UTC',
count: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule, muteAll: false })).toBe(false);
});
test('returns true when snooze is indefinite', () => {
const snoozeSchedule = [
{
duration: 100000000,
rRule: {
dtstart: DATE_9999,
tzid: 'UTC',
count: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule, muteAll: true })).toBe(true);
});
test('returns as expected for an indefinitely recurring snooze', () => {
const snoozeScheduleA = [
{
duration: 60 * 1000,
rRule: {
dtstart: DATE_2019,
tzid: 'UTC',
freq: RRule.DAILY,
interval: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true);
const snoozeScheduleB = [
{
duration: 60 * 1000,
rRule: {
dtstart: DATE_2019_PLUS_6_HOURS,
tzid: 'UTC',
freq: RRule.DAILY,
interval: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false);
const snoozeScheduleC = [
{
duration: 60 * 1000,
rRule: {
dtstart: DATE_2020_MINUS_1_HOUR,
tzid: 'UTC',
freq: RRule.HOURLY,
interval: 1,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleC, muteAll: false })).toBe(true);
});
test('returns as expected for a recurring snooze with limited occurrences', () => {
const snoozeScheduleA = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.HOURLY,
interval: 1,
tzid: 'UTC',
count: 8761,
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true);
const snoozeScheduleB = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.HOURLY,
interval: 1,
tzid: 'UTC',
count: 25,
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false);
const snoozeScheduleC = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.YEARLY,
interval: 1,
tzid: 'UTC',
count: 60,
dtstart: DATE_1970,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleC, muteAll: false })).toBe(true);
});
test('returns as expected for a recurring snooze with an end date', () => {
const snoozeScheduleA = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.HOURLY,
interval: 1,
tzid: 'UTC',
until: DATE_9999,
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true);
const snoozeScheduleB = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.HOURLY,
interval: 1,
tzid: 'UTC',
until: DATE_2020_MINUS_1_HOUR,
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false);
});
test('returns as expected for a recurring snooze on a day of the week', () => {
const snoozeScheduleA = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.WEEKLY,
interval: 1,
tzid: 'UTC',
byweekday: ['MO', 'WE', 'FR'], // Jan 1 2020 was a Wednesday
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true);
const snoozeScheduleB = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.WEEKLY,
interval: 1,
tzid: 'UTC',
byweekday: ['TU', 'TH', 'SA', 'SU'],
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false);
const snoozeScheduleC = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.WEEKLY,
interval: 1,
tzid: 'UTC',
byweekday: ['MO', 'WE', 'FR'],
count: 12,
dtstart: DATE_2020_MINUS_1_MONTH,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleC, muteAll: false })).toBe(false);
const snoozeScheduleD = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.WEEKLY,
interval: 1,
tzid: 'UTC',
byweekday: ['MO', 'WE', 'FR'],
count: 15,
dtstart: DATE_2020_MINUS_1_MONTH,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleD, muteAll: false })).toBe(true);
});
test('returns as expected for a recurring snooze on an nth day of the week of a month', () => {
const snoozeScheduleA = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.MONTHLY,
interval: 1,
tzid: 'UTC',
byweekday: ['+1WE'], // Jan 1 2020 was the first Wednesday of the month
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true);
const snoozeScheduleB = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.MONTHLY,
interval: 1,
tzid: 'UTC',
byweekday: ['+2WE'],
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false);
});
test('using a timezone, returns as expected for a recurring snooze on a day of the week', () => {
const snoozeScheduleA = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.WEEKLY,
interval: 1,
byweekday: ['WE'],
tzid: 'Asia/Taipei',
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(false);
const snoozeScheduleB = [
{
duration: 60 * 1000,
rRule: {
freq: RRule.WEEKLY,
interval: 1,
byweekday: ['WE'],
byhour: [0],
byminute: [0],
tzid: 'UTC',
dtstart: DATE_2019,
} as RRuleRecord,
},
];
expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(true);
});
});

View file

@ -0,0 +1,63 @@
/*
* 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 { RRule, ByWeekday, Weekday, rrulestr } from 'rrule';
import { SanitizedRule, RuleTypeParams } from '../../common/rule';
type RuleSnoozeProps = Pick<SanitizedRule<RuleTypeParams>, 'muteAll' | 'snoozeSchedule'>;
export function getRuleSnoozeEndTime(rule: RuleSnoozeProps): Date | null {
if (rule.snoozeSchedule == null) {
return null;
}
const now = Date.now();
for (const snooze of rule.snoozeSchedule) {
const { duration, rRule } = snooze;
const startTimeMS = Date.parse(rRule.dtstart);
const initialEndTime = startTimeMS + duration;
// If now is during the first occurrence of the snooze
if (now >= startTimeMS && now < initialEndTime) return new Date(initialEndTime);
// Check to see if now is during a recurrence of the snooze
if (rRule) {
try {
const rRuleOptions = {
...rRule,
dtstart: new Date(rRule.dtstart),
until: rRule.until ? new Date(rRule.until) : null,
wkst: rRule.wkst ? Weekday.fromStr(rRule.wkst) : null,
byweekday: rRule.byweekday ? parseByWeekday(rRule.byweekday) : null,
};
const recurrenceRule = new RRule(rRuleOptions);
const lastOccurrence = recurrenceRule.before(new Date(now), true);
if (!lastOccurrence) continue;
const lastOccurrenceEndTime = lastOccurrence.getTime() + duration;
if (now < lastOccurrenceEndTime) return new Date(lastOccurrenceEndTime);
} catch (e) {
throw new Error(`Failed to process RRule ${rRule}: ${e}`);
}
}
}
return null;
}
export function isRuleSnoozed(rule: RuleSnoozeProps) {
if (rule.muteAll) {
return true;
}
return Boolean(getRuleSnoozeEndTime(rule));
}
function parseByWeekday(byweekday: Array<string | number>): ByWeekday[] {
const rRuleString = `RRULE:BYDAY=${byweekday.join(',')}`;
const parsedRRule = rrulestr(rRuleString);
return parsedRRule.origOptions.byweekday as ByWeekday[];
}

View file

@ -67,12 +67,14 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
notifyWhen,
muteAll,
mutedInstanceIds,
snoozeSchedule,
executionStatus: { lastExecutionDate, lastDuration, ...executionStatus },
...rest
}) => ({
...rest,
rule_type_id: alertTypeId,
scheduled_task_id: scheduledTaskId,
snooze_schedule: snoozeSchedule,
created_by: createdBy,
updated_by: updatedBy,
created_at: createdAt,

View file

@ -35,7 +35,8 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
executionStatus,
actions,
scheduledTaskId,
snoozeEndTime,
snoozeSchedule,
isSnoozedUntil,
...rest
}) => ({
...rest,
@ -46,10 +47,10 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
updated_at: updatedAt,
api_key_owner: apiKeyOwner,
notify_when: notifyWhen,
mute_all: muteAll,
muted_alert_ids: mutedInstanceIds,
// Remove this object spread boolean check after snoozeEndTime is added to the public API
...(snoozeEndTime !== undefined ? { snooze_end_time: snoozeEndTime } : {}),
mute_all: muteAll,
...(isSnoozedUntil !== undefined ? { is_snoozed_until: isSnoozedUntil } : {}),
snooze_schedule: snoozeSchedule,
scheduled_task_id: scheduledTaskId,
execution_status: executionStatus && {
...omit(executionStatus, 'lastExecutionDate', 'lastDuration'),

View file

@ -21,7 +21,8 @@ export const rewriteRule = ({
executionStatus,
actions,
scheduledTaskId,
snoozeEndTime,
snoozeSchedule,
isSnoozedUntil,
...rest
}: SanitizedRule<RuleTypeParams>) => ({
...rest,
@ -35,8 +36,8 @@ export const rewriteRule = ({
mute_all: muteAll,
muted_alert_ids: mutedInstanceIds,
scheduled_task_id: scheduledTaskId,
// Remove this object spread boolean check after snoozeEndTime is added to the public API
...(snoozeEndTime !== undefined ? { snooze_end_time: snoozeEndTime } : {}),
snooze_schedule: snoozeSchedule,
...(isSnoozedUntil != null ? { is_snoozed_until: isSnoozedUntil } : {}),
execution_status: executionStatus && {
...omit(executionStatus, 'lastExecutionDate', 'lastDuration'),
last_execution_date: executionStatus.lastExecutionDate,

View file

@ -70,12 +70,16 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
muteAll,
mutedInstanceIds,
executionStatus,
snoozeSchedule,
isSnoozedUntil,
...rest
}) => ({
...rest,
api_key_owner: apiKeyOwner,
created_by: createdBy,
updated_by: updatedBy,
snooze_schedule: snoozeSchedule,
...(isSnoozedUntil ? { is_snoozed_until: isSnoozedUntil } : {}),
...(alertTypeId ? { rule_type_id: alertTypeId } : {}),
...(scheduledTaskId ? { scheduled_task_id: scheduledTaskId } : {}),
...(createdAt ? { created_at: createdAt } : {}),

View file

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

View file

@ -54,6 +54,7 @@ import {
RuleWithLegacyId,
SanitizedRuleWithLegacyId,
PartialRuleWithLegacyId,
RuleSnooze,
RawAlertInstance as RawAlert,
} from '../types';
import {
@ -62,6 +63,7 @@ import {
getRuleNotifyWhenType,
validateMutatedRuleTypeParams,
convertRuleIdsToKueryNode,
getRuleSnoozeEndTime,
} from '../lib';
import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance';
import { RegistryRuleType, UntypedNormalizedRuleType } from '../rule_type_registry';
@ -310,7 +312,8 @@ export interface CreateOptions<Params extends RuleTypeParams> {
| 'mutedInstanceIds'
| 'actions'
| 'executionStatus'
| 'snoozeEndTime'
| 'snoozeSchedule'
| 'isSnoozedUntil'
> & { actions: NormalizedAlertAction[] };
options?: {
id?: string;
@ -391,7 +394,7 @@ export class RulesClient {
private readonly fieldsToExcludeFromPublicApi: Array<keyof SanitizedRule> = [
'monitoring',
'mapped_params',
'snoozeEndTime',
'snoozeSchedule',
];
constructor({
@ -504,7 +507,8 @@ export class RulesClient {
updatedBy: username,
createdAt: new Date(createTime).toISOString(),
updatedAt: new Date(createTime).toISOString(),
snoozeEndTime: null,
isSnoozedUntil: null,
snoozeSchedule: [],
params: updatedParams as RawRule['params'],
muteAll: false,
mutedInstanceIds: [],
@ -1018,7 +1022,7 @@ export class RulesClient {
},
snoozed: {
date_range: {
field: 'alert.attributes.snoozeEndTime',
field: 'alert.attributes.snoozeSchedule.rRule.dtstart',
format: 'strict_date_time',
ranges: [{ from: 'now' }],
},
@ -2120,8 +2124,21 @@ export class RulesClient {
// If snoozeEndTime is -1, instead mute all
const newAttrs =
snoozeEndTime === -1
? { muteAll: true, snoozeEndTime: null }
: { snoozeEndTime: new Date(snoozeEndTime).toISOString(), muteAll: false };
? {
muteAll: true,
snoozeSchedule: clearUnscheduledSnooze(attributes),
}
: {
snoozeSchedule: clearUnscheduledSnooze(attributes).concat({
duration: Date.parse(snoozeEndTime) - Date.now(),
rRule: {
dtstart: new Date().toISOString(),
tzid: 'UTC',
count: 1,
},
}),
muteAll: false,
};
const updateAttributes = this.updateMeta({
...newAttrs,
@ -2135,7 +2152,7 @@ export class RulesClient {
id,
updateAttributes,
updateOptions
);
).then(() => this.updateSnoozedUntilTime({ id }));
}
public async unsnooze({ id }: { id: string }): Promise<void> {
@ -2185,7 +2202,7 @@ export class RulesClient {
this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
const updateAttributes = this.updateMeta({
snoozeEndTime: null,
snoozeSchedule: clearUnscheduledSnooze(attributes),
muteAll: false,
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
@ -2200,6 +2217,30 @@ export class RulesClient {
);
}
public async updateSnoozedUntilTime({ id }: { id: string }): Promise<void> {
const { attributes, version } = await this.unsecuredSavedObjectsClient.get<RawRule>(
'alert',
id
);
const isSnoozedUntil = getRuleSnoozeEndTime(attributes);
if (!isSnoozedUntil) return;
const updateAttributes = this.updateMeta({
isSnoozedUntil: isSnoozedUntil.toISOString(),
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
await partiallyUpdateAlert(
this.unsecuredSavedObjectsClient,
id,
updateAttributes,
updateOptions
);
}
public async muteAll({ id }: { id: string }): Promise<void> {
return await retryIfConflicts(
this.logger,
@ -2249,7 +2290,7 @@ export class RulesClient {
const updateAttributes = this.updateMeta({
muteAll: true,
mutedInstanceIds: [],
snoozeEndTime: null,
snoozeSchedule: clearUnscheduledSnooze(attributes),
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
@ -2312,7 +2353,7 @@ export class RulesClient {
const updateAttributes = this.updateMeta({
muteAll: false,
mutedInstanceIds: [],
snoozeEndTime: null,
snoozeSchedule: clearUnscheduledSnooze(attributes),
updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
@ -2560,15 +2601,23 @@ export class RulesClient {
executionStatus,
schedule,
actions,
snoozeEndTime,
snoozeSchedule,
isSnoozedUntil,
...partialRawRule
}: Partial<RawRule>,
references: SavedObjectReference[] | undefined,
includeLegacyId: boolean = false,
excludeFromPublicApi: boolean = false
): PartialRule<Params> | PartialRuleWithLegacyId<Params> {
const snoozeEndTimeDate = snoozeEndTime != null ? new Date(snoozeEndTime) : snoozeEndTime;
const includeSnoozeEndTime = snoozeEndTimeDate !== undefined && !excludeFromPublicApi;
const snoozeScheduleDates = snoozeSchedule?.map((s) => ({
...s,
rRule: {
...s.rRule,
dtstart: new Date(s.rRule.dtstart),
...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}),
},
}));
const includeSnoozeSchedule = snoozeSchedule !== undefined;
const rule = {
id,
notifyWhen,
@ -2578,9 +2627,10 @@ export class RulesClient {
schedule: schedule as IntervalSchedule,
actions: actions ? this.injectReferencesIntoActions(id, actions, references || []) : [],
params: this.injectReferencesIntoParams(id, ruleType, params, references || []) as Params,
...(includeSnoozeEndTime ? { snoozeEndTime: snoozeEndTimeDate } : {}),
...(includeSnoozeSchedule ? { snoozeSchedule: snoozeScheduleDates } : {}),
...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}),
...(createdAt ? { createdAt: new Date(createdAt) } : {}),
...(isSnoozedUntil ? { isSnoozedUntil: new Date(isSnoozedUntil) } : {}),
...(scheduledTaskId ? { scheduledTaskId } : {}),
...(executionStatus
? { executionStatus: ruleExecutionStatusFromRaw(this.logger, id, executionStatus) }
@ -2795,3 +2845,9 @@ function parseDate(dateString: string | undefined, propertyName: string, default
return parsedDate;
}
function clearUnscheduledSnooze(attributes: { snoozeSchedule?: RuleSnooze }) {
return attributes.snoozeSchedule
? attributes.snoozeSchedule.filter((s) => typeof s.id !== 'undefined')
: [];
}

View file

@ -203,7 +203,7 @@ describe('aggregate()', () => {
},
snoozed: {
date_range: {
field: 'alert.attributes.snoozeEndTime',
field: 'alert.attributes.snoozeSchedule.rRule.dtstart',
format: 'strict_date_time',
ranges: [{ from: 'now' }],
},
@ -240,7 +240,7 @@ describe('aggregate()', () => {
},
snoozed: {
date_range: {
field: 'alert.attributes.snoozeEndTime',
field: 'alert.attributes.snoozeSchedule.rRule.dtstart',
format: 'strict_date_time',
ranges: [{ from: 'now' }],
},

View file

@ -300,7 +300,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
actions: [
{
@ -376,6 +376,7 @@ describe('create()', () => {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -412,6 +413,7 @@ describe('create()', () => {
"status": "pending",
"warning": null,
},
"isSnoozedUntil": null,
"legacyId": null,
"meta": Object {
"versionApiKeyLastmodified": "v8.0.0",
@ -434,7 +436,7 @@ describe('create()', () => {
"schedule": Object {
"interval": "1m",
},
"snoozeEndTime": null,
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -506,7 +508,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
actions: [
{
@ -566,7 +568,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
actions: [
{
@ -618,6 +620,7 @@ describe('create()', () => {
"status": "pending",
"warning": null,
},
"isSnoozedUntil": null,
"legacyId": "123",
"meta": Object {
"versionApiKeyLastmodified": "v7.10.0",
@ -640,7 +643,7 @@ describe('create()', () => {
"schedule": Object {
"interval": "1m",
},
"snoozeEndTime": null,
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -1044,6 +1047,7 @@ describe('create()', () => {
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
isSnoozedUntil: null,
legacyId: null,
executionStatus: {
error: null,
@ -1054,7 +1058,7 @@ describe('create()', () => {
monitoring: getDefaultRuleMonitoring(),
meta: { versionApiKeyLastmodified: kibanaVersion },
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
name: 'abc',
notifyWhen: 'onActiveAlert',
@ -1243,6 +1247,7 @@ describe('create()', () => {
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
isSnoozedUntil: null,
legacyId: null,
executionStatus: {
error: null,
@ -1253,7 +1258,7 @@ describe('create()', () => {
monitoring: getDefaultRuleMonitoring(),
meta: { versionApiKeyLastmodified: kibanaVersion },
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
name: 'abc',
notifyWhen: 'onActiveAlert',
@ -1407,6 +1412,7 @@ describe('create()', () => {
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
isSnoozedUntil: null,
legacyId: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
@ -1421,7 +1427,7 @@ describe('create()', () => {
monitoring: getDefaultRuleMonitoring(),
meta: { versionApiKeyLastmodified: kibanaVersion },
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
name: 'abc',
notifyWhen: 'onActiveAlert',
@ -1530,7 +1536,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
notifyWhen: 'onActionGroupChange',
actions: [
@ -1571,6 +1577,7 @@ describe('create()', () => {
alertTypeId: '123',
consumer: 'bar',
name: 'abc',
isSnoozedUntil: null,
legacyId: null,
params: { bar: true },
apiKey: null,
@ -1587,7 +1594,7 @@ describe('create()', () => {
throttle: '10m',
notifyWhen: 'onActionGroupChange',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
@ -1638,6 +1645,7 @@ describe('create()', () => {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -1662,7 +1670,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
notifyWhen: 'onThrottleInterval',
actions: [
@ -1700,6 +1708,7 @@ describe('create()', () => {
params: { foo: true },
},
],
isSnoozedUntil: null,
legacyId: null,
alertTypeId: '123',
consumer: 'bar',
@ -1719,7 +1728,7 @@ describe('create()', () => {
throttle: '10m',
notifyWhen: 'onThrottleInterval',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
@ -1770,6 +1779,7 @@ describe('create()', () => {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -1794,7 +1804,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
notifyWhen: 'onActiveAlert',
actions: [
@ -1832,6 +1842,7 @@ describe('create()', () => {
params: { foo: true },
},
],
isSnoozedUntil: null,
legacyId: null,
alertTypeId: '123',
consumer: 'bar',
@ -1851,7 +1862,7 @@ describe('create()', () => {
throttle: null,
notifyWhen: 'onActiveAlert',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
@ -1902,6 +1913,7 @@ describe('create()', () => {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -1935,7 +1947,7 @@ describe('create()', () => {
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
actions: [
{
@ -1993,13 +2005,14 @@ describe('create()', () => {
],
apiKeyOwner: null,
apiKey: null,
isSnoozedUntil: null,
legacyId: null,
createdBy: 'elastic',
updatedBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
executionStatus: {
status: 'pending',
@ -2066,6 +2079,7 @@ describe('create()', () => {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"snoozeSchedule": Array [],
"tags": Array [
"foo",
],
@ -2345,6 +2359,7 @@ describe('create()', () => {
alertTypeId: '123',
consumer: 'bar',
name: 'abc',
isSnoozedUntil: null,
legacyId: null,
params: { bar: true },
apiKey: Buffer.from('123:abc').toString('base64'),
@ -2361,7 +2376,7 @@ describe('create()', () => {
throttle: null,
notifyWhen: 'onActiveAlert',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
@ -2444,6 +2459,7 @@ describe('create()', () => {
params: { foo: true },
},
],
isSnoozedUntil: null,
legacyId: null,
alertTypeId: '123',
consumer: 'bar',
@ -2463,7 +2479,7 @@ describe('create()', () => {
throttle: null,
notifyWhen: 'onActiveAlert',
muteAll: false,
snoozeEndTime: null,
snoozeSchedule: [],
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {

View file

@ -82,7 +82,7 @@ describe('muteAll()', () => {
{
muteAll: true,
mutedInstanceIds: [],
snoozeEndTime: null,
snoozeSchedule: [],
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},

View file

@ -82,7 +82,7 @@ describe('unmuteAll()', () => {
{
muteAll: false,
mutedInstanceIds: [],
snoozeEndTime: null,
snoozeSchedule: [],
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},

View file

@ -30,7 +30,8 @@ export const AlertAttributesExcludedFromAAD = [
'updatedAt',
'executionStatus',
'monitoring',
'snoozeEndTime',
'snoozeSchedule',
'isSnoozedUntil',
];
// useful for Pick<RawAlert, AlertAttributesExcludedFromAADType> which is a
@ -45,7 +46,8 @@ export type AlertAttributesExcludedFromAADType =
| 'updatedAt'
| 'executionStatus'
| 'monitoring'
| 'snoozeEndTime';
| 'snoozeSchedule'
| 'isSnoozedUntil';
export function setupSavedObjects(
savedObjects: SavedObjectsServiceSetup,

View file

@ -185,7 +185,73 @@ export const alertMappings: SavedObjectsTypeMappingDefinition = {
},
},
},
snoozeEndTime: {
snoozeSchedule: {
type: 'nested',
properties: {
id: {
type: 'keyword',
},
duration: {
type: 'long',
},
rRule: {
type: 'nested',
properties: {
freq: {
type: 'keyword',
},
dtstart: {
type: 'date',
format: 'strict_date_time',
},
tzid: {
type: 'keyword',
},
until: {
type: 'date',
format: 'strict_date_time',
},
count: {
type: 'long',
},
interval: {
type: 'long',
},
wkst: {
type: 'keyword',
},
byweekday: {
type: 'keyword',
},
bymonth: {
type: 'short',
},
bysetpos: {
type: 'long',
},
bymonthday: {
type: 'short',
},
byyearday: {
type: 'short',
},
byweekno: {
type: 'short',
},
byhour: {
type: 'long',
},
byminute: {
type: 'long',
},
bysecond: {
type: 'long',
},
},
},
},
},
isSnoozedUntil: {
type: 'date',
format: 'strict_date_time',
},

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import sinon from 'sinon';
import uuid from 'uuid';
import { getMigrations, isAnyActionSupportIncidents } from './migrations';
import { RawRule } from '../types';
@ -2318,6 +2319,27 @@ describe('successful migrations', () => {
});
describe('8.3.0', () => {
test('migrates snoozed rules to the new data model', () => {
const fakeTimer = sinon.useFakeTimers();
const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[
'8.3.0'
];
const mutedAlert = getMockData(
{
snoozeEndTime: '1970-01-02T00:00:00.000Z',
},
true
);
const migratedMutedAlert830 = migration830(mutedAlert, migrationContext);
expect(migratedMutedAlert830.attributes.snoozeSchedule.length).toEqual(1);
expect(migratedMutedAlert830.attributes.snoozeSchedule[0].rRule.dtstart).toEqual(
'1970-01-01T00:00:00.000Z'
);
expect(migratedMutedAlert830.attributes.snoozeSchedule[0].duration).toEqual(86400000);
fakeTimer.restore();
});
test('migrates es_query alert params', () => {
const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[
'8.3.0'

View file

@ -7,6 +7,8 @@
import { isRuleType, ruleTypeMappings } from '@kbn/securitysolution-rules';
import { isString } from 'lodash/fp';
import { omit } from 'lodash';
import moment from 'moment-timezone';
import { gte } from 'semver';
import {
LogMeta,
@ -164,7 +166,7 @@ export function getMigrations(
const migrationRules830 = createEsoMigration(
encryptedSavedObjects,
(doc: SavedObjectUnsanitizedDoc<RawRule>): doc is SavedObjectUnsanitizedDoc<RawRule> => true,
pipeMigrations(addSearchType, removeInternalTags)
pipeMigrations(addSearchType, removeInternalTags, convertSnoozes)
);
return mergeSavedObjectMigrationMaps(
@ -888,6 +890,33 @@ function addMappedParams(
return doc;
}
function convertSnoozes(
doc: SavedObjectUnsanitizedDoc<RawRule>
): SavedObjectUnsanitizedDoc<RawRule> {
const {
attributes: { snoozeEndTime },
} = doc;
return {
...doc,
attributes: {
...(omit(doc.attributes, ['snoozeEndTime']) as RawRule),
snoozeSchedule: snoozeEndTime
? [
{
duration: Date.parse(snoozeEndTime as string) - Date.now(),
rRule: {
dtstart: new Date().toISOString(),
tzid: moment.tz.guess(),
count: 1,
},
},
]
: [],
},
};
}
function getCorrespondingAction(
actions: SavedObjectAttribute,
connectorRef: string

View file

@ -470,17 +470,42 @@ describe('Task Runner', () => {
const snoozeTestParams: SnoozeTestParams[] = [
[false, null, false],
[false, undefined, false],
[false, DATE_1970, false],
[false, DATE_9999, true],
// Stringify the snooze schedules for better failure reporting
[
false,
JSON.stringify([
{ rRule: { dtstart: DATE_9999, tzid: 'UTC', count: 1 }, duration: 100000000 },
]),
false,
],
[
false,
JSON.stringify([
{ rRule: { dtstart: DATE_1970, tzid: 'UTC', count: 1 }, duration: 100000000 },
]),
true,
],
[true, null, true],
[true, undefined, true],
[true, DATE_1970, true],
[true, DATE_9999, true],
[
true,
JSON.stringify([
{ rRule: { dtstart: DATE_9999, tzid: 'UTC', count: 1 }, duration: 100000000 },
]),
true,
],
[
true,
JSON.stringify([
{ rRule: { dtstart: DATE_1970, tzid: 'UTC', count: 1 }, duration: 100000000 },
]),
true,
],
];
test.each(snoozeTestParams)(
'snoozing works as expected with muteAll: %s; snoozeEndTime: %s',
async (muteAll, snoozeEndTime, shouldBeSnoozed) => {
'snoozing works as expected with muteAll: %s; snoozeSchedule: %s',
async (muteAll, snoozeSchedule, shouldBeSnoozed) => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
ruleType.executor.mockImplementation(
@ -507,7 +532,7 @@ describe('Task Runner', () => {
rulesClient.get.mockResolvedValue({
...mockedRuleTypeSavedObject,
muteAll,
snoozeEndTime: snoozeEndTime != null ? new Date(snoozeEndTime) : snoozeEndTime,
snoozeSchedule: snoozeSchedule != null ? JSON.parse(snoozeSchedule) : [],
});
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT);
await taskRunner.run();

View file

@ -25,6 +25,7 @@ import {
getRecoveredAlerts,
ruleExecutionStatusToRaw,
validateRuleTypeParams,
isRuleSnoozed,
} from '../lib';
import {
Rule,
@ -247,18 +248,6 @@ export class TaskRunner<
}
}
private isRuleSnoozed(rule: SanitizedRule<Params>): boolean {
if (rule.muteAll) {
return true;
}
if (rule.snoozeEndTime == null) {
return false;
}
return Date.now() < rule.snoozeEndTime.getTime();
}
private shouldLogAndScheduleActionsForAlerts() {
// if execution hasn't been cancelled, return true
if (!this.cancelled) {
@ -477,7 +466,10 @@ export class TaskRunner<
});
}
const ruleIsSnoozed = this.isRuleSnoozed(rule);
const ruleIsSnoozed = isRuleSnoozed(rule);
if (ruleIsSnoozed) {
this.markRuleAsSnoozed(rule.id);
}
if (!ruleIsSnoozed && this.shouldLogAndScheduleActionsForAlerts()) {
const mutedAlertIdsSet = new Set(mutedInstanceIds);
@ -580,6 +572,23 @@ export class TaskRunner<
return this.executeRule(fakeRequest, rule, validatedParams, executionHandler, spaceId);
}
private async markRuleAsSnoozed(id: string) {
let apiKey: string | null;
const {
params: { alertId: ruleId, spaceId },
} = this.taskInstance;
try {
const decryptedAttributes = await this.getDecryptedAttributes(ruleId, spaceId);
apiKey = decryptedAttributes.apiKey;
} catch (err) {
throw new ErrorWithReason(RuleExecutionStatusErrorReasons.Decrypt, err);
}
const fakeRequest = this.getFakeKibanaRequest(spaceId, apiKey);
const rulesClient = this.context.getRulesClientWithRequest(fakeRequest);
await rulesClient.updateSnoozedUntilTime({ id });
}
private async loadRuleAttributesAndRun(): Promise<Resultable<RuleRunResult, Error>> {
const {
params: { alertId: ruleId, spaceId },

View file

@ -40,6 +40,7 @@ import {
SanitizedRuleConfig,
RuleMonitoring,
MappedParams,
RuleSnooze,
} from '../common';
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
@ -249,7 +250,8 @@ export interface RawRule extends SavedObjectAttributes {
meta?: RuleMeta;
executionStatus: RawRuleExecutionStatus;
monitoring?: RuleMonitoring;
snoozeEndTime?: string | null; // Remove ? when this parameter is made available in the public API
snoozeSchedule?: RuleSnooze; // Remove ? when this parameter is made available in the public API
isSnoozedUntil?: string | null;
}
export interface AlertingPlugin {

View file

@ -10,33 +10,33 @@ import { getRuleStatusDropdownLazy } from '../../../common/get_rule_status_dropd
export const RuleStatusDropdownSandbox: React.FC<{}> = () => {
const [enabled, setEnabled] = useState(true);
const [snoozeEndTime, setSnoozeEndTime] = useState<Date | null>(null);
const [isSnoozedUntil, setIsSnoozedUntil] = useState<Date | null>(null);
const [muteAll, setMuteAll] = useState(false);
return getRuleStatusDropdownLazy({
rule: {
enabled,
snoozeEndTime,
isSnoozedUntil,
muteAll,
},
enableRule: async () => {
setEnabled(true);
setMuteAll(false);
setSnoozeEndTime(null);
setIsSnoozedUntil(null);
},
disableRule: async () => setEnabled(false),
snoozeRule: async (time) => {
if (time === -1) {
setSnoozeEndTime(null);
setIsSnoozedUntil(null);
setMuteAll(true);
} else {
setSnoozeEndTime(new Date(time));
setIsSnoozedUntil(new Date(time));
setMuteAll(false);
}
},
unsnoozeRule: async () => {
setMuteAll(false);
setSnoozeEndTime(null);
setIsSnoozedUntil(null);
},
onRuleChanged: () => {},
isEditable: true,

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.snoozeEndTime > now)",
"filter": "alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)",
"search": undefined,
"search_fields": undefined,
},
@ -262,7 +262,7 @@ describe('loadRuleAggregations', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)",
"search": undefined,
"search_fields": undefined,
},
@ -281,7 +281,7 @@ describe('loadRuleAggregations', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)",
"search": undefined,
"search_fields": undefined,
},

View file

@ -43,7 +43,8 @@ export const transformRule: RewriteRequestCase<Rule> = ({
scheduled_task_id: scheduledTaskId,
execution_status: executionStatus,
actions: actions,
snooze_end_time: snoozeEndTime,
snooze_schedule: snoozeSchedule,
is_snoozed_until: isSnoozedUntil,
...rest
}: any) => ({
ruleTypeId,
@ -55,12 +56,13 @@ export const transformRule: RewriteRequestCase<Rule> = ({
notifyWhen,
muteAll,
mutedInstanceIds,
snoozeEndTime,
snoozeSchedule,
executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined,
actions: actions
? actions.map((action: AsApiContract<RuleAction>) => transformAction(action))
: [],
scheduledTaskId,
isSnoozedUntil,
...rest,
});

View file

@ -46,7 +46,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['enabled'],
})
).toEqual([
'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)',
]);
expect(
@ -54,21 +54,21 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['disabled'],
})
).toEqual([
'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)',
]);
expect(
mapFiltersToKql({
ruleStatusesFilter: ['snoozed'],
})
).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)']);
).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)']);
expect(
mapFiltersToKql({
ruleStatusesFilter: ['enabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)',
]);
expect(
@ -76,7 +76,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['disabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)',
]);
expect(
@ -84,7 +84,7 @@ describe('mapFiltersToKql', () => {
ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)',
]);
});

View file

@ -57,7 +57,7 @@ export const mapFiltersToKql = ({
if (ruleStatusesFilter && ruleStatusesFilter.length) {
const enablementFilter = getEnablementFilter(ruleStatusesFilter);
const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)`;
const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)`;
const hasEnablement =
ruleStatusesFilter.includes('enabled') || ruleStatusesFilter.includes('disabled');
const hasSnoozed = ruleStatusesFilter.includes('snoozed');

View file

@ -266,7 +266,7 @@ describe('loadRules', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)",
"page": 1,
"per_page": 10,
"search": undefined,
@ -295,7 +295,7 @@ describe('loadRules', () => {
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"filter": "alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)",
"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 or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"filter": "alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)",
"page": 1,
"per_page": 10,
"search": undefined,

View file

@ -10,7 +10,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import { RuleStatusDropdown, ComponentOpts } from './rule_status_dropdown';
const NOW_STRING = '2020-03-01T00:00:00.000Z';
const SNOOZE_END_TIME = new Date('2020-03-04T00:00:00.000Z');
const SNOOZE_UNTIL = new Date('2020-03-04T00:00:00.000Z');
describe('RuleStatusDropdown', () => {
const enableRule = jest.fn();
@ -51,7 +51,7 @@ describe('RuleStatusDropdown', () => {
notifyWhen: null,
index: 0,
updatedAt: new Date('2020-08-20T19:23:38Z'),
snoozeEndTime: null,
snoozeSchedule: [],
} as ComponentOpts['rule'],
onRuleChanged: jest.fn(),
};
@ -86,7 +86,7 @@ describe('RuleStatusDropdown', () => {
const wrapper = mountWithIntl(
<RuleStatusDropdown
{...{ ...props, rule: { ...props.rule, snoozeEndTime: SNOOZE_END_TIME } }}
{...{ ...props, rule: { ...props.rule, isSnoozedUntil: SNOOZE_UNTIL } }}
/>
);
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe('Snoozed');
@ -108,7 +108,7 @@ describe('RuleStatusDropdown', () => {
test('renders status control as disabled when rule is snoozed but also disabled', () => {
const wrapper = mountWithIntl(
<RuleStatusDropdown
{...{ ...props, rule: { ...props.rule, enabled: false, snoozeEndTime: SNOOZE_END_TIME } }}
{...{ ...props, rule: { ...props.rule, enabled: false, isSnoozedUntil: SNOOZE_UNTIL } }}
/>
);
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe(
@ -121,7 +121,7 @@ describe('RuleStatusDropdown', () => {
<RuleStatusDropdown
{...{
...props,
rule: { ...props.rule, snoozeEndTime: SNOOZE_END_TIME },
rule: { ...props.rule },
}}
isEditable={false}
/>

View file

@ -36,7 +36,7 @@ import { Rule } from '../../../../types';
type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M';
const SNOOZE_END_TIME_FORMAT = 'LL @ LT';
type DropdownRuleRecord = Pick<Rule, 'enabled' | 'snoozeEndTime' | 'muteAll'>;
type DropdownRuleRecord = Pick<Rule, 'enabled' | 'muteAll' | 'isSnoozedUntil'>;
export interface ComponentOpts {
rule: DropdownRuleRecord;
@ -74,6 +74,11 @@ const usePreviousSnoozeInterval: (p?: string | null) => [string | null, (n: stri
return [previousSnoozeInterval, storeAndSetPreviousSnoozeInterval];
};
const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) =>
Boolean(
(rule.isSnoozedUntil && new Date(rule.isSnoozedUntil).getTime() > Date.now()) || rule.muteAll
);
export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
rule,
onRuleChanged,
@ -158,11 +163,13 @@ export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
isEnabled && isSnoozed ? (
<EuiToolTip
content={
rule.muteAll ? INDEFINITELY : moment(rule.snoozeEndTime).format(SNOOZE_END_TIME_FORMAT)
rule.muteAll
? INDEFINITELY
: moment(new Date(rule.isSnoozedUntil!)).format(SNOOZE_END_TIME_FORMAT)
}
>
<EuiText color="subdued" size="xs">
{rule.muteAll ? INDEFINITELY : moment(rule.snoozeEndTime).fromNow(true)}
{rule.muteAll ? INDEFINITELY : moment(new Date(rule.isSnoozedUntil!)).fromNow(true)}
</EuiText>
</EuiToolTip>
) : null;
@ -215,7 +222,7 @@ export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
onChangeSnooze={onChangeSnooze}
isEnabled={isEnabled}
isSnoozed={isSnoozed}
snoozeEndTime={rule.snoozeEndTime}
snoozeEndTime={rule.isSnoozedUntil}
previousSnoozeInterval={previousSnoozeInterval}
/>
</EuiPopover>
@ -476,15 +483,6 @@ const SnoozePanel: React.FunctionComponent<SnoozePanelProps> = ({
);
};
const isRuleSnoozed = (rule: DropdownRuleRecord) => {
const { snoozeEndTime, muteAll } = rule;
if (muteAll) return true;
if (!snoozeEndTime) {
return false;
}
return moment(Date.now()).isBefore(snoozeEndTime);
};
const futureTimeToInterval = (time?: Date | null) => {
if (!time) return;
const relativeTime = moment(time).locale('en').fromNow(true);

View file

@ -115,6 +115,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
created_by: user.username,
schedule: { interval: '1m' },
scheduled_task_id: response.body.scheduled_task_id,
snooze_schedule: response.body.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
throttle: '1m',

View file

@ -73,6 +73,7 @@ const findTestUtils = (
params: {},
created_by: 'elastic',
scheduled_task_id: match.scheduled_task_id,
snooze_schedule: match.snooze_schedule,
created_at: match.created_at,
updated_at: match.updated_at,
throttle: '1m',
@ -82,9 +83,7 @@ const findTestUtils = (
mute_all: false,
muted_alert_ids: [],
execution_status: match.execution_status,
...(describeType === 'internal'
? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time }
: {}),
...(describeType === 'internal' ? { monitoring: match.monitoring } : {}),
});
expect(Date.parse(match.created_at)).to.be.greaterThan(0);
expect(Date.parse(match.updated_at)).to.be.greaterThan(0);
@ -283,9 +282,8 @@ const findTestUtils = (
created_at: match.created_at,
updated_at: match.updated_at,
execution_status: match.execution_status,
...(describeType === 'internal'
? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time }
: {}),
snooze_schedule: match.snooze_schedule,
...(describeType === 'internal' ? { monitoring: match.monitoring } : {}),
});
expect(Date.parse(match.created_at)).to.be.greaterThan(0);
expect(Date.parse(match.updated_at)).to.be.greaterThan(0);

View file

@ -72,6 +72,7 @@ const getTestUtils = (
params: {},
created_by: 'elastic',
scheduled_task_id: response.body.scheduled_task_id,
snooze_schedule: response.body.snooze_schedule,
updated_at: response.body.updated_at,
created_at: response.body.created_at,
throttle: '1m',
@ -84,7 +85,6 @@ const getTestUtils = (
...(describeType === 'internal'
? {
monitoring: response.body.monitoring,
snooze_end_time: response.body.snooze_end_time,
}
: {}),
});

View file

@ -99,7 +99,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext)
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(true);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,
@ -156,7 +156,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext)
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(true);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,
@ -224,7 +224,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext)
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(true);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,
@ -292,7 +292,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext)
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(true);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,

View file

@ -97,12 +97,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const now = Date.now();
const { body: updatedAlert } = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME);
expect(updatedAlert.snooze_schedule.length).to.eql(1);
// Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time
const { rRule, duration } = updatedAlert.snooze_schedule[0];
expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true);
expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be(
true
);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -156,12 +163,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const now = Date.now();
const { body: updatedAlert } = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME);
expect(updatedAlert.snooze_schedule.length).to.eql(1);
// Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time
const { rRule, duration } = updatedAlert.snooze_schedule[0];
expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true);
expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be(
true
);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -226,12 +240,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const now = Date.now();
const { body: updatedAlert } = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME);
expect(updatedAlert.snooze_schedule.length).to.eql(1);
// Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time
const { rRule, duration } = updatedAlert.snooze_schedule[0];
expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true);
expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be(
true
);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -296,12 +317,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const now = Date.now();
const { body: updatedAlert } = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME);
expect(updatedAlert.snooze_schedule.length).to.eql(1);
// Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time
const { rRule, duration } = updatedAlert.snooze_schedule[0];
expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true);
expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be(
true
);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -383,7 +411,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(null);
expect(updatedAlert.snooze_schedule).to.eql([]);
expect(updatedAlert.mute_all).to.eql(true);
// Ensure AAD isn't broken
await checkAAD({

View file

@ -104,7 +104,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(false);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,
@ -166,7 +166,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(false);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,
@ -239,7 +239,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(false);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,
@ -312,7 +312,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.mute_all).to.eql(false);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest,

View file

@ -98,7 +98,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(null);
expect(updatedAlert.snooze_schedule).to.eql(null);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -155,7 +155,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(null);
expect(updatedAlert.snooze_schedule).to.eql(null);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -223,7 +223,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(null);
expect(updatedAlert.snooze_schedule).to.eql(null);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -291,7 +291,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(null);
expect(updatedAlert.snooze_schedule).to.eql(null);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({

View file

@ -129,6 +129,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
},
],
scheduled_task_id: createdAlert.scheduled_task_id,
snooze_schedule: createdAlert.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
@ -213,6 +214,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
snooze_schedule: createdAlert.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
@ -308,6 +310,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
snooze_schedule: createdAlert.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
@ -403,6 +406,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
snooze_schedule: createdAlert.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
@ -496,6 +500,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
snooze_schedule: createdAlert.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,

View file

@ -85,6 +85,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
created_by: null,
schedule: { interval: '1m' },
scheduled_task_id: response.body.scheduled_task_id,
snooze_schedule: response.body.snooze_schedule,
updated_by: null,
api_key_owner: null,
throttle: '1m',
@ -180,6 +181,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
created_by: null,
schedule: { interval: '1m' },
scheduled_task_id: response.body.scheduled_task_id,
snooze_schedule: response.body.snooze_schedule,
updated_by: null,
api_key_owner: null,
throttle: '1m',
@ -475,6 +477,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
createdBy: null,
schedule: { interval: '1m' },
scheduledTaskId: response.body.scheduledTaskId,
snoozeSchedule: response.body.snoozeSchedule,
updatedBy: null,
apiKeyOwner: null,
throttle: '1m',

View file

@ -64,6 +64,7 @@ const findTestUtils = (
created_by: null,
api_key_owner: null,
scheduled_task_id: match.scheduled_task_id,
snooze_schedule: match.snooze_schedule,
updated_by: null,
throttle: '1m',
notify_when: 'onThrottleInterval',
@ -72,9 +73,7 @@ const findTestUtils = (
created_at: match.created_at,
updated_at: match.updated_at,
execution_status: match.execution_status,
...(describeType === 'internal'
? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time }
: {}),
...(describeType === 'internal' ? { monitoring: match.monitoring } : {}),
});
expect(Date.parse(match.created_at)).to.be.greaterThan(0);
expect(Date.parse(match.updated_at)).to.be.greaterThan(0);
@ -296,6 +295,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
createdBy: null,
apiKeyOwner: null,
scheduledTaskId: match.scheduledTaskId,
snoozeSchedule: match.snoozeSchedule,
updatedBy: null,
throttle: '1m',
notifyWhen: 'onThrottleInterval',

View file

@ -45,6 +45,7 @@ const getTestUtils = (
params: {},
created_by: null,
scheduled_task_id: response.body.scheduled_task_id,
snooze_schedule: response.body.snooze_schedule,
updated_by: null,
api_key_owner: null,
throttle: '1m',
@ -55,7 +56,7 @@ const getTestUtils = (
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
...(describeType === 'internal'
? { monitoring: response.body.monitoring, snooze_end_time: response.body.snooze_end_time }
? { monitoring: response.body.monitoring, snooze_schedule: response.body.snooze_schedule }
: {}),
});
expect(Date.parse(response.body.created_at)).to.be.greaterThan(0);
@ -136,6 +137,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
params: {},
createdBy: null,
scheduledTaskId: response.body.scheduledTaskId,
snoozeSchedule: response.body.snoozeSchedule,
updatedBy: null,
apiKeyOwner: null,
throttle: '1m',

View file

@ -41,7 +41,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.mute_all).to.eql(true);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
supertest: supertestWithoutAuth,
@ -70,7 +70,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.mute_all).to.eql(true);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({

View file

@ -70,11 +70,16 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const now = Date.now();
const { body: updatedAlert } = await supertestWithoutAuth
.get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`)
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME);
expect(updatedAlert.snooze_schedule.length).to.eql(1);
// Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time
const { rRule, duration } = updatedAlert.snooze_schedule[0];
expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true);
expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be(true);
expect(updatedAlert.mute_all).to.eql(false);
// Ensure AAD isn't broken
await checkAAD({
@ -126,7 +131,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext
.get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`)
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.snooze_end_time).to.eql(null);
expect(updatedAlert.snooze_schedule).to.eql([]);
expect(updatedAlert.mute_all).to.eql(true);
// Ensure AAD isn't broken
await checkAAD({

View file

@ -42,7 +42,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.mute_all).to.eql(false);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({
@ -76,7 +76,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.mute_all).to.eql(false);
expect(updatedAlert.snooze_end_time).to.eql(undefined);
expect(updatedAlert.snooze_schedule.length).to.eql(0);
// Ensure AAD isn't broken
await checkAAD({

View file

@ -60,6 +60,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
muted_alert_ids: [],
notify_when: 'onThrottleInterval',
scheduled_task_id: createdAlert.scheduled_task_id,
snooze_schedule: createdAlert.snooze_schedule,
created_at: response.body.created_at,
updated_at: response.body.updated_at,
execution_status: response.body.execution_status,
@ -160,6 +161,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
mutedInstanceIds: [],
notifyWhen: 'onThrottleInterval',
scheduledTaskId: createdAlert.scheduled_task_id,
snoozeSchedule: createdAlert.snooze_schedule,
createdAt: response.body.createdAt,
updatedAt: response.body.updatedAt,
executionStatus: response.body.executionStatus,

View file

@ -7006,6 +7006,13 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/rrule@^2.2.9":
version "2.2.9"
resolved "https://registry.yarnpkg.com/@types/rrule/-/rrule-2.2.9.tgz#b25222b5057b9a9e6eea28ce9e94673a957c960f"
integrity sha512-OWTezBoGwsL2nn9SFbLbiTrAic1hpxAIRqeF8QDB84iW6KBEAHM6Oj9T2BEokgeIDgT1q73sfD0gI1S2yElSFA==
dependencies:
rrule "*"
"@types/seedrandom@>=2.0.0 <4.0.0":
version "2.4.28"
resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
@ -19469,11 +19476,16 @@ lru-queue@0.1:
dependencies:
es5-ext "~0.10.2"
luxon@^1.25.0:
luxon@^1.21.3, luxon@^1.25.0:
version "1.28.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf"
integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==
luxon@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.2.tgz#5f2f3002b8c39b60a7b7ad24b2a85d90dc5db49c"
integrity sha512-MlAQQVMFhGk4WUA6gpfsy0QycnKP0+NlCBJRVRNPxxSIbjrCbQ65nrpJD3FVyJNZLuJ0uoqL57ye6BmDYgHaSw==
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
@ -25234,6 +25246,24 @@ rollup@^0.25.8:
minimist "^1.2.0"
source-map-support "^0.3.2"
rrule@*:
version "2.6.9"
resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.6.9.tgz#8ee4ee261451e84852741f92ded769245580744a"
integrity sha512-PE4ErZDMfAcRnc1B35bZgPGS9mbn7Z9bKDgk6+XgrIwvBjeWk7JVEYsqKwHYTrDGzsHPtZTpaon8IyeKzAhj5w==
dependencies:
tslib "^1.10.0"
optionalDependencies:
luxon "^1.21.3"
rrule@2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.6.4.tgz#7f4f31fda12bc7249bb176c891109a9bc448e035"
integrity sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==
dependencies:
tslib "^1.10.0"
optionalDependencies:
luxon "^1.21.3"
rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"