diff --git a/package.json b/package.json index 0ccba659d3c77..4c92c552c6656 100644 --- a/package.json +++ b/package.json @@ -1103,8 +1103,8 @@ "css-box-model": "^1.2.1", "css.escape": "^1.5.1", "cypress-data-session": "^2.8.0", - "cytoscape": "^3.10.0", - "cytoscape-dagre": "^2.2.2", + "cytoscape": "^3.30.4", + "cytoscape-dagre": "^2.5.0", "d3": "3.5.17", "d3-array": "2.12.1", "d3-brush": "^3.0.0", @@ -1543,7 +1543,7 @@ "@types/classnames": "^2.2.9", "@types/cli-progress": "^3.11.5", "@types/color": "^3.0.3", - "@types/cytoscape": "^3.14.0", + "@types/cytoscape": "^3.21.8", "@types/d3": "^3.5.43", "@types/d3-array": "^2.12.1", "@types/d3-brush": "^3.0.0", diff --git a/packages/kbn-rrule/index.ts b/packages/kbn-rrule/index.ts index 2c860a548c248..7c63337710699 100644 --- a/packages/kbn-rrule/index.ts +++ b/packages/kbn-rrule/index.ts @@ -7,6 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { RRule, Frequency, Weekday } from './rrule'; -export type { Options } from './rrule'; -export declare type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU'; +export { RRule } from './rrule'; +export type { Options, WeekdayStr } from './types'; +export { Frequency, Weekday } from './types'; diff --git a/packages/kbn-rrule/rrule.test.ts b/packages/kbn-rrule/rrule.test.ts index 0a024e27bc7ed..f30e7d235012d 100644 --- a/packages/kbn-rrule/rrule.test.ts +++ b/packages/kbn-rrule/rrule.test.ts @@ -8,7 +8,8 @@ */ import sinon from 'sinon'; -import { RRule, Frequency, Weekday } from './rrule'; +import { RRule } from './rrule'; +import { Frequency, Weekday } from './types'; const DATE_2019 = '2019-01-01T00:00:00.000Z'; const DATE_2019_DECEMBER_19 = '2019-12-19T00:00:00.000Z'; @@ -730,6 +731,228 @@ describe('RRule', () => { ] `); }); + it('ignores invalid byweekday values', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.WEEKLY, + interval: 1, + tzid: 'UTC', + byweekday: [Weekday.TH, 0, -2], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2019-12-19T00:00:00.000Z, + 2019-12-26T00:00:00.000Z, + 2020-01-02T00:00:00.000Z, + 2020-01-09T00:00:00.000Z, + 2020-01-16T00:00:00.000Z, + 2020-01-23T00:00:00.000Z, + 2020-01-30T00:00:00.000Z, + 2020-02-06T00:00:00.000Z, + 2020-02-13T00:00:00.000Z, + 2020-02-20T00:00:00.000Z, + 2020-02-27T00:00:00.000Z, + 2020-03-05T00:00:00.000Z, + 2020-03-12T00:00:00.000Z, + 2020-03-19T00:00:00.000Z, + ] + `); + + const rule2 = new RRule({ + dtstart: new Date(DATE_2019), + freq: Frequency.WEEKLY, + interval: 1, + tzid: 'UTC', + byweekday: [Weekday.SA, Weekday.SU, Weekday.MO, 0], + }); + + expect(rule2.all(9)).toMatchInlineSnapshot(` + Array [ + 2019-01-05T00:00:00.000Z, + 2019-01-06T00:00:00.000Z, + 2019-01-07T00:00:00.000Z, + 2019-01-12T00:00:00.000Z, + 2019-01-13T00:00:00.000Z, + 2019-01-14T00:00:00.000Z, + 2019-01-19T00:00:00.000Z, + 2019-01-20T00:00:00.000Z, + 2019-01-21T00:00:00.000Z, + ] + `); + }); + }); + + describe('bymonth', () => { + it('works with yearly frequency', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.YEARLY, + interval: 1, + tzid: 'UTC', + bymonth: [2, 5], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2020-02-19T00:00:00.000Z, + 2020-05-19T00:00:00.000Z, + 2021-02-19T00:00:00.000Z, + 2021-05-19T00:00:00.000Z, + 2022-02-19T00:00:00.000Z, + 2022-05-19T00:00:00.000Z, + 2023-02-19T00:00:00.000Z, + 2023-05-19T00:00:00.000Z, + 2024-02-19T00:00:00.000Z, + 2024-05-19T00:00:00.000Z, + 2025-02-19T00:00:00.000Z, + 2025-05-19T00:00:00.000Z, + 2026-02-19T00:00:00.000Z, + 2026-05-19T00:00:00.000Z, + ] + `); + }); + it('ignores invalid bymonth values', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.YEARLY, + interval: 1, + tzid: 'UTC', + bymonth: [0], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2019-12-19T00:00:00.000Z, + 2020-12-19T00:00:00.000Z, + 2021-12-19T00:00:00.000Z, + 2022-12-19T00:00:00.000Z, + 2023-12-19T00:00:00.000Z, + 2024-12-19T00:00:00.000Z, + 2025-12-19T00:00:00.000Z, + 2026-12-19T00:00:00.000Z, + 2027-12-19T00:00:00.000Z, + 2028-12-19T00:00:00.000Z, + 2029-12-19T00:00:00.000Z, + 2030-12-19T00:00:00.000Z, + 2031-12-19T00:00:00.000Z, + 2032-12-19T00:00:00.000Z, + ] + `); + }); + }); + + describe('bymonthday', () => { + it('works with monthly frequency', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.MONTHLY, + interval: 1, + tzid: 'UTC', + bymonthday: [1, 15], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2020-01-01T00:00:00.000Z, + 2020-01-15T00:00:00.000Z, + 2020-02-01T00:00:00.000Z, + 2020-02-15T00:00:00.000Z, + 2020-03-01T00:00:00.000Z, + 2020-03-15T00:00:00.000Z, + 2020-04-01T00:00:00.000Z, + 2020-04-15T00:00:00.000Z, + 2020-05-01T00:00:00.000Z, + 2020-05-15T00:00:00.000Z, + 2020-06-01T00:00:00.000Z, + 2020-06-15T00:00:00.000Z, + 2020-07-01T00:00:00.000Z, + 2020-07-15T00:00:00.000Z, + ] + `); + }); + it('ignores invalid bymonthday values', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.MONTHLY, + interval: 1, + tzid: 'UTC', + bymonthday: [0, -1, 32], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2019-12-19T00:00:00.000Z, + 2020-01-19T00:00:00.000Z, + 2020-02-19T00:00:00.000Z, + 2020-03-19T00:00:00.000Z, + 2020-04-19T00:00:00.000Z, + 2020-05-19T00:00:00.000Z, + 2020-06-19T00:00:00.000Z, + 2020-07-19T00:00:00.000Z, + 2020-08-19T00:00:00.000Z, + 2020-09-19T00:00:00.000Z, + 2020-10-19T00:00:00.000Z, + 2020-11-19T00:00:00.000Z, + 2020-12-19T00:00:00.000Z, + 2021-01-19T00:00:00.000Z, + ] + `); + }); + }); + + describe('bymonth, bymonthday', () => { + it('works with yearly frequency', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.YEARLY, + interval: 1, + tzid: 'UTC', + bymonth: [2, 5], + bymonthday: [8], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2020-02-08T00:00:00.000Z, + 2020-05-08T00:00:00.000Z, + 2021-02-08T00:00:00.000Z, + 2021-05-08T00:00:00.000Z, + 2022-02-08T00:00:00.000Z, + 2022-05-08T00:00:00.000Z, + 2023-02-08T00:00:00.000Z, + 2023-05-08T00:00:00.000Z, + 2024-02-08T00:00:00.000Z, + 2024-05-08T00:00:00.000Z, + 2025-02-08T00:00:00.000Z, + 2025-05-08T00:00:00.000Z, + 2026-02-08T00:00:00.000Z, + 2026-05-08T00:00:00.000Z, + ] + `); + }); + it('ignores valid dates that do not exist e.g. February 30th', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2019_DECEMBER_19), + freq: Frequency.YEARLY, + interval: 1, + tzid: 'UTC', + bymonth: [2, 5], + bymonthday: [30], + }); + expect(rule.all(14)).toMatchInlineSnapshot(` + Array [ + 2020-05-30T00:00:00.000Z, + 2021-05-30T00:00:00.000Z, + 2022-05-30T00:00:00.000Z, + 2023-05-30T00:00:00.000Z, + 2024-05-30T00:00:00.000Z, + 2025-05-30T00:00:00.000Z, + 2026-05-30T00:00:00.000Z, + 2027-05-30T00:00:00.000Z, + 2028-05-30T00:00:00.000Z, + 2029-05-30T00:00:00.000Z, + 2030-05-30T00:00:00.000Z, + 2031-05-30T00:00:00.000Z, + 2032-05-30T00:00:00.000Z, + 2033-05-30T00:00:00.000Z, + ] + `); + }); }); describe('byhour, byminute, bysecond', () => { @@ -844,6 +1067,30 @@ describe('RRule', () => { ] `); }); + it('ignores invalid byyearday values', () => { + const rule = new RRule({ + dtstart: new Date(DATE_2020), + freq: Frequency.YEARLY, + byyearday: [0, -1], + interval: 1, + tzid: 'UTC', + }); + + expect(rule.all(10)).toMatchInlineSnapshot(` + Array [ + 2020-01-01T00:00:00.000Z, + 2021-01-01T00:00:00.000Z, + 2022-01-01T00:00:00.000Z, + 2023-01-01T00:00:00.000Z, + 2024-01-01T00:00:00.000Z, + 2025-01-01T00:00:00.000Z, + 2026-01-01T00:00:00.000Z, + 2027-01-01T00:00:00.000Z, + 2028-01-01T00:00:00.000Z, + 2029-01-01T00:00:00.000Z, + ] + `); + }); }); describe('error handling', () => { @@ -872,5 +1119,33 @@ describe('RRule', () => { `"Cannot create RRule: until is an invalid date"` ); }); + + it('throws an error on an interval of 0', () => { + const testFn = () => + new RRule({ + dtstart: new Date(DATE_2020), + freq: Frequency.HOURLY, + interval: 0, + tzid: 'UTC', + }); + expect(testFn).toThrowErrorMatchingInlineSnapshot( + `"Cannot create RRule: interval must be greater than 0"` + ); + }); + + it('throws an error when exceeding the iteration limit', () => { + const testFn = () => { + const rule = new RRule({ + dtstart: new Date(DATE_2020), + freq: Frequency.YEARLY, + byyearday: [1], + interval: 1, + tzid: 'UTC', + }); + rule.all(100001); + }; + + expect(testFn).toThrowErrorMatchingInlineSnapshot(`"RRule iteration limit exceeded"`); + }); }); }); diff --git a/packages/kbn-rrule/rrule.ts b/packages/kbn-rrule/rrule.ts index 43e89ee209cb7..3279ec53d5194 100644 --- a/packages/kbn-rrule/rrule.ts +++ b/packages/kbn-rrule/rrule.ts @@ -7,58 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import moment, { Moment } from 'moment-timezone'; - -export enum Frequency { - YEARLY = 0, - MONTHLY = 1, - WEEKLY = 2, - DAILY = 3, - HOURLY = 4, - MINUTELY = 5, - SECONDLY = 6, -} - -export enum Weekday { - MO = 1, - TU = 2, - WE = 3, - TH = 4, - FR = 5, - SA = 6, - SU = 7, -} +import moment, { type Moment } from 'moment-timezone'; -export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU'; -interface IterOptions { - refDT: Moment; - wkst?: Weekday | number | null; - byyearday?: number[] | null; - bymonth?: number[] | null; - bysetpos?: number[] | null; - bymonthday?: number[] | null; - byweekday?: Weekday[] | null; - byhour?: number[] | null; - byminute?: number[] | null; - bysecond?: number[] | null; -} - -type Options = Omit & { - dtstart: Date; - freq?: Frequency; - interval?: number; - until?: Date | null; - count?: number; - tzid: string; -}; +import { Frequency, Weekday, type WeekdayStr, type Options, type IterOptions } from './types'; +import { sanitizeOptions } from './sanitize'; type ConstructorOptions = Omit & { byweekday?: Array | null; wkst?: Weekday | WeekdayStr | number | null; }; -export type { ConstructorOptions as Options }; - const ISO_WEEKDAYS = [ Weekday.MO, Weekday.TU, @@ -74,19 +32,15 @@ type AllResult = Date[] & { }; const ALL_LIMIT = 10000; +const TIMEOUT_LIMIT = 100000; export class RRule { private options: Options; constructor(options: ConstructorOptions) { - this.options = options as Options; - if (isNaN(options.dtstart.getTime())) { - throw new Error('Cannot create RRule: dtstart is an invalid date'); - } - if (options.until && isNaN(options.until.getTime())) { - throw new Error('Cannot create RRule: until is an invalid date'); - } + this.options = sanitizeOptions(options as Options); if (typeof options.wkst === 'string') { this.options.wkst = Weekday[options.wkst]; + if (!this.options.wkst) delete this.options.wkst; } const weekdayParseResult = parseByWeekdayPos(options.byweekday); if (weekdayParseResult) { @@ -112,12 +66,17 @@ export class RRule { .toDate(); const nextRecurrences: Moment[] = []; + let iters = 0; while ( (!count && !until) || (count && yieldedRecurrenceCount < count) || - (until && current.getTime() < new Date(until).getTime()) + (until && current.getTime() < until.getTime()) ) { + iters++; + if (iters > TIMEOUT_LIMIT) { + throw new Error('RRule iteration limit exceeded'); + } const next = nextRecurrences.shift()?.toDate(); if (next) { current = next; @@ -313,6 +272,7 @@ const getYearOfRecurrences = function ({ return getMonthOfRecurrences({ refDT: currentMonth, wkst, + bymonth, bymonthday, byweekday, byhour, @@ -327,6 +287,7 @@ const getYearOfRecurrences = function ({ return derivedByyearday.flatMap((dayOfYear) => { const currentDate = moment(refDT).dayOfYear(dayOfYear); + if (currentDate.year() !== refDT.year()) return []; if (!derivedByweekday.includes(currentDate.isoWeekday())) return []; return getDayOfRecurrences({ refDT: currentDate, byhour, byminute, bysecond }); }); @@ -345,7 +306,7 @@ const getMonthOfRecurrences = function ({ }: IterOptions) { const derivedByweekday = byweekday ?? ISO_WEEKDAYS; const currentMonth = refDT.month(); - if (bymonth && !bymonth.includes(currentMonth)) return []; + if (bymonth && !bymonth.includes(currentMonth + 1)) return []; let derivedBymonthday = bymonthday ?? [refDT.date()]; if (bysetpos) { @@ -392,6 +353,7 @@ const getMonthOfRecurrences = function ({ return derivedBymonthday.flatMap((date) => { const currentDate = moment(refDT).date(date); + if (bymonth && !bymonth.includes(currentDate.month() + 1)) return []; if (!derivedByweekday.includes(currentDate.isoWeekday())) return []; return getDayOfRecurrences({ refDT: currentDate, byhour, byminute, bysecond }); }); diff --git a/packages/kbn-rrule/sanitize.test.ts b/packages/kbn-rrule/sanitize.test.ts new file mode 100644 index 0000000000000..d316748571f76 --- /dev/null +++ b/packages/kbn-rrule/sanitize.test.ts @@ -0,0 +1,131 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { sanitizeOptions } from './sanitize'; +import { Weekday, Frequency, type Options } from './types'; + +describe('sanitizeOptions', () => { + const options: Options = { + wkst: Weekday.MO, + byyearday: [1, 2, 3], + bymonth: [1], + bysetpos: [1], + bymonthday: [1], + byweekday: [Weekday.MO], + byhour: [1], + byminute: [1], + bysecond: [1], + dtstart: new Date('September 3, 1998 03:24:00'), + freq: Frequency.YEARLY, + interval: 1, + until: new Date('February 25, 2022 03:24:00'), + count: 3, + tzid: 'foobar', + }; + + it('happy path', () => { + expect(sanitizeOptions(options)).toEqual(options); + }); + + it('throws an error when dtstart is missing', () => { + // @ts-expect-error + expect(() => sanitizeOptions({ ...options, dtstart: null })).toThrowError( + 'Cannot create RRule: dtstart is required' + ); + }); + + it('throws an error when tzid is missing', () => { + expect(() => sanitizeOptions({ ...options, tzid: '' })).toThrowError( + 'Cannot create RRule: tzid is required' + ); + }); + + it('throws an error when until field is invalid', () => { + expect(() => + sanitizeOptions({ + ...options, + // @ts-expect-error + until: { + getTime: () => NaN, + }, + }) + ).toThrowError('Cannot create RRule: until is an invalid date'); + }); + + it('throws an error when interval is less than 0', () => { + expect(() => sanitizeOptions({ ...options, interval: -3 })).toThrowError( + 'Cannot create RRule: interval must be greater than 0' + ); + }); + + it('throws an error when interval is not a number', () => { + // @ts-expect-error + expect(() => sanitizeOptions({ ...options, interval: 'foobar' })).toThrowError( + 'Cannot create RRule: interval must be a number' + ); + }); + + it('filters out invalid bymonth values', () => { + expect(sanitizeOptions({ ...options, bymonth: [0, 6, 13] })).toEqual({ + ...options, + bymonth: [6], + }); + }); + + it('removes bymonth when it is empty', () => { + expect(sanitizeOptions({ ...options, bymonth: [0] })).toEqual({ + ...options, + bymonth: undefined, + }); + }); + + it('filters out invalid bymonthday values', () => { + expect(sanitizeOptions({ ...options, bymonthday: [0, 15, 32] })).toEqual({ + ...options, + bymonthday: [15], + }); + }); + + it('removes bymonthday when it is empty', () => { + expect(sanitizeOptions({ ...options, bymonthday: [0] })).toEqual({ + ...options, + bymonthday: undefined, + }); + }); + + it('filters out invalid byweekday values', () => { + // @ts-expect-error + expect(sanitizeOptions({ ...options, byweekday: [0, 4, 8] })).toEqual({ + ...options, + byweekday: [4], + }); + }); + + it('removes byweekday when it is empty', () => { + // @ts-expect-error + expect(sanitizeOptions({ ...options, byweekday: [0] })).toEqual({ + ...options, + byweekday: undefined, + }); + }); + + it('filters out invalid byyearday values', () => { + expect(sanitizeOptions({ ...options, byyearday: [0, 150, 367] })).toEqual({ + ...options, + byyearday: [150], + }); + }); + + it('removes byyearday when it is empty', () => { + expect(sanitizeOptions({ ...options, byyearday: [0] })).toEqual({ + ...options, + byyearday: undefined, + }); + }); +}); diff --git a/packages/kbn-rrule/sanitize.ts b/packages/kbn-rrule/sanitize.ts new file mode 100644 index 0000000000000..7225c6668dcd0 --- /dev/null +++ b/packages/kbn-rrule/sanitize.ts @@ -0,0 +1,82 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Options } from './types'; + +export function sanitizeOptions(opts: Options) { + const options = { ...opts }; + + // Guard against invalid options that can't be omitted + if (!options.dtstart) { + throw new Error('Cannot create RRule: dtstart is required'); + } + + if (!options.tzid) { + throw new Error('Cannot create RRule: tzid is required'); + } + + if (isNaN(options.dtstart.getTime())) { + throw new Error('Cannot create RRule: dtstart is an invalid date'); + } + + if (options.until && isNaN(options.until.getTime())) { + throw new Error('Cannot create RRule: until is an invalid date'); + } + + if (options.interval != null) { + if (typeof options.interval !== 'number') { + throw new Error('Cannot create RRule: interval must be a number'); + } + + if (options.interval < 1) { + throw new Error('Cannot create RRule: interval must be greater than 0'); + } + } + + // Omit invalid options + if (options.bymonth) { + // Only months between 1 and 12 are valid + options.bymonth = options.bymonth.filter( + (month) => typeof month === 'number' && month >= 1 && month <= 12 + ); + if (!options.bymonth.length) { + delete options.bymonth; + } + } + + if (options.bymonthday) { + // Only days between 1 and 31 are valid + options.bymonthday = options.bymonthday.filter( + (day) => typeof day === 'number' && day >= 1 && day <= 31 + ); + if (!options.bymonthday.length) { + delete options.bymonthday; + } + } + + if (options.byweekday) { + // Only weekdays between 1 and 7 are valid + options.byweekday = options.byweekday.filter( + (weekday) => typeof weekday === 'number' && weekday >= 1 && weekday <= 7 + ); + if (!options.byweekday.length) { + delete options.byweekday; + } + } + + if (options.byyearday) { + // Only days between 1 and 366 are valid + options.byyearday = options.byyearday.filter((day) => day >= 1 && day <= 366); + if (!options.byyearday.length) { + delete options.byyearday; + } + } + + return options; +} diff --git a/packages/kbn-rrule/types.ts b/packages/kbn-rrule/types.ts new file mode 100644 index 0000000000000..8d8895fdfe91c --- /dev/null +++ b/packages/kbn-rrule/types.ts @@ -0,0 +1,54 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Moment } from 'moment'; + +export enum Frequency { + YEARLY = 0, + MONTHLY = 1, + WEEKLY = 2, + DAILY = 3, + HOURLY = 4, + MINUTELY = 5, + SECONDLY = 6, +} + +export enum Weekday { + MO = 1, + TU = 2, + WE = 3, + TH = 4, + FR = 5, + SA = 6, + SU = 7, +} + +export type WeekdayStr = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU'; + +export interface IterOptions { + refDT: Moment; + wkst?: Weekday | number | null; + byyearday?: number[] | null; + bymonth?: number[] | null; + bysetpos?: number[] | null; + bymonthday?: number[] | null; + byweekday?: Weekday[] | null; + byhour?: number[] | null; + byminute?: number[] | null; + bysecond?: number[] | null; +} + +export type Options = Omit & { + dtstart: Date; + freq?: Frequency; + interval?: number; + until?: Date | null; + count?: number; + tzid: string; +}; diff --git a/x-pack/plugins/alerting/common/routes/r_rule/request/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/r_rule/request/schemas/v1.ts index 43528f7c0f722..65425ea25d3d4 100644 --- a/x-pack/plugins/alerting/common/routes/r_rule/request/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/r_rule/request/schemas/v1.ts @@ -34,17 +34,28 @@ export const rRuleRequestSchema = schema.object({ }) ), byweekday: schema.maybe( - schema.arrayOf(schema.string(), { - validate: createValidateRecurrenceByV1('byweekday'), - }) + schema.arrayOf( + schema.oneOf([ + schema.literal('MO'), + schema.literal('TU'), + schema.literal('WE'), + schema.literal('TH'), + schema.literal('FR'), + schema.literal('SA'), + schema.literal('SU'), + ]), + { + validate: createValidateRecurrenceByV1('byweekday'), + } + ) ), bymonthday: schema.maybe( - schema.arrayOf(schema.number(), { + schema.arrayOf(schema.number({ min: 1, max: 31 }), { validate: createValidateRecurrenceByV1('bymonthday'), }) ), bymonth: schema.maybe( - schema.arrayOf(schema.number(), { + schema.arrayOf(schema.number({ min: 1, max: 12 }), { validate: createValidateRecurrenceByV1('bymonth'), }) ), diff --git a/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts b/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts index 0c83d16dee024..72c40cacd5f87 100644 --- a/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts +++ b/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts @@ -29,17 +29,28 @@ export const rRuleRequestSchema = schema.object({ }) ), byweekday: schema.maybe( - schema.arrayOf(schema.string(), { - validate: createValidateRecurrenceBy('byweekday'), - }) + schema.arrayOf( + schema.oneOf([ + schema.literal('MO'), + schema.literal('TU'), + schema.literal('WE'), + schema.literal('TH'), + schema.literal('FR'), + schema.literal('SA'), + schema.literal('SU'), + ]), + { + validate: createValidateRecurrenceBy('byweekday'), + } + ) ), bymonthday: schema.maybe( - schema.arrayOf(schema.number(), { + schema.arrayOf(schema.number({ min: 1, max: 31 }), { validate: createValidateRecurrenceBy('bymonthday'), }) ), bymonth: schema.maybe( - schema.arrayOf(schema.number(), { + schema.arrayOf(schema.number({ min: 1, max: 12 }), { validate: createValidateRecurrenceBy('bymonth'), }) ), diff --git a/x-pack/plugins/alerting/server/lib/snooze/is_snooze_active.test.ts b/x-pack/plugins/alerting/server/lib/snooze/is_snooze_active.test.ts index 9fdce331a7da9..da3915facc460 100644 --- a/x-pack/plugins/alerting/server/lib/snooze/is_snooze_active.test.ts +++ b/x-pack/plugins/alerting/server/lib/snooze/is_snooze_active.test.ts @@ -230,4 +230,25 @@ describe('isSnoozeActive', () => { expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`); fakeTimer.restore(); }); + + test('snooze still works with invalid bymonth value', () => { + // Set the current time as: + // - Feb 27 2023 08:15:00 GMT+0000 - Monday + fakeTimer = sinon.useFakeTimers(new Date('2023-02-09T08:15:00.000Z')); + + const snoozeA = { + duration: moment('2023-01', 'YYYY-MM').daysInMonth() * 24 * 60 * 60 * 1000, // 1 month + rRule: { + freq: Frequency.YEARLY, + interval: 1, + bymonthday: [1], + bymonth: [0], + tzid: 'Europe/Madrid', + dtstart: '2023-01-01T00:00:00.000Z', + } as RRuleRecord, + id: '9141dc1f-ed85-4656-91e4-119173105432', + }; + expect(isSnoozeActive(snoozeA)).toMatchInlineSnapshot(`null`); + fakeTimer.restore(); + }); }); diff --git a/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts index d65a323e84d5b..8c5792b37807d 100644 --- a/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts +++ b/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts @@ -18,7 +18,11 @@ import pRetry from 'p-retry'; const BEFORE_SETUP_TIMEOUT = 30 * 60 * 1000; // 30 minutes; const DOCKER_START_TIMEOUT = 5 * 60 * 1000; // 5 minutes -const DOCKER_IMAGE = `docker.elastic.co/package-registry/distribution:lite`; +// This image comes from the latest successful build of https://buildkite.com/elastic/kibana-package-registry-promote +// which is promoted after acceptance tests succeed against docker.elastic.co/package-registry/distribution:lite +const DOCKER_IMAGE = + process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || + 'docker.elastic.co/kibana-ci/package-registry-distribution:lite'; function firstWithTimeout(source$: Rx.Observable, errorMsg: string, ms = 30 * 1000) { return Rx.race( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index 16811a8429d18..44a6906d5dec0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -110,7 +110,7 @@ export function Cytoscape({ return () => { if (cy) { - cy.removeListener('data', undefined, dataHandler as cytoscape.EventHandler); + cy.removeListener('data', dataHandler); } }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/controls.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/controls.tsx index ac15281d61212..01248f0a55c75 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/controls.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/controls.tsx @@ -86,7 +86,7 @@ function useDebugDownloadUrl(cy?: cytoscape.Core) { return () => { if (cy) { - cy.off('add remove', undefined, elementsHandler); + cy.off('add remove', elementsHandler); } }; }, [cy, debug]); @@ -131,7 +131,7 @@ export function Controls() { return () => { if (cy) { - cy.off('zoom', undefined, zoomHandler); + cy.off('zoom', zoomHandler); } }; }, [cy]); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/cytoscape_options.ts b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/cytoscape_options.ts index af0befbc06165..efc1961034de2 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/cytoscape_options.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/cytoscape_options.ts @@ -174,15 +174,12 @@ const getStyle = (theme: EuiTheme, isTraceExplorerEnabled: boolean): cytoscape.S 'source-arrow-shape': isIE11 ? 'none' : 'triangle', 'source-arrow-color': lineColor, 'target-arrow-shape': isIE11 ? 'none' : 'triangle', - // @ts-expect-error 'source-distance-from-node': isIE11 ? undefined : parseInt(theme.eui.euiSizeXS, 10), 'target-distance-from-node': isIE11 ? undefined : parseInt(theme.eui.euiSizeXS, 10), }, }, { selector: 'edge[isInverseEdge]', - // @ts-expect-error DefinitelyTyped says visibility is "none" but it's - // actually "hidden" style: { visibility: 'hidden' }, }, { diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/popover/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/popover/index.tsx index a66f77909072d..8644b95a80444 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/popover/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/popover/index.tsx @@ -143,7 +143,7 @@ export function Popover({ focusedServiceName, environment, kuery, start, end }: if (cy) { cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', deselect); - cy.removeListener('viewport', undefined, deselect); + cy.removeListener('viewport', deselect); cy.removeListener('drag', 'node', deselect); cy.removeListener('select', 'edge', selectHandler); cy.removeListener('unselect', 'edge', deselect); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.ts b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.ts index e40ee3e80eaaa..f09ebd84989d5 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_map/use_cytoscape_event_handlers.ts @@ -49,7 +49,6 @@ function getLayoutOptions({ return { animationDuration: animationOptions.duration, - // @ts-expect-error upgrade typescript v5.1.6 animationEasing: animationOptions.easing, fit, name: 'dagre', @@ -61,7 +60,7 @@ function getLayoutOptions({ rankSep: 128, rankDir: 'LR', ranker: 'network-simplex', - }; + } as cytoscape.LayoutOptions; } function setCursor(cursor: string, event: cytoscape.EventObjectCore) { @@ -201,19 +200,18 @@ export function useCytoscapeEventHandlers({ if (cy) { cy.removeListener( 'custom:data drag dragfree layoutstop select tapstart tapend unselect', - undefined, debugHandler ); - cy.removeListener('custom:data', undefined, dataHandler); - cy.removeListener('layoutstop', undefined, layoutstopHandler); + cy.removeListener('custom:data', dataHandler); + cy.removeListener('layoutstop', layoutstopHandler); cy.removeListener('mouseover', 'edge, node', mouseoverHandler); cy.removeListener('mouseout', 'edge, node', mouseoutHandler); cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', unselectHandler); cy.removeListener('drag', 'node', dragHandler); cy.removeListener('dragfree', 'node', dragfreeHandler); - cy.removeListener('tapstart', undefined, tapstartHandler); - cy.removeListener('tapend', undefined, tapendHandler); + cy.removeListener('tapstart', tapstartHandler); + cy.removeListener('tapend', tapendHandler); } }; }, [cy, serviceName, trackApmEvent, theme]); diff --git a/x-pack/test/fleet_api_integration/config.base.ts b/x-pack/test/fleet_api_integration/config.base.ts index 1be78bcdc04df..852b0be4f82ac 100644 --- a/x-pack/test/fleet_api_integration/config.base.ts +++ b/x-pack/test/fleet_api_integration/config.base.ts @@ -15,10 +15,11 @@ import { const getFullPath = (relativePath: string) => path.join(path.dirname(__filename), relativePath); // Docker image to use for Fleet API integration tests. -// This hash comes from the latest successful build of the Production Distribution of the Package Registry, for -// example: https://internal-ci.elastic.co/blue/organizations/jenkins/package_storage%2Findexing-job/detail/main/1884/pipeline/147. -// It should be updated any time there is a new package published. -export const dockerImage = 'docker.elastic.co/package-registry/distribution:lite'; +// This image comes from the latest successful build of https://buildkite.com/elastic/kibana-package-registry-promote +// which is promoted after acceptance tests succeed against docker.elastic.co/package-registry/distribution:lite +export const dockerImage = + process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || + 'docker.elastic.co/kibana-ci/package-registry-distribution:lite'; export const BUNDLED_PACKAGE_DIR = '/tmp/fleet_bundled_packages'; diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index d5c6d77785b85..9924f29cd4bcb 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -11,10 +11,11 @@ import { services } from './services'; import { pageObjects } from './page_objects'; // Docker image to use for Fleet API integration tests. -// This hash comes from the latest successful build of the Production Distribution of the Package Registry, for -// example: https://internal-ci.elastic.co/blue/organizations/jenkins/package_storage%2Findexing-job/detail/main/1884/pipeline/147. -// It should be updated any time there is a new package published. -export const dockerImage = 'docker.elastic.co/package-registry/distribution:lite'; +// This image comes from the latest successful build of https://buildkite.com/elastic/kibana-package-registry-promote +// which is promoted after acceptance tests succeed against docker.elastic.co/package-registry/distribution:lite +export const dockerImage = + process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || + 'docker.elastic.co/kibana-ci/package-registry-distribution:lite'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/yarn.lock b/yarn.lock index af06edc7d6e06..ad45519a47413 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11238,10 +11238,10 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== -"@types/cytoscape@^3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.14.0.tgz#346b5430a7a1533784bcf44fcbe6c5255b948d36" - integrity sha512-qOhhZFGb35457vmCu9YHqwjqleraSAEikaIFmMUPWzTC7FrwkS7VR/6ymqRHW7Z8wivHuzsZDYCoePIepd69qg== +"@types/cytoscape@^3.21.8": + version "3.21.8" + resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.21.8.tgz#6166a2eabd66d3ae3259024875e037492adb8db6" + integrity sha512-6Bo9ZDrv0vfwe8Sg/ERc5VL0yU0gYvP4dgZi0fAXYkKHfyHaNqWRMcwYm3mu4sLsXbB8ZuXE75sR7qnaOL5JgQ== "@types/d3-array@^2.12.1": version "2.12.3" @@ -16350,20 +16350,17 @@ cypress@13.17.0: untildify "^4.0.0" yauzl "^2.10.0" -cytoscape-dagre@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz#5f32a85c0ba835f167efee531df9e89ac58ff411" - integrity sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw== +cytoscape-dagre@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz#47d9835ab64dd0b596d9c94731f070282f82fc5a" + integrity sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g== dependencies: - dagre "^0.8.2" + dagre "^0.8.5" -cytoscape@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.10.0.tgz#3b462e0d35121ecd2d2702f470915fd6dae01777" - integrity sha512-lWOnG4HJQD0cy+tCiBmbV/QRknInuZ8HvjcUifQZ7E4LWmKMvl6d5eP0LaaRLfBJAplXWcJfwc17ZJ/nwPeaYg== - dependencies: - heap "^0.2.6" - lodash.debounce "^4.0.8" +cytoscape@^3.30.4: + version "3.30.4" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.30.4.tgz#3404da0a159c00a1a3df2c85b2b43fdc66a0e28e" + integrity sha512-OxtlZwQl1WbwMmLiyPSEBuzeTIQnwZhJYYWFzZ2PhEHVFwpeaqNIkUzSiso00D98qk60l8Gwon2RP304d3BJ1A== "d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3.2.2, d3-array@^3.2.2: version "3.2.2" @@ -16621,7 +16618,7 @@ d3@3.5.17, d3@^3.5.6: resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g= -dagre@^0.8.2: +dagre@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw== @@ -20423,11 +20420,6 @@ headers-polyfill@^4.0.2: resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== -heap@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" - integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= - hexoid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-2.0.0.tgz#fb36c740ebbf364403fa1ec0c7efd268460ec5b9"