mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			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);
 | ||
|       }
 | ||
|     );
 | ||
|   });
 | ||
| });
 |