diff --git a/webui/react/src/components/CheckpointModal.tsx b/webui/react/src/components/CheckpointModal.tsx index 0563cf8c637..0893ed74bd3 100644 --- a/webui/react/src/components/CheckpointModal.tsx +++ b/webui/react/src/components/CheckpointModal.tsx @@ -1,3 +1,4 @@ +import dayjs from 'dayjs'; import Breadcrumb from 'hew/Breadcrumb'; import Button from 'hew/Button'; import Glossary, { InfoRow } from 'hew/Glossary'; @@ -15,7 +16,7 @@ import { CoreApiGenericCheckpoint, ExperimentConfig, } from 'types'; -import { formatDatetime } from 'utils/datetime'; +import { DEFAULT_DATETIME_FORMAT } from 'utils/datetime'; import handleError, { DetError, ErrorType } from 'utils/error'; import { createPachydermLineageLink } from 'utils/integrations'; import { isAbsolutePath } from 'utils/routes'; @@ -178,7 +179,10 @@ ${checkpoint?.totalBatches}? This action may complete or fail without further no ), }); if ('endTime' in checkpoint && checkpoint?.endTime) - glossaryContent.push({ label: 'Ended', value: formatDatetime(checkpoint.endTime) }); + glossaryContent.push({ + label: 'Ended', + value: dayjs.utc(checkpoint.endTime).format(DEFAULT_DATETIME_FORMAT), + }); glossaryContent.push({ label: 'Total Size', value: ( diff --git a/webui/react/src/components/TrialLogPreview.tsx b/webui/react/src/components/TrialLogPreview.tsx index 7dd5b847307..9eaf939abd4 100644 --- a/webui/react/src/components/TrialLogPreview.tsx +++ b/webui/react/src/components/TrialLogPreview.tsx @@ -12,7 +12,6 @@ import { detApi } from 'services/apiConfig'; import { mapV1LogsResponse } from 'services/decoder'; import { readStream } from 'services/utils'; import { LogLevel, RunState, TrialDetails } from 'types'; -import { formatDatetime } from 'utils/datetime'; import css from './TrialLogPreview.module.scss'; @@ -60,7 +59,7 @@ const TrialLogPreview: React.FC = ({ (event) => { const entry = mapV1LogsResponse(event); setLogEntry({ - formattedTime: formatDatetime(entry.time, { format: DATETIME_FORMAT }), + formattedTime: dayjs.utc(entry.time).format(DATETIME_FORMAT), level: entry.level || LogLevel.Info, message: entry.message, }); diff --git a/webui/react/src/pages/ModelDetails/ModelHeader.tsx b/webui/react/src/pages/ModelDetails/ModelHeader.tsx index 085ab288c2e..7abf13cb51c 100644 --- a/webui/react/src/pages/ModelDetails/ModelHeader.tsx +++ b/webui/react/src/pages/ModelDetails/ModelHeader.tsx @@ -1,3 +1,4 @@ +import dayjs from 'dayjs'; import Alert from 'hew/Alert'; import Button from 'hew/Button'; import Column from 'hew/Column'; @@ -20,7 +21,6 @@ import Avatar from 'components/UserAvatar'; import usePermissions from 'hooks/usePermissions'; import userStore from 'stores/users'; import { ModelItem, Workspace } from 'types'; -import { formatDatetime } from 'utils/datetime'; import { useObservable } from 'utils/observable'; import { getDisplayName } from 'utils/user'; @@ -71,7 +71,7 @@ const ModelHeader: React.FC = ({ icon={} name={user?.username ?? 'Unavailable'} />{' '} - on {formatDatetime(model.creationTime, { format: 'MMM D, YYYY' })} + on {dayjs.utc(model.creationTime).format('MMM D, YYYY')} ); }} diff --git a/webui/react/src/pages/ModelVersionDetails/ModelVersionHeader.tsx b/webui/react/src/pages/ModelVersionDetails/ModelVersionHeader.tsx index bbe2b06d73c..7a2b93ea295 100644 --- a/webui/react/src/pages/ModelVersionDetails/ModelVersionHeader.tsx +++ b/webui/react/src/pages/ModelVersionDetails/ModelVersionHeader.tsx @@ -1,3 +1,4 @@ +import dayjs from 'dayjs'; import Button from 'hew/Button'; import Dropdown, { MenuOption } from 'hew/Dropdown'; import Glossary, { InfoRow } from 'hew/Glossary'; @@ -19,7 +20,6 @@ import Avatar from 'components/UserAvatar'; import usePermissions from 'hooks/usePermissions'; import userStore from 'stores/users'; import { ModelVersion } from 'types'; -import { formatDatetime } from 'utils/datetime'; import { useObservable } from 'utils/observable'; import { getDisplayName } from 'utils/user'; @@ -73,7 +73,7 @@ const ModelVersionHeader: React.FC = ({ icon={} name={user?.username ?? 'Unavailable'} />{' '} - on {formatDatetime(modelVersion.creationTime, { format: 'MMM D, YYYY' })} + on {dayjs.utc(modelVersion.creationTime).format('MMM D, YYYY')} ); }} diff --git a/webui/react/src/utils/datetime.test.ts b/webui/react/src/utils/datetime.test.ts index 173caac52ff..55d17d7a9f8 100644 --- a/webui/react/src/utils/datetime.test.ts +++ b/webui/react/src/utils/datetime.test.ts @@ -1,79 +1,6 @@ -import dayjs from 'dayjs'; - import * as utils from './datetime'; describe('Datetime Utilities', () => { - describe('formatDatetime', () => { - const DATE = [ - '2021-11-23T05:59:59.500Z', - 'December 31, 1980 23:59:59.999Z', - '2021-11-11T01:01:01.000+07:00', - '2021-11-11T11:11:11.000-07:00', - ]; - const FORMAT = ['MMM DD, YYYY HH:mma', 'MMMM DD (dddd)']; - - [ - { input: DATE[0], output: '2021-11-23, 05:59:59' }, - { input: DATE[1], output: '1980-12-31, 23:59:59' }, - ].forEach((test) => { - it(`should format "${test.input}" as default format`, () => { - expect(utils.formatDatetime(test.input)).toBe(test.output); - }); - }); - - [ - { - input: { date: DATE[0], options: { format: FORMAT[0] } }, - output: 'Nov 23, 2021 05:59am', - }, - { - input: { date: DATE[1], options: { format: FORMAT[1] } }, - output: 'December 31 (Wednesday)', - }, - ].forEach((test) => { - const { date, options } = test.input; - it(`should format "${date}" as "${options.format}" in UTC`, () => { - expect(utils.formatDatetime(date, options)).toBe(test.output); - }); - }); - - [ - { - input: { date: DATE[2], options: { format: FORMAT[0], outputUTC: false } }, - output: dayjs.utc(DATE[2]).local().format(FORMAT[0]), - }, - { - input: { date: DATE[3], options: { format: FORMAT[1], outputUTC: false } }, - output: dayjs.utc(DATE[3]).local().format(FORMAT[1]), - }, - ].forEach((test) => { - const { date, options } = test.input; - it(`should format "${date}" as "${options.format}" in local time`, () => { - expect(utils.formatDatetime(date, options)).toBe(test.output); - }); - }); - - [ - { - input: { date: DATE[2], options: { inputUTC: true } }, - output: dayjs.utc(utils.stripTimezone(DATE[2])).format(utils.DEFAULT_DATETIME_FORMAT), - }, - { - input: { date: DATE[3], options: { inputUTC: true, outputUTC: false } }, - output: dayjs - .utc(utils.stripTimezone(DATE[3])) - .local() - .format(utils.DEFAULT_DATETIME_FORMAT), - }, - ].forEach((test) => { - const { date, options } = test.input; - const resultFormat = options.outputUTC ? 'UTC' : 'local time'; - it(`should read "${date}" as UTC and format as ${resultFormat}`, () => { - expect(utils.formatDatetime(date, options)).toBe(test.output); - }); - }); - }); - describe('getDuration', () => { const DATES = ['2021-11-11 01:01:01Z', '2021-11-11 11:11:11Z', 'Nov 11, 2021 11:11:11Z']; @@ -132,18 +59,4 @@ describe('Datetime Utilities', () => { }); }); }); - - describe('stripTimezone', () => { - it('should strip timezone from datetime strings', () => { - const tests = [ - { input: '2021-11-11T00:00:00', output: '2021-11-11T00:00:00' }, - { input: '2021-11-11T00:00:00Z', output: '2021-11-11T00:00:00' }, - { input: '2021-11-11T00:00:00+11:11', output: '2021-11-11T00:00:00' }, - { input: '2021-11-11T00:00:00-05:05', output: '2021-11-11T00:00:00' }, - ]; - tests.forEach((test) => { - expect(utils.stripTimezone(test.input)).toBe(test.output); - }); - }); - }); }); diff --git a/webui/react/src/utils/datetime.ts b/webui/react/src/utils/datetime.ts index f5b6ea30986..b11a07655cc 100644 --- a/webui/react/src/utils/datetime.ts +++ b/webui/react/src/utils/datetime.ts @@ -47,29 +47,6 @@ export const durationInEnglish = humanizeDuration.humanizer({ units: ['y', 'mo', 'w', 'd', 'h', 'm', 's', 'ms'], }); -export const formatDatetime = ( - datetime: string, - options: { format?: string; inputUTC?: boolean; outputUTC?: boolean } = {}, -): string => { - const config = { - format: DEFAULT_DATETIME_FORMAT, - inputUTC: false, - outputUTC: true, - ...options, - }; - // Strip out the timezone info if we want to force UTC input. - const dateString = config.inputUTC ? stripTimezone(datetime) : datetime; - - // `dayjs.utc` respects timezone in the datetime string if available. - let dayjsDate = dayjs.utc(dateString); - - // Prep the date as UTC or local time based on output UTC option. - if (!config.outputUTC) dayjsDate = dayjsDate.local(); - - // Return the formatted date based on provided format. - return dayjsDate.format(config.format); -}; - // Experiment duration (naive) in miliseconds export const getDuration = (times: StartEndTimes): number => { const endTime = times.endTime ? new Date(times.endTime) : new Date(); @@ -79,11 +56,6 @@ export const getDuration = (times: StartEndTimes): number => { export const secondToHour = (seconds: number): number => seconds / 3600; -export const stripTimezone = (datetime: string): string => { - const timezoneRegex = /(Z|(-|\+)\d{2}:\d{2})$/; - return datetime.replace(timezoneRegex, ''); -}; - export const getDurationInEnglish = (record: BulkExperimentItem): string => { const duration = getDuration(record); const options = {