diff --git a/package.json b/package.json index 52a681a39b4d0..2d3009b7b7099 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index f056ad7e0e4b7..eeb3db0be0066 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -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; diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 4509a004c6e58..f690e1b603359 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -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; export type RuleTypeParams = Record; @@ -104,12 +105,13 @@ export interface Rule { 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 = Omit, 'apiKey'>; diff --git a/x-pack/plugins/alerting/common/rule_snooze_type.ts b/x-pack/plugins/alerting/common/rule_snooze_type.ts new file mode 100644 index 0000000000000..405cbef357242 --- /dev/null +++ b/x-pack/plugins/alerting/common/rule_snooze_type.ts @@ -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 & Pick; + // 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; + bymonth?: number[]; + bysetpos?: number[]; + bymonthday: number; + byyearday: number[]; + byweekno: number[]; + byhour: number[]; + byminute: number[]; + bysecond: number[]; +} diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index 31528c0d50683..4c0d4a00b05de 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -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'; diff --git a/x-pack/plugins/alerting/server/lib/is_rule_snoozed.test.ts b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.test.ts new file mode 100644 index 0000000000000..14ad981a5e903 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts new file mode 100644 index 0000000000000..7ae4b99e4df75 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts @@ -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, '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): ByWeekday[] { + const rRuleString = `RRULE:BYDAY=${byweekday.join(',')}`; + const parsedRRule = rrulestr(rRuleString); + return parsedRRule.origOptions.byweekday as ByWeekday[]; +} diff --git a/x-pack/plugins/alerting/server/routes/create_rule.ts b/x-pack/plugins/alerting/server/routes/create_rule.ts index cf044c94f2529..442162ae21cbb 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.ts @@ -67,12 +67,14 @@ const rewriteBodyRes: RewriteResponseCase> = ({ 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, diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index f4414b0364dcb..c735d68f83bbe 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -35,7 +35,8 @@ const rewriteBodyRes: RewriteResponseCase> = ({ executionStatus, actions, scheduledTaskId, - snoozeEndTime, + snoozeSchedule, + isSnoozedUntil, ...rest }) => ({ ...rest, @@ -46,10 +47,10 @@ const rewriteBodyRes: RewriteResponseCase> = ({ 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'), diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts index 537d42bbc4f47..162177d695e0a 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts @@ -21,7 +21,8 @@ export const rewriteRule = ({ executionStatus, actions, scheduledTaskId, - snoozeEndTime, + snoozeSchedule, + isSnoozedUntil, ...rest }: SanitizedRule) => ({ ...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, diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts index d2130e1f33541..1faddd66c8f0e 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -70,12 +70,16 @@ const rewriteBodyRes: RewriteResponseCase> = ({ 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 } : {}), diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 302824221ded8..44914e3e3bce8 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -35,6 +35,7 @@ const createRulesClientMock = () => { bulkEdit: jest.fn(), snooze: jest.fn(), unsnooze: jest.fn(), + updateSnoozedUntilTime: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index ec01c2c15abf4..4e248412eae15 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -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 { | 'mutedInstanceIds' | 'actions' | 'executionStatus' - | 'snoozeEndTime' + | 'snoozeSchedule' + | 'isSnoozedUntil' > & { actions: NormalizedAlertAction[] }; options?: { id?: string; @@ -391,7 +394,7 @@ export class RulesClient { private readonly fieldsToExcludeFromPublicApi: Array = [ '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 { @@ -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 { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + '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 { 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, references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false ): PartialRule | PartialRuleWithLegacyId { - 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') + : []; +} diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index 1a3d203162bd6..bc1c8d276aedd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -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' }], }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 8e24b7c183262..f5c839c5006fd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -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: { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts index 7f8ae28a20c6e..e2625be88482c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts @@ -82,7 +82,7 @@ describe('muteAll()', () => { { muteAll: true, mutedInstanceIds: [], - snoozeEndTime: null, + snoozeSchedule: [], updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts index cf063eea07862..f5d4cb372f867 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts @@ -82,7 +82,7 @@ describe('unmuteAll()', () => { { muteAll: false, mutedInstanceIds: [], - snoozeEndTime: null, + snoozeSchedule: [], updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 6566fee15d4a8..f4f23cced722c 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -30,7 +30,8 @@ export const AlertAttributesExcludedFromAAD = [ 'updatedAt', 'executionStatus', 'monitoring', - 'snoozeEndTime', + 'snoozeSchedule', + 'isSnoozedUntil', ]; // useful for Pick which is a @@ -45,7 +46,8 @@ export type AlertAttributesExcludedFromAADType = | 'updatedAt' | 'executionStatus' | 'monitoring' - | 'snoozeEndTime'; + | 'snoozeSchedule' + | 'isSnoozedUntil'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.ts b/x-pack/plugins/alerting/server/saved_objects/mappings.ts index 5e2803222ecba..31ad40117a7ec 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.ts +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.ts @@ -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', }, diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index c83d0a95dfdcb..bbf93f85450cb 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -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' diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index b3f8d873d8ef0..ddae200ae8fa6 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -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): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(addSearchType, removeInternalTags) + pipeMigrations(addSearchType, removeInternalTags, convertSnoozes) ); return mergeSavedObjectMigrationMaps( @@ -888,6 +890,33 @@ function addMappedParams( return doc; } +function convertSnoozes( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + 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 diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 7d95f63f3c43c..f3d2c7039585b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -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(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 6cd6b73b9539e..525c252b40b66 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -25,6 +25,7 @@ import { getRecoveredAlerts, ruleExecutionStatusToRaw, validateRuleTypeParams, + isRuleSnoozed, } from '../lib'; import { Rule, @@ -247,18 +248,6 @@ export class TaskRunner< } } - private isRuleSnoozed(rule: SanitizedRule): 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> { const { params: { alertId: ruleId, spaceId }, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 1c453df386e24..7b1725e42bd5e 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -40,6 +40,7 @@ import { SanitizedRuleConfig, RuleMonitoring, MappedParams, + RuleSnooze, } from '../common'; export type WithoutQueryAndParams = Pick>; 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 { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx index 46b7fed8e14d4..e17721930858d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx @@ -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(null); + const [isSnoozedUntil, setIsSnoozedUntil] = useState(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, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index 5377e4269f46e..104f0507aef8e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -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, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts index 67838f4f84881..5648aa30820c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts @@ -43,7 +43,8 @@ export const transformRule: RewriteRequestCase = ({ 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 = ({ notifyWhen, muteAll, mutedInstanceIds, - snoozeEndTime, + snoozeSchedule, executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined, actions: actions ? actions.map((action: AsApiContract) => transformAction(action)) : [], scheduledTaskId, + isSnoozedUntil, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index f67a27ef5409c..8d744c84d6f77 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -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)', ]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index ff2a49e3a5e45..6629024e3eb11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -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'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 2a20c9d9469f5..e06ee24464d78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -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, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx index 15086518124b4..b2ea5e9a78aae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx @@ -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( ); 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( ); expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe( @@ -121,7 +121,7 @@ describe('RuleStatusDropdown', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index 40658ae282e16..8a68abecf2873 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -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; +type DropdownRuleRecord = Pick; 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 = ({ rule, onRuleChanged, @@ -158,11 +163,13 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ isEnabled && isSnoozed ? ( - {rule.muteAll ? INDEFINITELY : moment(rule.snoozeEndTime).fromNow(true)} + {rule.muteAll ? INDEFINITELY : moment(new Date(rule.isSnoozedUntil!)).fromNow(true)} ) : null; @@ -215,7 +222,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ onChangeSnooze={onChangeSnooze} isEnabled={isEnabled} isSnoozed={isSnoozed} - snoozeEndTime={rule.snoozeEndTime} + snoozeEndTime={rule.isSnoozedUntil} previousSnoozeInterval={previousSnoozeInterval} /> @@ -474,15 +481,6 @@ const SnoozePanel: React.FunctionComponent = ({ ); }; -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); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index e601c6ee15ec7..2d3829f42a678 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -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', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 20a5e82d303fe..177e51ab78eea 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -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); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts index 48559aa35ac3c..c2c94af19b209 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts @@ -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, } : {}), }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts index 5a4c792463b62..f0ce5962de368 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts @@ -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, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts index 553e090498f00..0ca1ce4bf1eb7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts @@ -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({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts index dde198f54f771..9c918b3225f9e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts @@ -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, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts index c868654235c21..8b6a8aa2c6c45 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts @@ -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({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts index c49fa62c606b6..d28b81f479b11 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts @@ -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, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 143d845d074c4..a33f7fc5a1a2c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -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', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index a1b0f5c7eeb14..021a2be1ebb5d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -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', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 58c68def04372..ee993c425fa38 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -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', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts index 53517b191bab6..a56b95ed09219 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -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({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts index 5be5b59a15248..80cfa5a105467 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts @@ -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({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts index 782df6d86d542..62ff63052f841 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -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({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index c5a9c93d45e81..c431654f0fd20 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -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, diff --git a/yarn.lock b/yarn.lock index ebfce5de26090..30f73d40cd149 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"