From 020a71100d50c978a77d8050493e023341f417b7 Mon Sep 17 00:00:00 2001 From: bor Date: Mon, 2 Jun 2025 16:13:04 +0330 Subject: [PATCH 1/8] fix: make backend accept static date format allowed by frontend --- time/parse.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/time/parse.go b/time/parse.go index 154c2c8..5e39f1e 100644 --- a/time/parse.go +++ b/time/parse.go @@ -44,10 +44,14 @@ func Validate(value string) error { // ParseTime parses time. 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 + if rfc3339, err := time.Parse(time.RFC3339, value); err == nil { + return rfc3339, nil + } + if dateTime, err := time.Parse(time.DateTime, value); err == nil { + return dateTime, nil + } + if dateTimeNoSeconds, err := time.Parse(time.DateTime, value+":00"); err == nil { + return dateTimeNoSeconds, nil } - return timemath.Parse(now, value, startOf, weekday) } From e1cb84518b4ade6691c512ec196b00ad77ddf6c7 Mon Sep 17 00:00:00 2001 From: bor Date: Mon, 2 Jun 2025 21:09:39 +0330 Subject: [PATCH 2/8] Revert "fix: make backend accept static date format allowed by frontend" This reverts commit d9e5c23e6628bc2651a7b5bae0a623bbb2ed38c6. --- time/parse.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/time/parse.go b/time/parse.go index 5e39f1e..154c2c8 100644 --- a/time/parse.go +++ b/time/parse.go @@ -44,14 +44,10 @@ func Validate(value string) error { // ParseTime parses time. func ParseTime(now time.Time, value string, startOf bool, weekday time.Weekday) (time.Time, error) { - if rfc3339, err := time.Parse(time.RFC3339, value); err == nil { - return rfc3339, nil - } - if dateTime, err := time.Parse(time.DateTime, value); err == nil { - return dateTime, nil - } - if dateTimeNoSeconds, err := time.Parse(time.DateTime, value+":00"); err == nil { - return dateTimeNoSeconds, nil + parse, err := time.Parse(time.RFC3339, value) + if err == nil { + return parse, nil } + return timemath.Parse(now, value, startOf, weekday) } From a7350acb1d3efec470c61cefde8f5c6a96c9ab5c Mon Sep 17 00:00:00 2001 From: bor Date: Mon, 2 Jun 2025 21:50:37 +0330 Subject: [PATCH 3/8] fix: make ui convert range static dates to rfc3339 --- ui/src/dashboard/Entry/AddPopup.tsx | 5 +++-- ui/src/dashboard/Entry/DashboardEntry.tsx | 5 +++-- ui/src/dashboard/Entry/EditPopup.tsx | 5 +++-- ui/src/utils/range.test.ts | 26 +++++++++++++++++++++++ ui/src/utils/range.ts | 15 +++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 ui/src/utils/range.test.ts diff --git a/ui/src/dashboard/Entry/AddPopup.tsx b/ui/src/dashboard/Entry/AddPopup.tsx index 22aa090..83813c9 100644 --- a/ui/src/dashboard/Entry/AddPopup.tsx +++ b/ui/src/dashboard/Entry/AddPopup.tsx @@ -10,6 +10,7 @@ import * as gqlDashboard from '../../gql/dashboard'; import {Fade} from '../../common/Fade'; import {DashboardEntryForm, isValidDashboardEntry} from './DashboardEntryForm'; import {AddDashboardEntry, AddDashboardEntryVariables} from '../../gql/__generated__/AddDashboardEntry'; +import {normalizeRangeDateFormat} from '../../utils/range'; interface EditPopupProps { dashboardId: number; @@ -83,10 +84,10 @@ export const AddPopup: React.FC = ({ tags: entry.statsSelection.tags, interval: entry.statsSelection.interval, range: entry.statsSelection.range - ? { + ? normalizeRangeDateFormat({ from: entry.statsSelection.range.from, to: entry.statsSelection.range.to, - } + }) : null, rangeId: entry.statsSelection.rangeId, }, diff --git a/ui/src/dashboard/Entry/DashboardEntry.tsx b/ui/src/dashboard/Entry/DashboardEntry.tsx index 4822d27..f0e02e6 100644 --- a/ui/src/dashboard/Entry/DashboardEntry.tsx +++ b/ui/src/dashboard/Entry/DashboardEntry.tsx @@ -14,7 +14,7 @@ import {DashboardBarChart} from './DashboardBarChart'; import {DashboardLineChart} from './DashboardLineChart'; import {CenteredSpinner} from '../../common/CenteredSpinner'; import {Center} from '../../common/Center'; -import {findRange, Range} from '../../utils/range'; +import {findRange, normalizeRangeDateFormat, Range} from '../../utils/range'; import {DashboardTable} from './DashboardTable'; interface DashboardEntryProps { @@ -43,13 +43,14 @@ export const DashboardEntry: React.FC = React.forwardRef<{} // tslint:disable-next-line:cyclomatic-complexity mccabe-complexity const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; range: Range}> = ({entry, range}) => { const interval = entry.statsSelection.interval; + const normalizedRange = normalizeRangeDateFormat(range); const stats = useQuery(gqlStats.Stats2, { variables: { now: moment() .startOf('hour') .format(), stats: { - range, + range: normalizedRange, interval, tags: entry.statsSelection.tags, }, diff --git a/ui/src/dashboard/Entry/EditPopup.tsx b/ui/src/dashboard/Entry/EditPopup.tsx index 5a5f66a..d3ade6a 100644 --- a/ui/src/dashboard/Entry/EditPopup.tsx +++ b/ui/src/dashboard/Entry/EditPopup.tsx @@ -10,6 +10,7 @@ import * as gqlDashboard from '../../gql/dashboard'; import {UpdateDashboardEntry, UpdateDashboardEntryVariables} from '../../gql/__generated__/UpdateDashboardEntry'; import {Fade} from '../../common/Fade'; import {DashboardEntryForm, isValidDashboardEntry} from './DashboardEntryForm'; +import {normalizeRangeDateFormat} from '../../utils/range'; interface EditPopupProps { entry: Dashboards_dashboards_items; @@ -69,10 +70,10 @@ export const EditPopup: React.FC = ({entry, anchorEl, onChange: tags: entry.statsSelection.tags, interval: entry.statsSelection.interval, range: entry.statsSelection.range - ? { + ? normalizeRangeDateFormat({ from: entry.statsSelection.range.from, to: entry.statsSelection.range.to, - } + }) : null, rangeId: entry.statsSelection.rangeId, }, diff --git a/ui/src/utils/range.test.ts b/ui/src/utils/range.test.ts new file mode 100644 index 0000000..e268e4d --- /dev/null +++ b/ui/src/utils/range.test.ts @@ -0,0 +1,26 @@ +import {normalizeDate} from './range'; +import moment from 'moment'; + +moment.updateLocale('en', { + week: { + dow: 1, // monday + doy: moment.localeData('en').firstDayOfYear(), + }, +}); +moment.tz.setDefault('UTC'); + +it('should convert to RFC3339', () => { + expect(normalizeDate('2025-01-01 10:10')).toBe('2025-01-01T10:10:00Z'); +}); + +it('should not modify relative ranges', () => { + expect(normalizeDate('now-1d')).toBe('now-1d'); + expect(normalizeDate('now-120s')).toBe('now-120s'); + expect(normalizeDate('now-1d-1h')).toBe('now-1d-1h'); + expect(normalizeDate('now/w')).toBe('now/w'); + expect(normalizeDate('now/w')).toBe('now/w'); + expect(normalizeDate('now-1w/w')).toBe('now-1w/w'); + expect(normalizeDate('now-1y+1w/w')).toBe('now-1y+1w/w'); + expect(normalizeDate('now/d+5h')).toBe('now/d+5h'); + expect(normalizeDate('now/y')).toBe('now/y'); +}); diff --git a/ui/src/utils/range.ts b/ui/src/utils/range.ts index 83d3290..6d7bb8d 100644 --- a/ui/src/utils/range.ts +++ b/ui/src/utils/range.ts @@ -1,3 +1,5 @@ +import moment from 'moment-timezone'; + export interface Range { from: string; to: string; @@ -14,3 +16,16 @@ export const findRange = (selection: {range: Range | null; rangeId: number | nul }; export const exclusiveRange = (range: Range) => ({from: range.from, to: range.to}); + +export function normalizeDate(date: string): string { + const d = moment(date); + if (d.isValid()) { + return d.utc().format(); + } else { + return date; + } +} + +export function normalizeRangeDateFormat(range: Range): Range { + return {from: normalizeDate(range.from), to: normalizeDate(range.to)}; +} From 8aa733e2c1b10847450eb70d827d0eef6c4f8e87 Mon Sep 17 00:00:00 2001 From: bor Date: Mon, 2 Jun 2025 23:38:44 +0330 Subject: [PATCH 4/8] fix: use OmitTimeZone for dates passed to backend for dashboard --- time/parse.go | 2 +- ui/src/utils/range.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/utils/range.ts b/ui/src/utils/range.ts index 6d7bb8d..ef7eb4d 100644 --- a/ui/src/utils/range.ts +++ b/ui/src/utils/range.ts @@ -20,7 +20,7 @@ export const exclusiveRange = (range: Range) => ({from: range.from, to: range.to export function normalizeDate(date: string): string { const d = moment(date); if (d.isValid()) { - return d.utc().format(); + return d.format(); } else { return date; } From 53dfc56a9c4370691257366ffe8effd07ee85db3 Mon Sep 17 00:00:00 2001 From: bor Date: Tue, 3 Jun 2025 00:22:22 +0330 Subject: [PATCH 5/8] refactor: normalizing dates in ui now happens in RelativeDateTimeSelector component --- ui/src/common/RelativeDateTimeSelector.tsx | 9 +++--- ui/src/dashboard/Entry/AddPopup.tsx | 5 ++-- ui/src/dashboard/Entry/DashboardEntry.tsx | 5 ++-- ui/src/dashboard/Entry/EditPopup.tsx | 5 ++-- ui/src/utils/range.test.ts | 26 ------------------ ui/src/utils/range.ts | 15 ---------- ui/src/utils/time.test.ts | 32 +++++++++++++++++++--- ui/src/utils/time.ts | 24 ++++++++++++++-- 8 files changed, 60 insertions(+), 61 deletions(-) delete mode 100644 ui/src/utils/range.test.ts diff --git a/ui/src/common/RelativeDateTimeSelector.tsx b/ui/src/common/RelativeDateTimeSelector.tsx index 0ca541c..d5881b0 100644 --- a/ui/src/common/RelativeDateTimeSelector.tsx +++ b/ui/src/common/RelativeDateTimeSelector.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import {TextField} from '@material-ui/core'; -import {parseRelativeTime} from '../utils/time'; +import {normalizeDate, parseRelativeTime, userFriendlyDate} from '../utils/time'; import Typography from '@material-ui/core/Typography'; import useTimeout from '@rooks/use-timeout'; @@ -16,7 +16,7 @@ interface RelativeDateTimeSelectorProps { } export const RelativeDateTimeSelector: React.FC = ({ - value, + value: apiValue, onChange: setValue, type, style, @@ -28,8 +28,9 @@ 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 value = userFriendlyDate(apiValue); - const parsed = parseRelativeTime(value, type); return ( = disabled={disabled} InputProps={{disableUnderline}} onChange={(e) => { - const newValue = e.target.value; + const newValue = normalizeDate(e.target.value); const result = parseRelativeTime(newValue, type); setErrVisible(false); stop(); diff --git a/ui/src/dashboard/Entry/AddPopup.tsx b/ui/src/dashboard/Entry/AddPopup.tsx index 83813c9..22aa090 100644 --- a/ui/src/dashboard/Entry/AddPopup.tsx +++ b/ui/src/dashboard/Entry/AddPopup.tsx @@ -10,7 +10,6 @@ import * as gqlDashboard from '../../gql/dashboard'; import {Fade} from '../../common/Fade'; import {DashboardEntryForm, isValidDashboardEntry} from './DashboardEntryForm'; import {AddDashboardEntry, AddDashboardEntryVariables} from '../../gql/__generated__/AddDashboardEntry'; -import {normalizeRangeDateFormat} from '../../utils/range'; interface EditPopupProps { dashboardId: number; @@ -84,10 +83,10 @@ export const AddPopup: React.FC = ({ tags: entry.statsSelection.tags, interval: entry.statsSelection.interval, range: entry.statsSelection.range - ? normalizeRangeDateFormat({ + ? { from: entry.statsSelection.range.from, to: entry.statsSelection.range.to, - }) + } : null, rangeId: entry.statsSelection.rangeId, }, diff --git a/ui/src/dashboard/Entry/DashboardEntry.tsx b/ui/src/dashboard/Entry/DashboardEntry.tsx index f0e02e6..4822d27 100644 --- a/ui/src/dashboard/Entry/DashboardEntry.tsx +++ b/ui/src/dashboard/Entry/DashboardEntry.tsx @@ -14,7 +14,7 @@ import {DashboardBarChart} from './DashboardBarChart'; import {DashboardLineChart} from './DashboardLineChart'; import {CenteredSpinner} from '../../common/CenteredSpinner'; import {Center} from '../../common/Center'; -import {findRange, normalizeRangeDateFormat, Range} from '../../utils/range'; +import {findRange, Range} from '../../utils/range'; import {DashboardTable} from './DashboardTable'; interface DashboardEntryProps { @@ -43,14 +43,13 @@ export const DashboardEntry: React.FC = React.forwardRef<{} // tslint:disable-next-line:cyclomatic-complexity mccabe-complexity const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; range: Range}> = ({entry, range}) => { const interval = entry.statsSelection.interval; - const normalizedRange = normalizeRangeDateFormat(range); const stats = useQuery(gqlStats.Stats2, { variables: { now: moment() .startOf('hour') .format(), stats: { - range: normalizedRange, + range, interval, tags: entry.statsSelection.tags, }, diff --git a/ui/src/dashboard/Entry/EditPopup.tsx b/ui/src/dashboard/Entry/EditPopup.tsx index d3ade6a..5a5f66a 100644 --- a/ui/src/dashboard/Entry/EditPopup.tsx +++ b/ui/src/dashboard/Entry/EditPopup.tsx @@ -10,7 +10,6 @@ import * as gqlDashboard from '../../gql/dashboard'; import {UpdateDashboardEntry, UpdateDashboardEntryVariables} from '../../gql/__generated__/UpdateDashboardEntry'; import {Fade} from '../../common/Fade'; import {DashboardEntryForm, isValidDashboardEntry} from './DashboardEntryForm'; -import {normalizeRangeDateFormat} from '../../utils/range'; interface EditPopupProps { entry: Dashboards_dashboards_items; @@ -70,10 +69,10 @@ export const EditPopup: React.FC = ({entry, anchorEl, onChange: tags: entry.statsSelection.tags, interval: entry.statsSelection.interval, range: entry.statsSelection.range - ? normalizeRangeDateFormat({ + ? { from: entry.statsSelection.range.from, to: entry.statsSelection.range.to, - }) + } : null, rangeId: entry.statsSelection.rangeId, }, diff --git a/ui/src/utils/range.test.ts b/ui/src/utils/range.test.ts deleted file mode 100644 index e268e4d..0000000 --- a/ui/src/utils/range.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {normalizeDate} from './range'; -import moment from 'moment'; - -moment.updateLocale('en', { - week: { - dow: 1, // monday - doy: moment.localeData('en').firstDayOfYear(), - }, -}); -moment.tz.setDefault('UTC'); - -it('should convert to RFC3339', () => { - expect(normalizeDate('2025-01-01 10:10')).toBe('2025-01-01T10:10:00Z'); -}); - -it('should not modify relative ranges', () => { - expect(normalizeDate('now-1d')).toBe('now-1d'); - expect(normalizeDate('now-120s')).toBe('now-120s'); - expect(normalizeDate('now-1d-1h')).toBe('now-1d-1h'); - expect(normalizeDate('now/w')).toBe('now/w'); - expect(normalizeDate('now/w')).toBe('now/w'); - expect(normalizeDate('now-1w/w')).toBe('now-1w/w'); - expect(normalizeDate('now-1y+1w/w')).toBe('now-1y+1w/w'); - expect(normalizeDate('now/d+5h')).toBe('now/d+5h'); - expect(normalizeDate('now/y')).toBe('now/y'); -}); diff --git a/ui/src/utils/range.ts b/ui/src/utils/range.ts index ef7eb4d..83d3290 100644 --- a/ui/src/utils/range.ts +++ b/ui/src/utils/range.ts @@ -1,5 +1,3 @@ -import moment from 'moment-timezone'; - export interface Range { from: string; to: string; @@ -16,16 +14,3 @@ export const findRange = (selection: {range: Range | null; rangeId: number | nul }; export const exclusiveRange = (range: Range) => ({from: range.from, to: range.to}); - -export function normalizeDate(date: string): string { - const d = moment(date); - if (d.isValid()) { - return d.format(); - } else { - return date; - } -} - -export function normalizeRangeDateFormat(range: Range): Range { - return {from: normalizeDate(range.from), to: normalizeDate(range.to)}; -} diff --git a/ui/src/utils/time.test.ts b/ui/src/utils/time.test.ts index e215263..50c99e4 100644 --- a/ui/src/utils/time.test.ts +++ b/ui/src/utils/time.test.ts @@ -1,4 +1,4 @@ -import {isValidDate, parseRelativeTime} from './time'; +import {isValidDate, normalizeDate, parseRelativeTime, userFriendlyDate} from './time'; import moment from 'moment'; moment.updateLocale('en', { @@ -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 @@ -33,6 +36,27 @@ it('should parse', () => { expectSuccess(parseRelativeTime('now/y', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-01-01 00:00:00'); }); +it('should convert to RFC3339 and back', () => { + // can't put exact dates since moment doesn't allow overriding `.local()`'s timezone for unit tests + const userDate = '2025-01-01 10:10'; + const rfcDate = moment(userDate).utc().format(); + + expect(normalizeDate(userDate)).toBe(rfcDate); + expect(userFriendlyDate(rfcDate)).toBe(userDate); +}); + +it('should not modify relative ranges', () => { + expect(normalizeDate('now-1d')).toBe('now-1d'); + expect(normalizeDate('now-120s')).toBe('now-120s'); + expect(normalizeDate('now-1d-1h')).toBe('now-1d-1h'); + expect(normalizeDate('now/w')).toBe('now/w'); + expect(normalizeDate('now/w')).toBe('now/w'); + expect(normalizeDate('now-1w/w')).toBe('now-1w/w'); + expect(normalizeDate('now-1y+1w/w')).toBe('now-1y+1w/w'); + expect(normalizeDate('now/d+5h')).toBe('now/d+5h'); + expect(normalizeDate('now/y')).toBe('now/y'); +}); + const expectSuccess = (value: ReturnType) => { if (value.success) { return expect(value.value.format('YYYY-MM-DD HH:mm:ss')); diff --git a/ui/src/utils/time.ts b/ui/src/utils/time.ts index e1a78c9..3538e35 100644 --- a/ui/src/utils/time.ts +++ b/ui/src/utils/time.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment from 'moment-timezone'; interface Success { success: true; @@ -112,7 +112,7 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no 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 => { @@ -133,10 +133,28 @@ 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 => { const fromString = from.format('YYYYMMDD'); return to === undefined || fromString === to.format('YYYYMMDD'); }; + +export function normalizeDate(date: string): string { + if (isValidDate(date, 'YYYY-MM-DD HH:mm')) { + return moment(date) + .utc() + .format(); + } else { + return date; + } +} + +export function userFriendlyDate(date: string): string { + if (isValidDate(date)) { + return moment(date).local().format('YYYY-MM-DD HH:mm'); + } else { + return date; + } +} From f30a71f11f4a5e938f9ec9f83b32e654e9773460 Mon Sep 17 00:00:00 2001 From: bor Date: Tue, 3 Jun 2025 00:37:56 +0330 Subject: [PATCH 6/8] refactor: auto indent --- ui/src/utils/time.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/utils/time.ts b/ui/src/utils/time.ts index 3538e35..62dfd9d 100644 --- a/ui/src/utils/time.ts +++ b/ui/src/utils/time.ts @@ -153,7 +153,9 @@ export function normalizeDate(date: string): string { export function userFriendlyDate(date: string): string { if (isValidDate(date)) { - return moment(date).local().format('YYYY-MM-DD HH:mm'); + return moment(date) + .local() + .format('YYYY-MM-DD HH:mm'); } else { return date; } From e54bd84e4be9e9bb3c8ac36e26e2e6c18981731d Mon Sep 17 00:00:00 2001 From: bor Date: Wed, 25 Jun 2025 16:36:16 +0330 Subject: [PATCH 7/8] feat: support date without time (+ refactoring) --- ui/src/common/RelativeDateTimeSelector.tsx | 16 +- ui/src/utils/time.test.ts | 197 ++++++++++++++++----- ui/src/utils/time.ts | 59 +++--- 3 files changed, 193 insertions(+), 79 deletions(-) diff --git a/ui/src/common/RelativeDateTimeSelector.tsx b/ui/src/common/RelativeDateTimeSelector.tsx index d5881b0..0a66423 100644 --- a/ui/src/common/RelativeDateTimeSelector.tsx +++ b/ui/src/common/RelativeDateTimeSelector.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import {TextField} from '@material-ui/core'; -import {normalizeDate, parseRelativeTime, userFriendlyDate} from '../utils/time'; +import {parseRelativeTime} from '../utils/time'; import Typography from '@material-ui/core/Typography'; import useTimeout from '@rooks/use-timeout'; @@ -29,18 +29,16 @@ export const RelativeDateTimeSelector: React.FC = const [error, setError] = React.useState(''); const {start, stop} = useTimeout(() => setErrVisible(true), 200); const parsed = parseRelativeTime(apiValue, type); - const value = userFriendlyDate(apiValue); return ( { - const newValue = normalizeDate(e.target.value); - const result = parseRelativeTime(newValue, type); + const result = parseRelativeTime(e.target.value, type); setErrVisible(false); stop(); if (!result.success) { @@ -49,18 +47,16 @@ export const RelativeDateTimeSelector: React.FC = } else { setError(''); } - setValue(newValue, result.success); + setValue(result.success ? result.normalized : e.target.value, result.success); }} error={error !== ''} helperText={ - small ? ( - undefined - ) : errVisible ? ( + small ? undefined : errVisible ? ( {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 50c99e4..bb5ae66 100644 --- a/ui/src/utils/time.test.ts +++ b/ui/src/utils/time.test.ts @@ -1,4 +1,4 @@ -import {isValidDate, normalizeDate, parseRelativeTime, userFriendlyDate} from './time'; +import {isValidDate, parseRelativeTime} from './time'; import moment from 'moment'; moment.updateLocale('en', { @@ -24,43 +24,160 @@ 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'); -}); - -it('should convert to RFC3339 and back', () => { - // can't put exact dates since moment doesn't allow overriding `.local()`'s timezone for unit tests - const userDate = '2025-01-01 10:10'; - const rfcDate = moment(userDate).utc().format(); - - expect(normalizeDate(userDate)).toBe(rfcDate); - expect(userFriendlyDate(rfcDate)).toBe(userDate); -}); - -it('should not modify relative ranges', () => { - expect(normalizeDate('now-1d')).toBe('now-1d'); - expect(normalizeDate('now-120s')).toBe('now-120s'); - expect(normalizeDate('now-1d-1h')).toBe('now-1d-1h'); - expect(normalizeDate('now/w')).toBe('now/w'); - expect(normalizeDate('now/w')).toBe('now/w'); - expect(normalizeDate('now-1w/w')).toBe('now-1w/w'); - expect(normalizeDate('now-1y+1w/w')).toBe('now-1y+1w/w'); - expect(normalizeDate('now/d+5h')).toBe('now/d+5h'); - expect(normalizeDate('now/y')).toBe('now/y'); -}); - -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 62dfd9d..d8b759a 100644 --- a/ui/src/utils/time.ts +++ b/ui/src/utils/time.ts @@ -2,7 +2,9 @@ import moment from 'moment-timezone'; interface Success { success: true; - value: moment.Moment; + preview: moment.Moment; + localized: string; + normalized: string; } interface Failure { @@ -32,9 +34,28 @@ 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') { @@ -71,7 +92,7 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no case Type.Unit: if (!isUnit(currentChar)) { return failure( - 'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar + 'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar, ); } @@ -105,18 +126,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('\'now\' must be at the start'); } - return failure("Expected valid date (e.g. 2020-01-01 16:30) 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}; @@ -140,23 +161,3 @@ export const isSameDate = (from: moment.Moment, to?: moment.Moment): boolean => const fromString = from.format('YYYYMMDD'); return to === undefined || fromString === to.format('YYYYMMDD'); }; - -export function normalizeDate(date: string): string { - if (isValidDate(date, 'YYYY-MM-DD HH:mm')) { - return moment(date) - .utc() - .format(); - } else { - return date; - } -} - -export function userFriendlyDate(date: string): string { - if (isValidDate(date)) { - return moment(date) - .local() - .format('YYYY-MM-DD HH:mm'); - } else { - return date; - } -} From daac58ec73a880efc30eb1bdebb6be571033bc54 Mon Sep 17 00:00:00 2001 From: bor Date: Wed, 25 Jun 2025 17:21:03 +0330 Subject: [PATCH 8/8] chore: ran prettier --- ui/src/common/RelativeDateTimeSelector.tsx | 4 ++- ui/src/utils/time.test.ts | 34 ++++++++++++++++------ ui/src/utils/time.ts | 15 +++++++--- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/ui/src/common/RelativeDateTimeSelector.tsx b/ui/src/common/RelativeDateTimeSelector.tsx index 0a66423..f953f4c 100644 --- a/ui/src/common/RelativeDateTimeSelector.tsx +++ b/ui/src/common/RelativeDateTimeSelector.tsx @@ -51,7 +51,9 @@ export const RelativeDateTimeSelector: React.FC = }} error={error !== ''} helperText={ - small ? undefined : errVisible ? ( + small ? ( + undefined + ) : errVisible ? ( {error} diff --git a/ui/src/utils/time.test.ts b/ui/src/utils/time.test.ts index bb5ae66..fddcb0b 100644 --- a/ui/src/utils/time.test.ts +++ b/ui/src/utils/time.test.ts @@ -111,7 +111,9 @@ it.each([ divide: 'startOf', expected: '2025-01-01 10:10:00', localized: '2025-01-01 10:10', - normalized: moment('2025-01-01 10:10').utc().format(), + normalized: moment('2025-01-01 10:10') + .utc() + .format(), shouldParse: true, }, { @@ -119,7 +121,9 @@ it.each([ divide: 'endOf', expected: '2025-01-01 10:10:00', localized: '2025-01-01 10:10', - normalized: moment('2025-01-01 10:10').utc().format(), + normalized: moment('2025-01-01 10:10') + .utc() + .format(), shouldParse: true, }, { @@ -127,7 +131,9 @@ it.each([ divide: 'startOf', expected: '2025-01-02 00:00:00', localized: '2025-01-02', - normalized: moment('2025-01-02 00:00').utc().format(), + normalized: moment('2025-01-02 00:00') + .utc() + .format(), shouldParse: true, }, { @@ -135,7 +141,9 @@ it.each([ divide: 'endOf', expected: '2025-01-02 23:59:59', localized: '2025-01-02', - normalized: moment('2025-01-02 23:59:59').utc().format(), + normalized: moment('2025-01-02 23:59:59') + .utc() + .format(), shouldParse: true, }, { @@ -143,7 +151,9 @@ it.each([ 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(), + normalized: moment('2025-01-02 10:00:00') + .utc() + .format(), shouldParse: true, }, { @@ -151,15 +161,21 @@ it.each([ divide: 'startOf', expected: '2025-01-02 00:00:00', localized: '2025-01-02', - normalized: moment('2025-01-02 00:00:00').utc().format(), + normalized: moment('2025-01-02 00:00:00') + .utc() + .format(), shouldParse: true, }, { - value: moment('2025-01-02 23:59:59').utc().format(), + 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(), + normalized: moment('2025-01-02 23:59:59') + .utc() + .format(), shouldParse: true, }, { @@ -177,7 +193,7 @@ it.each([ expect(result.preview.format('YYYY-MM-DD HH:mm:ss')).toEqual(expected); expect(result.normalized).toEqual(normalized); expect(result.localized).toEqual(localized); - }else{ + } else { expect(result.error).toEqual(expected); } }); diff --git a/ui/src/utils/time.ts b/ui/src/utils/time.ts index d8b759a..65b1950 100644 --- a/ui/src/utils/time.ts +++ b/ui/src/utils/time.ts @@ -54,7 +54,14 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no value = asDate(value).format('YYYY-MM-DD HH:mm'); } } - return success(parsed, value, parsed.clone().utc().format()); + return success( + parsed, + value, + parsed + .clone() + .utc() + .format() + ); } } @@ -92,7 +99,7 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no case Type.Unit: if (!isUnit(currentChar)) { return failure( - 'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar, + 'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar ); } @@ -130,10 +137,10 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no } if (value.indexOf('now') !== -1) { - return failure('\'now\' must be at the start'); + return failure("'now' must be at the start"); } - return failure('Expected valid date (e.g. 2020-01-01 16:30) 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, localized: string, normalized: string): Success => {