Time regions: Add option for cron syntax to support complex schedules (#99548)

Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
add-full-pr-check-for-docs
Leon Sorokin 4 months ago committed by GitHub
parent 2e82ac0cc1
commit 59280d5242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      package.json
  2. 4
      packages/grafana-data/src/datetime/durationutil.ts
  3. 1
      packages/grafana-data/src/index.ts
  4. 399
      public/app/core/utils/timeRegions.test.ts
  5. 235
      public/app/core/utils/timeRegions.ts
  6. 153
      public/app/plugins/datasource/grafana/components/TimeRegionEditor.tsx
  7. 9
      public/app/plugins/datasource/grafana/datasource.ts
  8. 49
      public/app/plugins/datasource/grafana/timeRegions.test.ts
  9. 56
      public/app/plugins/datasource/grafana/timeRegions.ts
  10. 372
      public/app/plugins/panel/graph/specs/time_region_manager.test.ts
  11. 6
      public/locales/en-US/grafana.json
  12. 6
      public/locales/pseudo-LOCALE/grafana.json
  13. 8
      yarn.lock

@ -320,6 +320,7 @@
"combokeys": "^3.0.0",
"comlink": "4.4.2",
"common-tags": "1.8.2",
"croner": "^9.0.0",
"d3": "7.9.0",
"d3-force": "3.0.0",
"d3-scale-chromatic": "3.1.0",

@ -26,6 +26,10 @@ export function intervalToAbbreviatedDurationString(interval: Interval, includeS
}
const duration = intervalToDuration(interval);
return reverseParseDuration(duration, includeSeconds);
}
export function reverseParseDuration(duration: Duration, includeSeconds: boolean): string {
return Object.entries(duration).reduce((str, [unit, value]) => {
if (value && value !== 0 && !(unit === 'seconds' && !includeSeconds && str)) {
const padding = str !== '' ? ' ' : '';

@ -403,6 +403,7 @@ export { type DateTimeOptionsWhenParsing, dateTimeParse } from './datetime/parse
export {
intervalToAbbreviatedDurationString,
parseDuration,
reverseParseDuration,
addDurationToDate,
durationToMilliseconds,
isValidDate,

@ -1,19 +1,84 @@
import { dateTime, TimeRange } from '@grafana/data';
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
import { Duration } from 'date-fns';
import { AbsoluteTimeRange, dateTimeForTimeZone, reverseParseDuration, TimeRange } from '@grafana/data';
import { convertToCron, TimeRegionConfig } from 'app/core/utils/timeRegions';
import { calculateTimesWithin } from './timeRegions';
// note: calculateTimesWithin always returns time ranges in UTC
// random from the interwebs
function durationFromSeconds(seconds: number): Duration {
const secondsInYear = 31536000;
const secondsInMonth = 2628000;
const secondsInDay = 86400;
const secondsInHour = 3600;
const secondsInMinute = 60;
let years = Math.floor(seconds / secondsInYear);
let remainingSeconds = seconds % secondsInYear;
let months = Math.floor(remainingSeconds / secondsInMonth);
remainingSeconds %= secondsInMonth;
let days = Math.floor(remainingSeconds / secondsInDay);
remainingSeconds %= secondsInDay;
let hours = Math.floor(remainingSeconds / secondsInHour);
remainingSeconds %= secondsInHour;
let minutes = Math.floor(remainingSeconds / secondsInMinute);
let finalSeconds = remainingSeconds % secondsInMinute;
return {
years,
months,
days,
hours,
minutes,
seconds: finalSeconds,
};
}
function tsToDayOfWeek(ts: number, tz?: string) {
return new Date(ts).toLocaleString('en', {
timeZone: tz,
weekday: 'short',
});
}
function tsToDateTimeString(ts: number, tz?: string) {
return new Date(ts).toLocaleString('sv', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: tz,
timeZoneName: 'short',
});
}
function formatAbsoluteRange(range: AbsoluteTimeRange, tz?: string) {
return {
fr: `${tsToDayOfWeek(range.from, tz)} | ${tsToDateTimeString(range.from, tz)}`.replaceAll('−', '-'),
to: `${tsToDayOfWeek(range.to, tz)} | ${tsToDateTimeString(range.to, tz)}`.replaceAll('−', '-'),
};
}
describe('timeRegions', () => {
describe('day of week', () => {
it('returns regions with 4 Mondays in March 2023', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
};
const tr: TimeRange = {
from: dateTime('2023-03-01'),
to: dateTime('2023-03-31'),
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
@ -21,38 +86,41 @@ describe('timeRegions', () => {
};
const regions = calculateTimesWithin(cfg, tr);
expect(regions).toMatchInlineSnapshot(`
[
{
"from": 1678060800000,
"to": 1678147199000,
},
{
"from": 1678665600000,
"to": 1678751999000,
},
{
"from": 1679270400000,
"to": 1679356799000,
},
{
"from": 1679875200000,
"to": 1679961599000,
},
]
`);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-03-06 00:00:00 GMT-6',
to: 'Tue | 2023-03-07 00:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 00:00:00 GMT-5',
to: 'Tue | 2023-03-14 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 00:00:00 GMT-5',
to: 'Tue | 2023-03-21 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 00:00:00 GMT-5',
to: 'Tue | 2023-03-28 00:00:00 GMT-5',
},
]);
});
});
describe('day and time of week', () => {
it('returns regions with 4 Mondays at 20:00 in March 2023', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
from: '20:00',
};
const tr: TimeRange = {
from: dateTime('2023-03-01'),
to: dateTime('2023-03-31'),
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
@ -60,38 +128,86 @@ describe('timeRegions', () => {
};
const regions = calculateTimesWithin(cfg, tr);
expect(regions).toMatchInlineSnapshot(`
[
{
"from": 1678132800000,
"to": 1678132800000,
},
{
"from": 1678737600000,
"to": 1678737600000,
},
{
"from": 1679342400000,
"to": 1679342400000,
},
{
"from": 1679947200000,
"to": 1679947200000,
},
]
`);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-03-06 20:00:00 GMT-6',
to: 'Mon | 2023-03-06 20:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 20:00:00 GMT-5',
to: 'Mon | 2023-03-13 20:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 20:00:00 GMT-5',
to: 'Mon | 2023-03-20 20:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 20:00:00 GMT-5',
to: 'Mon | 2023-03-27 20:00:00 GMT-5',
},
]);
});
});
describe('day of week range', () => {
it('returns regions with days range', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
toDayOfWeek: 3,
};
const tr: TimeRange = {
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
},
};
const regions = calculateTimesWithin(cfg, tr);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-02-27 00:00:00 GMT-6',
to: 'Thu | 2023-03-02 00:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-06 00:00:00 GMT-6',
to: 'Thu | 2023-03-09 00:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 00:00:00 GMT-5',
to: 'Thu | 2023-03-16 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 00:00:00 GMT-5',
to: 'Thu | 2023-03-23 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 00:00:00 GMT-5',
to: 'Thu | 2023-03-30 00:00:00 GMT-5',
},
]);
});
it('returns regions with days range (browser time zone)', () => {
const dashboardTz = process.env.TZ;
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
toDayOfWeek: 3,
};
const tr: TimeRange = {
from: dateTime('2023-03-01'),
to: dateTime('2023-03-31'),
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
@ -99,29 +215,37 @@ describe('timeRegions', () => {
};
const regions = calculateTimesWithin(cfg, tr);
expect(regions).toMatchInlineSnapshot(`
[
{
"from": 1678060800000,
"to": 1678319999000,
},
{
"from": 1678665600000,
"to": 1678924799000,
},
{
"from": 1679270400000,
"to": 1679529599000,
},
{
"from": 1679875200000,
"to": 1680134399000,
},
]
`);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-02-27 00:00:00 GMT-5',
to: 'Thu | 2023-03-02 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-06 00:00:00 GMT-5',
to: 'Thu | 2023-03-09 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-13 00:00:00 GMT-5',
to: 'Thu | 2023-03-16 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 00:00:00 GMT-5',
to: 'Thu | 2023-03-23 00:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 00:00:00 GMT-5',
to: 'Thu | 2023-03-30 00:00:00 GMT-5',
},
]);
});
it('returns regions with days/times range', () => {
const dashboardTz = 'America/Chicago';
const regionsTz = dashboardTz;
const cfg: TimeRegionConfig = {
timezone: regionsTz,
fromDayOfWeek: 1,
from: '20:00',
toDayOfWeek: 2,
@ -129,8 +253,8 @@ describe('timeRegions', () => {
};
const tr: TimeRange = {
from: dateTime('2023-03-01'),
to: dateTime('2023-03-31'),
from: dateTimeForTimeZone(dashboardTz, '2023-03-01'),
to: dateTimeForTimeZone(dashboardTz, '2023-03-31'),
raw: {
to: '',
from: '',
@ -138,26 +262,119 @@ describe('timeRegions', () => {
};
const regions = calculateTimesWithin(cfg, tr);
expect(regions).toMatchInlineSnapshot(`
[
{
"from": 1678132800000,
"to": 1678183200000,
},
{
"from": 1678737600000,
"to": 1678788000000,
},
{
"from": 1679342400000,
"to": 1679392800000,
},
{
"from": 1679947200000,
"to": 1679997600000,
},
]
`);
const formatted = regions.map((r) => formatAbsoluteRange(r, regionsTz));
expect(formatted).toEqual([
{
fr: 'Mon | 2023-03-06 20:00:00 GMT-6',
to: 'Tue | 2023-03-07 10:00:00 GMT-6',
},
{
fr: 'Mon | 2023-03-13 20:00:00 GMT-5',
to: 'Tue | 2023-03-14 10:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-20 20:00:00 GMT-5',
to: 'Tue | 2023-03-21 10:00:00 GMT-5',
},
{
fr: 'Mon | 2023-03-27 20:00:00 GMT-5',
to: 'Tue | 2023-03-28 10:00:00 GMT-5',
},
]);
});
});
type TestDef = [
name: string,
fromDayOfWeek: number | null,
from: string | null,
toDayOfWeek: number | null,
to: string | null,
cronExpr: string,
duration: string,
];
let _ = null;
describe('various scenarios (regions)', () => {
/* eslint-disable */
// prettier-ignore
let tests: TestDef[] = [
['from every day (time before) to every day (time after)', _, '10:27', _, '14:27', '27 10 * * *', '4h'],
['from every day (time after) to every day (time before)', _, '22:27', _, '02:27', '27 22 * * *', '4h'],
['from every day (time) to every day (no time)', _, '10:27', _, _, '27 10 * * *', ''],
['from fri (no time)', 5, _, _, _, '0 0 * * 5', '1d'],
['from fri (no time) to tues (no time)', 5, _, 2, _, '0 0 * * 5', '5d'],
['from fri (no time) to tues (time)', 5, _, 2, '02:27', '0 0 * * 5', '4d 2h 27m'],
['from fri (time) to tues (no time)', 5, '10:27', 2, _, '27 10 * * 5', '4d'],
['from fri (time) to tues (time)', 5, '10:27', 2, '14:27', '27 10 * * 5', '4d 4h'],
// same day
['from fri (time before) to fri (time after)', 5, '10:27', 5, '14:27', '27 10 * * 5', '4h'],
// "toDay" should assume Fri
['from fri (time before) to every day (time after)', 5, '10:27', _, '14:27', '27 10 * * 5', '4h'],
// wrap-around case
['from fri (time after) to fri (time before)', 5, '14:27', 5, '10:27', '27 14 * * 5', '6d 20h'],
];
/* eslint-enable */
tests.forEach(([name, fromDayOfWeek, from, toDayOfWeek, to, cronExpr, duration]) => {
it(name, () => {
const cron = convertToCron(fromDayOfWeek, from, toDayOfWeek, to);
expect(cron).not.toBeUndefined();
expect(cron?.cronExpr).toEqual(cronExpr);
expect(reverseParseDuration(durationFromSeconds(cron?.duration ?? 0), false)).toEqual(duration);
});
});
});
describe('various scenarios (points)', () => {
/* eslint-disable */
// prettier-ignore
let tests: TestDef[] = [
['from every day (time)', _, '10:03', _, _, '3 10 * * *', ''],
['from every day (time) to every day (time)', _, '10:03', _, '10:03', '3 10 * * *', ''],
['from tues (time)', 2, '10:03', _, _, '3 10 * * 2', ''],
['from tues (time) to tues (time)', 2, '10:03', _, '10:03', '3 10 * * 2', ''],
];
/* eslint-enable */
tests.forEach(([name, fromDayOfWeek, from, toDayOfWeek, to, cronExpr, duration]) => {
it(name, () => {
const cron = convertToCron(fromDayOfWeek, from, toDayOfWeek, to);
expect(cron).not.toBeUndefined();
expect(cron?.cronExpr).toEqual(cronExpr);
expect(reverseParseDuration(durationFromSeconds(cron?.duration ?? 0), false)).toEqual(duration);
});
});
});
describe('convert simple time region config to cron string and duration', () => {
it.each`
from | fromDOW | to | toDOW | timezone | expectedCron | expectedDuration
${'03:03'} | ${1} | ${'03:03'} | ${2} | ${'browser'} | ${'3 3 * * 1'} | ${'1d'}
${'03:03'} | ${7} | ${'03:03'} | ${1} | ${'browser'} | ${'3 3 * * 7'} | ${'1d'}
${'09:03'} | ${7} | ${'03:03'} | ${1} | ${'browser'} | ${'3 9 * * 7'} | ${'18h'}
${'03:03'} | ${7} | ${'04:03'} | ${7} | ${'browser'} | ${'3 3 * * 7'} | ${'1h'}
${'03:03'} | ${7} | ${'02:03'} | ${7} | ${'browser'} | ${'3 3 * * 7'} | ${'6d 23h'}
${'03:03'} | ${7} | ${'3:03'} | ${7} | ${'browser'} | ${'3 3 * * 7'} | ${''}
`(
"time region config with from time '$from' and DOW '$fromDOW', to: '$to' and DOW '$toDOW' should generate a cron string of '$expectedCron' and '$expectedDuration'",
({ from, fromDOW, to, toDOW, timezone, expectedCron, expectedDuration }) => {
const timeConfig: TimeRegionConfig = { from, fromDayOfWeek: fromDOW, to, toDayOfWeek: toDOW, timezone };
const convertedCron = convertToCron(
timeConfig.fromDayOfWeek,
timeConfig.from,
timeConfig.toDayOfWeek,
timeConfig.to
)!;
expect(convertedCron).not.toBeUndefined();
expect(convertedCron.cronExpr).toEqual(expectedCron);
expect(reverseParseDuration(durationFromSeconds(convertedCron.duration), false)).toEqual(expectedDuration);
}
);
});
});

@ -1,6 +1,11 @@
import { AbsoluteTimeRange, dateTime, TimeRange } from '@grafana/data';
import { Cron } from 'croner';
import { AbsoluteTimeRange, TimeRange, durationToMilliseconds, parseDuration } from '@grafana/data';
export type TimeRegionMode = null | 'cron';
export interface TimeRegionConfig {
mode?: TimeRegionMode;
from?: string;
fromDayOfWeek?: number; // 1-7
@ -8,165 +13,155 @@ export interface TimeRegionConfig {
toDayOfWeek?: number; // 1-7
timezone?: string;
}
interface ParsedTime {
dayOfWeek?: number; // 1-7
h?: number; // 0-23
m?: number; // 0-59
s?: number; // 0-59
cronExpr?: string; // 0 9 * * 1-5
duration?: string; // 8h
}
export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange): AbsoluteTimeRange[] {
if (!(cfg.fromDayOfWeek || cfg.from) && !(cfg.toDayOfWeek || cfg.to)) {
return [];
}
const secsInDay = 24 * 3600;
// So we can mutate
const timeRegion = { ...cfg };
function getDurationSecs(
fromDay: number,
fromHour: number,
fromMin: number,
toDay: number,
toHour: number,
toMin: number
) {
let days = toDay - fromDay;
if (timeRegion.from && !timeRegion.to) {
timeRegion.to = timeRegion.from;
// account for rollover
if (days < 0) {
days += 7;
}
if (!timeRegion.from && timeRegion.to) {
timeRegion.from = timeRegion.to;
}
let fromSecs = fromHour * 3600 + fromMin * 60;
let toSecs = toHour * 3600 + toMin * 60;
const hRange = {
from: parseTimeOfDay(timeRegion.from),
to: parseTimeOfDay(timeRegion.to),
};
let durSecs = 0;
if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) {
timeRegion.fromDayOfWeek = timeRegion.toDayOfWeek;
// account for toTime < fromTime on same day
if (days === 0 && toSecs < fromSecs) {
durSecs = 7 * secsInDay - (fromSecs - toSecs);
} else {
let daysSecs = days * secsInDay;
durSecs = daysSecs - fromSecs + toSecs;
}
if (!timeRegion.toDayOfWeek && timeRegion.fromDayOfWeek) {
timeRegion.toDayOfWeek = timeRegion.fromDayOfWeek;
}
return durSecs;
}
if (timeRegion.fromDayOfWeek) {
hRange.from.dayOfWeek = Number(timeRegion.fromDayOfWeek);
export function convertToCron(
fromDay?: number | null,
from?: string | null,
toDay?: number | null,
to?: string | null
) {
// valid defs must have a "from"
if (fromDay == null && from == null) {
return undefined;
}
if (timeRegion.toDayOfWeek) {
hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek);
}
const cronCfg = {
cronExpr: '',
duration: 0,
};
if (hRange.from.dayOfWeek && hRange.from.h == null && hRange.from.m == null) {
hRange.from.h = 0;
hRange.from.m = 0;
hRange.from.s = 0;
}
const isEveryDay = fromDay == null && toDay == null;
// if the def contains only days of week, then they become end-day-inclusive
const toDayEnd = fromDay != null && from == null && to == null;
if (hRange.to.dayOfWeek && hRange.to.h == null && hRange.to.m == null) {
hRange.to.h = 23;
hRange.to.m = 59;
hRange.to.s = 59;
}
from ??= '00:00';
if (!hRange.from || !hRange.to) {
return [];
}
// 1. create cron (only requires froms)
let [fromHour, fromMin] = from.split(':').map((v) => +v);
if (hRange.from.h == null) {
hRange.from.h = 0;
}
cronCfg.cronExpr = `${fromMin} ${fromHour} * * ${fromDay ?? '*'}`;
// 2. determine duration
fromDay ??= 1;
toDay ??= fromDay;
if (hRange.to.h == null) {
hRange.to.h = 23;
// e.g. from Wed to Fri (implies inclusive Fri)
if (toDayEnd) {
to = '00:00';
toDay += toDay === 7 ? -6 : 1;
}
const regions: AbsoluteTimeRange[] = [];
to ??= from;
const fromStart = dateTime(tRange.from).utc();
fromStart.set('hour', 0);
fromStart.set('minute', 0);
fromStart.set('second', 0);
fromStart.set('millisecond', 0);
fromStart.add(hRange.from.h, 'hours');
fromStart.add(hRange.from.m, 'minutes');
fromStart.add(hRange.from.s, 'seconds');
let [toHour, toMin] = to.split(':').map((v) => +v);
while (fromStart.unix() <= tRange.to.unix()) {
while (hRange.from.dayOfWeek && hRange.from.dayOfWeek !== fromStart.isoWeekday()) {
fromStart.add(24, 'hours');
}
let fromSecs = fromHour * 3600 + fromMin * 60;
let toSecs = toHour * 3600 + toMin * 60;
if (fromStart.unix() > tRange.to.unix()) {
break;
}
// e.g. every day from 22:00 to 02:00 (implied next day)
// NOTE: the odd wrap-around case of toSecs < fromSecs in same day is handled inside getDurationSecs()
if (isEveryDay && toSecs < fromSecs) {
toDay += toDay === 7 ? -6 : 1;
}
const fromEnd = dateTime(fromStart).utc();
cronCfg.duration = getDurationSecs(fromDay, fromHour, fromMin, toDay, toHour, toMin);
if (fromEnd.hour) {
if (hRange.from.h <= hRange.to.h) {
fromEnd.add(hRange.to.h - hRange.from.h, 'hours');
} else if (hRange.from.h > hRange.to.h) {
while (fromEnd.hour() !== hRange.to.h) {
fromEnd.add(1, 'hours');
}
} else {
fromEnd.add(24 - hRange.from.h, 'hours');
return cronCfg;
}
while (fromEnd.hour() !== hRange.to.h) {
fromEnd.add(1, 'hours');
}
}
}
export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange): AbsoluteTimeRange[] {
const ranges: AbsoluteTimeRange[] = [];
fromEnd.set('minute', hRange.to.m ?? 0);
fromEnd.set('second', hRange.to.s ?? 0);
let cronExpr = '';
let durationMs = 0;
while (hRange.to.dayOfWeek && hRange.to.dayOfWeek !== fromEnd.isoWeekday()) {
fromEnd.add(24, 'hours');
}
let { fromDayOfWeek, from, toDayOfWeek, to, duration = '' } = cfg;
if (cfg.mode === 'cron') {
cronExpr = cfg.cronExpr ?? '';
durationMs = durationToMilliseconds(parseDuration(duration));
} else {
// remove empty strings
from = from === '' ? undefined : from;
to = to === '' ? undefined : to;
const outsideRange =
(fromStart.unix() < tRange.from.unix() && fromEnd.unix() < tRange.from.unix()) ||
(fromStart.unix() > tRange.to.unix() && fromEnd.unix() > tRange.to.unix());
const cron = convertToCron(fromDayOfWeek, from, toDayOfWeek, to);
if (!outsideRange) {
regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() });
if (cron != null) {
cronExpr = cron.cronExpr;
durationMs = cron.duration * 1e3;
}
}
fromStart.add(24, 'hours');
if (cronExpr === '') {
return [];
}
return regions;
}
try {
let tz = cfg.timezone === 'browser' ? undefined : cfg.timezone === 'utc' ? 'Etc/UTC' : cfg.timezone;
export function parseTimeOfDay(str?: string): ParsedTime {
const result: ParsedTime = {};
if (!str?.length) {
return result;
}
let job = new Cron(cronExpr, { timezone: tz });
const match = str.split(':');
if (!match?.length) {
return result;
}
// get previous run that may overlap with start of timerange
let fromDate: Date | null = new Date(tRange.from.valueOf() - durationMs);
result.h = Math.min(23, Math.max(0, Number(match[0])));
if (match.length > 1) {
result.m = Math.min(60, Math.max(0, Number(match[1])));
if (match.length > 2) {
result.s = Math.min(60, Math.max(0, Number(match[2])));
}
}
return result;
}
let toMs = tRange.to.valueOf();
let nextDate = job.nextRun(fromDate);
export function formatTimeOfDayString(t?: ParsedTime): string {
if (!t || (t.h == null && t.m == null && t.s == null)) {
return '';
}
while (nextDate != null) {
let nextMs = +nextDate;
let str = String(t.h ?? 0).padStart(2, '0') + ':' + String(t.m ?? 0).padStart(2, '0');
if (t.s != null) {
str += String(t.s ?? 0).padStart(2, '0');
if (nextMs > toMs) {
break;
} else {
ranges.push({
from: nextMs,
to: nextMs + durationMs,
});
nextDate = job.nextRun(nextDate);
}
}
} catch (e) {
// invalid expression
console.error(e);
}
return str;
return ranges;
}

@ -1,12 +1,24 @@
import { css } from '@emotion/css';
import moment, { Moment } from 'moment/moment';
import { useState } from 'react';
import { ChangeEvent, useState } from 'react';
import { dateTimeAsMoment, getTimeZoneInfo, GrafanaTheme2, isDateTime, SelectableValue } from '@grafana/data';
import { Button, Field, FieldSet, Select, Stack, TimeOfDayPicker, TimeZonePicker, useStyles2 } from '@grafana/ui';
import {
Button,
Field,
FieldSet,
Input,
Select,
Stack,
Switch,
TimeOfDayPicker,
TimeZonePicker,
useStyles2,
} from '@grafana/ui';
import { TimeZoneOffset } from '@grafana/ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneOffset';
import { TimeZoneTitle } from '@grafana/ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneTitle';
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
import { t } from 'app/core/internationalization';
import { TimeRegionConfig, TimeRegionMode } from 'app/core/utils/timeRegions';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
interface Props {
@ -20,6 +32,7 @@ const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
value: idx + 1,
};
});
export const TimeRegionEditor = ({ value, onChange }: Props) => {
const styles = useStyles2(getStyles);
const timestamp = Date.now();
@ -85,6 +98,18 @@ export const TimeRegionEditor = ({ value, onChange }: Props) => {
onChange({ ...value, timezone: v });
};
const onModeChange = (v: TimeRegionMode) => {
onChange({ ...value, mode: v });
};
const onCronExprChange = (v: string) => {
onChange({ ...value, cronExpr: v });
};
const onDurationChange = (v: string) => {
onChange({ ...value, duration: v });
};
const onFromDayOfWeekChange = (v: SelectableValue<number>) => {
const fromDayOfWeek = v ? v.value : undefined;
const toDayOfWeek = v ? value.toDayOfWeek : undefined; // clear if everyday
@ -125,46 +150,92 @@ export const TimeRegionEditor = ({ value, onChange }: Props) => {
return (
<FieldSet className={styles.wrapper}>
<Field label="From">
<Stack gap={0.5}>
<Select
options={days}
isClearable
placeholder="Everyday"
value={value.fromDayOfWeek ?? null}
onChange={(v) => onFromDayOfWeekChange(v)}
width={20}
/>
<TimeOfDayPicker
value={isDateTime(from) ? from : undefined}
onChange={(v) => onTimeChange(v ? dateTimeAsMoment(v) : v, 'from')}
allowEmpty={true}
placeholder="HH:mm"
size="sm"
/>
</Stack>
<Field
label={t('dashboard-settings.time-regions.advanced-label', 'Advanced')}
description={
<>
{t('dashboard-settings.time-regions.advanced-description-use', 'Use ')}
<a href="https://crontab.run/" target="_blank">
{t('dashboard-settings.time-regions.advanced-description-cron', 'Cron syntax')}
</a>
{t(
'dashboard-settings.time-regions.advanced-description-rest',
' to define a recurrence schedule and duration'
)}
</>
}
>
<Switch
id="time-regions-adanced-mode-toggle"
value={value.mode === 'cron'}
onChange={(e: ChangeEvent<HTMLInputElement>) => onModeChange(e.currentTarget.checked ? 'cron' : null)}
/>
</Field>
<Field label="To">
<Stack gap={0.5}>
{(value.fromDayOfWeek || value.toDayOfWeek) && (
<Select
options={days}
isClearable
placeholder={getToPlaceholder()}
value={value.toDayOfWeek ?? null}
onChange={(v) => onToDayOfWeekChange(v)}
width={20}
{value.mode == null && (
<>
<Field label="From">
<Stack gap={0.5}>
<Select
options={days}
isClearable
placeholder="Everyday"
value={value.fromDayOfWeek ?? null}
onChange={(v) => onFromDayOfWeekChange(v)}
width={20}
/>
<TimeOfDayPicker
value={isDateTime(from) ? from : undefined}
onChange={(v) => onTimeChange(v ? dateTimeAsMoment(v) : v, 'from')}
allowEmpty={true}
placeholder="HH:mm"
size="sm"
/>
</Stack>
</Field>
<Field label="To">
<Stack gap={0.5}>
{(value.fromDayOfWeek || value.toDayOfWeek) && (
<Select
options={days}
isClearable
placeholder={getToPlaceholder()}
value={value.toDayOfWeek ?? null}
onChange={(v) => onToDayOfWeekChange(v)}
width={20}
/>
)}
<TimeOfDayPicker
value={isDateTime(to) ? to : undefined}
onChange={(v) => onTimeChange(v ? dateTimeAsMoment(v) : v, 'to')}
allowEmpty={true}
placeholder="HH:mm"
size="sm"
/>
</Stack>
</Field>
</>
)}
{value.mode === 'cron' && (
<>
<Field label="Cron expression">
<Input
onChange={(e: ChangeEvent<HTMLInputElement>) => onCronExprChange(e.target.value)}
value={value.cronExpr}
placeholder="0 9 * * 1-5"
width={40}
/>
)}
<TimeOfDayPicker
value={isDateTime(to) ? to : undefined}
onChange={(v) => onTimeChange(v ? dateTimeAsMoment(v) : v, 'to')}
allowEmpty={true}
placeholder="HH:mm"
size="sm"
/>
</Stack>
</Field>
</Field>
<Field label="Duration">
<Input
onChange={(e: ChangeEvent<HTMLInputElement>) => onDurationChange(e.target.value)}
value={value.duration}
placeholder="8h"
width={40}
/>
</Field>
</>
)}
<Field label="Timezone">{renderTimezone()}</Field>
</FieldSet>
);

@ -108,7 +108,7 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
continue;
}
if (target.queryType === GrafanaQueryType.TimeRegions) {
const frame = doTimeRegionQuery('', target.timeRegion!, request.range, request.timezone);
const frame = doTimeRegionQuery('', target.timeRegion!, request.range);
results.push(
of({
data: frame ? [frame] : [],
@ -195,12 +195,7 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
async getAnnotations(options: AnnotationQueryRequest<GrafanaQuery>): Promise<DataQueryResponse> {
const query = options.annotation.target as GrafanaQuery;
if (query?.queryType === GrafanaQueryType.TimeRegions) {
const frame = doTimeRegionQuery(
options.annotation.name,
query.timeRegion!,
options.range,
getDashboardSrv().getCurrent()?.timezone // Annotation queries don't include the timezone
);
const frame = doTimeRegionQuery(options.annotation.name, query.timeRegion!, options.range);
return Promise.resolve({ data: frame ? [frame] : [] });
}

@ -6,7 +6,7 @@ describe('grafana data source', () => {
it('supports time region query', () => {
const frame = doTimeRegionQuery(
'test',
{ fromDayOfWeek: 1, toDayOfWeek: 2 },
{ fromDayOfWeek: 1, toDayOfWeek: 2, timezone: 'utc' },
{
from: dateTime('2023-03-01'),
to: dateTime('2023-03-31'),
@ -14,8 +14,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'utc'
}
);
expect(toDataFrameDTO(frame!)).toMatchInlineSnapshot(`
@ -39,10 +38,10 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678233599000,
1678838399000,
1679443199000,
1680047999000,
1678233600000,
1678838400000,
1679443200000,
1680048000000,
],
},
{
@ -80,8 +79,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'utc'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -101,7 +99,7 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678147199000,
1678147200000,
],
},
{
@ -133,8 +131,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'utc'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -154,7 +151,7 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678165199000,
1678165200000,
],
},
{
@ -186,8 +183,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'utc'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -207,7 +203,7 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678168799000,
1678168800000,
],
},
{
@ -239,8 +235,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'utc'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -260,7 +255,7 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678143599000,
1678143600000,
],
},
{
@ -292,8 +287,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'utc'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -313,7 +307,7 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678121999000,
1678122000000,
],
},
{
@ -345,8 +339,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'Asia/Dubai'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -366,7 +359,7 @@ describe('grafana data source', () => {
"name": "timeEnd",
"type": "time",
"values": [
1678147199000,
1678147200000,
],
},
{
@ -398,8 +391,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'America/Chicago'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
@ -451,8 +443,7 @@ describe('grafana data source', () => {
to: '',
from: '',
},
},
'America/Chicago'
}
);
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`

@ -1,48 +1,28 @@
import { TimeRange, DataFrame, FieldType, getTimeZoneInfo } from '@grafana/data';
import { TimeRange, DataFrame, FieldType } from '@grafana/data';
import { TimeRegionConfig, calculateTimesWithin } from 'app/core/utils/timeRegions';
export function doTimeRegionQuery(
name: string,
config: TimeRegionConfig,
range: TimeRange,
tz: string
): DataFrame | undefined {
if (!config) {
return undefined;
}
const regions = calculateTimesWithin(config, range); // UTC
if (!regions.length) {
return undefined;
}
export function doTimeRegionQuery(name: string, config: TimeRegionConfig, range: TimeRange): DataFrame | undefined {
const { mode, duration, cronExpr, from, fromDayOfWeek } = config;
const times: number[] = [];
const timesEnd: number[] = [];
const texts: string[] = [];
const isValidSimple = mode == null && (fromDayOfWeek != null || from != null);
const isValidAdvanced = mode === 'cron' && cronExpr != null && duration != null;
const regionTimezone = config.timezone ?? tz;
if (isValidSimple || isValidAdvanced) {
const ranges = calculateTimesWithin(config, range);
for (const region of regions) {
let from = region.from;
let to = region.to;
if (ranges.length > 0) {
const frame: DataFrame = {
fields: [
{ name: 'time', type: FieldType.time, values: ranges.map((r) => r.from), config: {} },
{ name: 'timeEnd', type: FieldType.time, values: ranges.map((r) => r.to), config: {} },
{ name: 'text', type: FieldType.string, values: Array(ranges.length).fill(name), config: {} },
],
length: ranges.length,
};
const info = getTimeZoneInfo(regionTimezone, from);
if (info) {
const offset = info.offsetInMins * 60 * 1000;
from += offset;
to += offset;
return frame;
}
times.push(from);
timesEnd.push(to);
texts.push(name);
}
return {
fields: [
{ name: 'time', type: FieldType.time, values: times, config: {} },
{ name: 'timeEnd', type: FieldType.time, values: timesEnd, config: {} },
{ name: 'text', type: FieldType.string, values: texts, config: {} },
],
length: times.length,
};
return undefined;
}

@ -1,372 +0,0 @@
import { dateTime } from '@grafana/data';
import { TimeRegionManager, colorModes } from '../time_region_manager';
describe('TimeRegionManager', () => {
function plotOptionsScenario(desc: string, func: any) {
describe(desc, () => {
const ctx: any = {
panel: {
timeRegions: [],
},
options: {
grid: { markings: [] },
},
panelCtrl: {
range: {},
dashboard: {},
},
};
ctx.setup = (regions: any, from: any, to: any) => {
ctx.panel.timeRegions = regions;
ctx.panelCtrl.range.from = from;
ctx.panelCtrl.range.to = to;
const manager = new TimeRegionManager(ctx.panelCtrl);
manager.addFlotOptions(ctx.options, ctx.panel);
};
ctx.printScenario = () => {
console.log(
`Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`,
ctx.panelCtrl.range.from._isUTC
);
ctx.options.grid.markings.forEach((m: any, i: number) => {
console.log(
`Marking (${i}): from=${dateTime(m.xaxis.from).format()}, to=${dateTime(m.xaxis.to).format()}, color=${
m.color
}`
);
});
};
func(ctx);
});
}
describe('When colors missing in config', () => {
plotOptionsScenario('should not throw an error when fillColor is undefined', (ctx: any) => {
const regions = [
{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, lineColor: '#ffffff', colorMode: 'custom' },
];
const from = dateTime('2018-01-01T00:00:00+01:00');
const to = dateTime('2018-01-01T23:59:00+01:00');
expect(() => ctx.setup(regions, from, to)).not.toThrow();
});
plotOptionsScenario('should not throw an error when lineColor is undefined', (ctx: any) => {
const regions = [
{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, fillColor: '#ffffff', line: true, colorMode: 'custom' },
];
const from = dateTime('2018-01-01T00:00:00+01:00');
const to = dateTime('2018-01-01T23:59:00+01:00');
expect(() => ctx.setup(regions, from, to)).not.toThrow();
});
});
describe('When creating plot markings using local time', () => {
plotOptionsScenario('for day of week region', (ctx: any) => {
const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }];
const from = dateTime('2018-01-01T00:00:00+01:00');
const to = dateTime('2018-01-01T23:59:00+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add fill', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-01T01:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-02T00:59:59+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
});
it('should add line before', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-01T01:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-01T01:00:00+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.line);
});
it('should add line after', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-02T00:59:59+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-02T00:59:59+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.line);
});
});
plotOptionsScenario('for time from region', (ctx: any) => {
const regions = [{ from: '05:00', fill: true, colorMode: 'red' }];
const from = dateTime('2018-01-01T00:00+01:00');
const to = dateTime('2018-01-03T23:59+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at 05:00 each day', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-01T06:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-01T06:00:00+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-02T06:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-02T06:00:00+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-03T06:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-03T06:00:00+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for time to region', (ctx: any) => {
const regions = [{ to: '05:00', fill: true, colorMode: 'red' }];
const from = dateTime('2018-02-01T00:00+01:00');
const to = dateTime('2018-02-03T23:59+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at 05:00 each day', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-02-01T06:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-02-01T06:00:00+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-02-02T06:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-02-02T06:00:00+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-02-03T06:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-02-03T06:00:00+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for time from/to region', (ctx: any) => {
const regions = [{ from: '00:00', to: '05:00', fill: true, colorMode: 'red' }];
const from = dateTime('2018-12-01T00:00+01:00');
const to = dateTime('2018-12-03T23:59+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill between 00:00 and 05:00 each day', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-01T01:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-01T06:00:00+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-12-02T01:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-12-02T06:00:00+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-12-03T01:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-12-03T06:00:00+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for time from/to region crossing midnight', (ctx: any) => {
const regions = [{ from: '22:00', to: '00:30', fill: true, colorMode: 'red' }];
const from = dateTime('2018-12-01T12:00+01:00');
const to = dateTime('2018-12-04T08:00+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill between 22:00 and 00:30 each day', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-01T23:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-02T01:30:00+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-12-02T23:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-12-03T01:30:00+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-12-03T23:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-12-04T01:30:00+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for day of week from/to region', (ctx: any) => {
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
const from = dateTime('2018-01-01T18:45:05+01:00');
const to = dateTime('2018-01-22T08:27:00+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at each sunday', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-07T01:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-08T00:59:59+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-14T01:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-15T00:59:59+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-21T01:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-22T00:59:59+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for day of week from region', (ctx: any) => {
const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }];
const from = dateTime('2018-01-01T18:45:05+01:00');
const to = dateTime('2018-01-22T08:27:00+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at each sunday', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-07T01:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-08T00:59:59+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-14T01:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-15T00:59:59+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-21T01:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-22T00:59:59+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for day of week to region', (ctx: any) => {
const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }];
const from = dateTime('2018-01-01T18:45:05+01:00');
const to = dateTime('2018-01-22T08:27:00+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at each sunday', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-01-07T01:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-01-08T00:59:59+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-01-14T01:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-01-15T00:59:59+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-01-21T01:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-01-22T00:59:59+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for day of week from/to time region', (ctx: any) => {
const regions = [{ fromDayOfWeek: 7, from: '23:00', toDayOfWeek: 1, to: '01:40', fill: true, colorMode: 'red' }];
const from = dateTime('2018-12-07T12:51:19+01:00');
const to = dateTime('2018-12-10T13:51:29+01:00');
ctx.setup(regions, from, to);
it('should add 1 marking', () => {
expect(ctx.options.grid.markings.length).toBe(1);
});
it('should add one fill between sunday 23:00 and monday 01:40', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-10T00:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-10T02:40:00+01:00').format());
});
});
plotOptionsScenario('for day of week from/to time region', (ctx: any) => {
const regions = [{ fromDayOfWeek: 6, from: '03:00', toDayOfWeek: 7, to: '02:00', fill: true, colorMode: 'red' }];
const from = dateTime('2018-12-07T12:51:19+01:00');
const to = dateTime('2018-12-10T13:51:29+01:00');
ctx.setup(regions, from, to);
it('should add 1 marking', () => {
expect(ctx.options.grid.markings.length).toBe(1);
});
it('should add one fill between saturday 03:00 and sunday 02:00', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-12-08T04:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-12-09T03:00:00+01:00').format());
});
});
plotOptionsScenario('for day of week from/to time region with daylight saving time', (ctx: any) => {
const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }];
const from = dateTime('2018-03-17T06:00:00+01:00');
const to = dateTime('2018-04-03T06:00:00+02:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at each sunday between 20:00 and 23:00', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-03-18T21:00:00+01:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-03-19T00:00:00+01:00').format());
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-03-25T22:00:00+02:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-03-26T01:00:00+02:00').format());
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-04-01T22:00:00+02:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-04-02T01:00:00+02:00').format());
});
});
plotOptionsScenario('for each day of week with winter time', (ctx: any) => {
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
const from = dateTime('2018-10-20T14:50:11+02:00');
const to = dateTime('2018-11-07T12:56:23+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill at each sunday', () => {
const markings = ctx.options.grid.markings;
expect(dateTime(markings[0].xaxis.from).format()).toBe(dateTime('2018-10-21T02:00:00+02:00').format());
expect(dateTime(markings[0].xaxis.to).format()).toBe(dateTime('2018-10-22T01:59:59+02:00').format());
expect(dateTime(markings[1].xaxis.from).format()).toBe(dateTime('2018-10-28T02:00:00+02:00').format());
expect(dateTime(markings[1].xaxis.to).format()).toBe(dateTime('2018-10-29T00:59:59+01:00').format());
expect(dateTime(markings[2].xaxis.from).format()).toBe(dateTime('2018-11-04T01:00:00+01:00').format());
expect(dateTime(markings[2].xaxis.to).format()).toBe(dateTime('2018-11-05T00:59:59+01:00').format());
});
});
});
});

@ -1337,6 +1337,12 @@
"time-zone-label": "Time zone",
"week-start-label": "Week start"
},
"time-regions": {
"advanced-description-cron": "Cron syntax",
"advanced-description-rest": " to define a recurrence schedule and duration",
"advanced-description-use": "Use ",
"advanced-label": "Advanced"
},
"variables": {
"title": "Variables"
},

@ -1337,6 +1337,12 @@
"time-zone-label": "Ŧįmę žőʼnę",
"week-start-label": "Ŵęęĸ şŧäřŧ"
},
"time-regions": {
"advanced-description-cron": "Cřőʼn şyʼnŧäχ",
"advanced-description-rest": " ŧő đęƒįʼnę ä řęčūřřęʼnčę şčĥęđūľę äʼnđ đūřäŧįőʼn",
"advanced-description-use": "Ůşę ",
"advanced-label": "Åđväʼnčęđ"
},
"variables": {
"title": "Väřįäþľęş"
},

@ -13935,6 +13935,13 @@ __metadata:
languageName: node
linkType: hard
"croner@npm:^9.0.0":
version: 9.0.0
resolution: "croner@npm:9.0.0"
checksum: 10/b3cea758eedfe92e35c4ebae46c9db615565348ad898b5938fd94ea77abc5ff68d86539db248a1666b007b0726da642c9046879e8fd598220afcc87c8135e656
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
@ -18265,6 +18272,7 @@ __metadata:
copy-webpack-plugin: "npm:12.0.2"
core-js: "npm:3.40.0"
crashme: "npm:0.0.15"
croner: "npm:^9.0.0"
css-loader: "npm:7.1.2"
css-minimizer-webpack-plugin: "npm:7.0.0"
cypress: "npm:13.10.0"

Loading…
Cancel
Save