diff --git a/time/parse.go b/time/parse.go index 154c2c8..850ceeb 100644 --- a/time/parse.go +++ b/time/parse.go @@ -46,7 +46,7 @@ func Validate(value string) error { func ParseTime(now time.Time, value string, startOf bool, weekday time.Weekday) (time.Time, error) { parse, err := time.Parse(time.RFC3339, value) if err == nil { - return parse, nil + return model.Time(parse).OmitTimeZone(), nil } return timemath.Parse(now, value, startOf, weekday) diff --git a/ui/src/common/RelativeDateTimeSelector.tsx b/ui/src/common/RelativeDateTimeSelector.tsx index 0ca541c..f953f4c 100644 --- a/ui/src/common/RelativeDateTimeSelector.tsx +++ b/ui/src/common/RelativeDateTimeSelector.tsx @@ -16,7 +16,7 @@ interface RelativeDateTimeSelectorProps { } export const RelativeDateTimeSelector: React.FC = ({ - value, + value: apiValue, onChange: setValue, type, style, @@ -28,18 +28,17 @@ export const RelativeDateTimeSelector: React.FC = const [errVisible, setErrVisible] = React.useState(false); const [error, setError] = React.useState(''); const {start, stop} = useTimeout(() => setErrVisible(true), 200); + const parsed = parseRelativeTime(apiValue, type); - const parsed = parseRelativeTime(value, type); return ( { - const newValue = e.target.value; - const result = parseRelativeTime(newValue, type); + const result = parseRelativeTime(e.target.value, type); setErrVisible(false); stop(); if (!result.success) { @@ -48,7 +47,7 @@ export const RelativeDateTimeSelector: React.FC = } else { setError(''); } - setValue(newValue, result.success); + setValue(result.success ? result.normalized : e.target.value, result.success); }} error={error !== ''} helperText={ @@ -59,7 +58,7 @@ export const RelativeDateTimeSelector: React.FC = {error} ) : ( - {!parsed.success ? '...' : parsed.value.format('llll')} + {!parsed.success ? '...' : parsed.preview.format('llll')} ) } label={label} diff --git a/ui/src/utils/time.test.ts b/ui/src/utils/time.test.ts index e215263..fddcb0b 100644 --- a/ui/src/utils/time.test.ts +++ b/ui/src/utils/time.test.ts @@ -9,9 +9,12 @@ moment.updateLocale('en', { }); it('should test for valid date', () => { - expect(isValidDate('2017-05-05')).toBe(false); - expect(isValidDate('2017-05-05T15:23')).toBe(false); - expect(isValidDate('2017-05-05 15:23')).toBe(true); + expect(isValidDate('2017-05-05', 'YYYY-MM-DD HH:mm')).toBe(false); + expect(isValidDate('2017-05-05T15:23', 'YYYY-MM-DD HH:mm')).toBe(false); + expect(isValidDate('2017-05-05 15:23', 'YYYY-MM-DD HH:mm')).toBe(true); + + expect(isValidDate('2019-10-20T15:55:00Z')).toBe(true); + expect(isValidDate('2017-05-05 15:23')).toBe(false); }); // 2018-10-15 Monday @@ -21,22 +24,176 @@ it('should test for valid date', () => { // 2019-10-14 Monday // 2019-10-21 Monday -it('should parse', () => { - expectSuccess(parseRelativeTime('now-1d', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-19 15:55:00'); - expectSuccess(parseRelativeTime('now-120s', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-20 15:53:15'); - expectSuccess(parseRelativeTime('now-1d-1h', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-19 14:55:00'); - expectSuccess(parseRelativeTime('now/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-14 00:00:00'); - expectSuccess(parseRelativeTime('now/w', 'endOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-20 23:59:59'); - expectSuccess(parseRelativeTime('now-1w/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-07 00:00:00'); - expectSuccess(parseRelativeTime('now-1y+1w/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2018-10-22 00:00:00'); - expectSuccess(parseRelativeTime('now/d+5h', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-20 05:00:00'); - expectSuccess(parseRelativeTime('now/y', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-01-01 00:00:00'); -}); - -const expectSuccess = (value: ReturnType) => { - if (value.success) { - return expect(value.value.format('YYYY-MM-DD HH:mm:ss')); +it.each([ + { + value: 'now-1d', + divide: 'startOf', + now: moment('2019-10-20T15:55:00'), + expected: '2019-10-19 15:55:00', + localized: 'now-1d', + normalized: 'now-1d', + shouldParse: true, + }, + { + value: 'now-120s', + divide: 'startOf', + now: moment('2019-10-20T15:55:15'), + expected: '2019-10-20 15:53:15', + localized: 'now-120s', + normalized: 'now-120s', + shouldParse: true, + }, + { + value: 'now-1d-1h', + divide: 'startOf', + now: moment('2019-10-20T15:55:00'), + expected: '2019-10-19 14:55:00', + localized: 'now-1d-1h', + normalized: 'now-1d-1h', + shouldParse: true, + }, + { + value: 'now/w', + divide: 'startOf', + now: moment('2019-10-20T15:55:15'), + expected: '2019-10-14 00:00:00', + localized: 'now/w', + normalized: 'now/w', + shouldParse: true, + }, + { + value: 'now/w', + divide: 'endOf', + now: moment('2019-10-20T15:55:15'), + expected: '2019-10-20 23:59:59', + localized: 'now/w', + normalized: 'now/w', + shouldParse: true, + }, + { + value: 'now-1w/w', + divide: 'startOf', + now: moment('2019-10-20T15:55:15'), + expected: '2019-10-07 00:00:00', + localized: 'now-1w/w', + normalized: 'now-1w/w', + shouldParse: true, + }, + { + value: 'now-1y+1w/w', + divide: 'startOf', + now: moment('2019-10-20T15:55:15'), + expected: '2018-10-22 00:00:00', + localized: 'now-1y+1w/w', + normalized: 'now-1y+1w/w', + shouldParse: true, + }, + { + value: 'now/d+5h', + divide: 'startOf', + now: moment('2019-10-20T15:55:00'), + expected: '2019-10-20 05:00:00', + localized: 'now/d+5h', + normalized: 'now/d+5h', + shouldParse: true, + }, + { + value: 'now/y', + divide: 'startOf', + now: moment('2019-10-20T15:55:15'), + expected: '2019-01-01 00:00:00', + localized: 'now/y', + normalized: 'now/y', + shouldParse: true, + }, + { + value: '2025-01-01 10:10', + divide: 'startOf', + expected: '2025-01-01 10:10:00', + localized: '2025-01-01 10:10', + normalized: moment('2025-01-01 10:10') + .utc() + .format(), + shouldParse: true, + }, + { + value: '2025-01-01 10:10', + divide: 'endOf', + expected: '2025-01-01 10:10:00', + localized: '2025-01-01 10:10', + normalized: moment('2025-01-01 10:10') + .utc() + .format(), + shouldParse: true, + }, + { + value: '2025-01-02', + divide: 'startOf', + expected: '2025-01-02 00:00:00', + localized: '2025-01-02', + normalized: moment('2025-01-02 00:00') + .utc() + .format(), + shouldParse: true, + }, + { + value: '2025-01-02', + divide: 'endOf', + expected: '2025-01-02 23:59:59', + localized: '2025-01-02', + normalized: moment('2025-01-02 23:59:59') + .utc() + .format(), + shouldParse: true, + }, + { + value: moment('2025-01-02 10:00:00').format(), + divide: 'startOf', + expected: '2025-01-02 10:00:00', + localized: '2025-01-02 10:00', + normalized: moment('2025-01-02 10:00:00') + .utc() + .format(), + shouldParse: true, + }, + { + value: moment('2025-01-02 00:00:00').format(), + divide: 'startOf', + expected: '2025-01-02 00:00:00', + localized: '2025-01-02', + normalized: moment('2025-01-02 00:00:00') + .utc() + .format(), + shouldParse: true, + }, + { + value: moment('2025-01-02 23:59:59') + .utc() + .format(), + divide: 'endOf', + expected: '2025-01-02 23:59:59', + localized: '2025-01-02', + normalized: moment('2025-01-02 23:59:59') + .utc() + .format(), + shouldParse: true, + }, + { + value: 'invalid', + divide: 'endOf', + expected: "Expected valid date (e.g. 2020-01-01 16:30) or 'now' at index 0", + localized: 'invalid', + normalized: undefined, + shouldParse: false, + }, +])('should parse', ({value, divide, now, expected, normalized, localized, shouldParse}) => { + const result = parseRelativeTime(value, divide as 'startOf' | 'endOf', now); + expect(result.success).toBe(shouldParse); + if (result.success) { + expect(result.preview.format('YYYY-MM-DD HH:mm:ss')).toEqual(expected); + expect(result.normalized).toEqual(normalized); + expect(result.localized).toEqual(localized); + } else { + expect(result.error).toEqual(expected); } - expect(value.error).toEqual('no error'); - return expect(''); -}; +}); diff --git a/ui/src/utils/time.ts b/ui/src/utils/time.ts index e1a78c9..65b1950 100644 --- a/ui/src/utils/time.ts +++ b/ui/src/utils/time.ts @@ -1,8 +1,10 @@ -import moment from 'moment'; +import moment from 'moment-timezone'; interface Success { success: true; - value: moment.Moment; + preview: moment.Moment; + localized: string; + normalized: string; } interface Failure { @@ -32,9 +34,35 @@ enum Unit { Second = 's', } +// tslint:disable-next-line:cyclomatic-complexity mccabe-complexity export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', nowDate = moment()): Success | Failure => { - if (isValidDate(value)) { - return success(asDate(value)); + for (const format of ['YYYY-MM-DD HH:mm', 'YYYY-MM-DD', 'YYYY-MM-DD[T]HH:mm:ssZ']) { + if (isValidDate(value, format)) { + const parsed = asDate(value, format); + if (divide === 'endOf' && format === 'YYYY-MM-DD') { + parsed.endOf('day'); + } + + if (format === 'YYYY-MM-DD[T]HH:mm:ssZ') { + const localDate = parsed.clone().local(); + if ( + (divide === 'startOf' && parsed.isSame(localDate.startOf('day'), 'second')) || + (divide === 'endOf' && parsed.isSame(localDate.endOf('day'), 'second')) + ) { + value = asDate(value).format('YYYY-MM-DD'); + } else { + value = asDate(value).format('YYYY-MM-DD HH:mm'); + } + } + return success( + parsed, + value, + parsed + .clone() + .utc() + .format() + ); + } } if (value.substr(0, 3) === 'now') { @@ -105,18 +133,18 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no if (expectNext === Type.Value) { return failure('Expected number at the end but got nothing'); } - return success(time); + return success(time, value, value); } if (value.indexOf('now') !== -1) { return failure("'now' must be at the start"); } - return failure("Expected valid date or 'now' at index 0"); + return failure("Expected valid date (e.g. 2020-01-01 16:30) or 'now' at index 0"); }; -export const success = (value: moment.Moment): Success => { - return {success: true, value}; +export const success = (value: moment.Moment, localized: string, normalized: string): Success => { + return {success: true, preview: value, normalized, localized}; }; export const failure = (error: string): Failure => { return {success: false, error}; @@ -133,7 +161,7 @@ export const isValidDate = (value: string, format?: string) => { return asDate(value, format).isValid(); }; -export const asDate = (value: string, format = 'YYYY-MM-DD HH:mm') => { +export const asDate = (value: string, format = 'YYYY-MM-DD[T]HH:mm:ssZ') => { return moment(value, format, true); }; export const isSameDate = (from: moment.Moment, to?: moment.Moment): boolean => {