mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 04:12:19 +08:00
381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
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';
|
||
|
||
// 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: 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-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: 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-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: 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-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,
|
||
to: '10:00',
|
||
};
|
||
|
||
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-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);
|
||
}
|
||
);
|
||
});
|
||
});
|