-
-
-
+
+
+
{hasDaysToKeepField ? (
) : null}
-
+
', () => {
interval: 1,
name: 'Run once schedule',
startDate: '2020-03-25',
- startTime: '10:00:00',
+ startTime: '10:00 AM',
timezone: 'America/New_York',
});
});
@@ -228,7 +224,7 @@ describe('', () => {
name: 'Run every 10 minutes 10 times',
occurrences: 10,
startDate: '2020-03-25',
- startTime: '10:30:00',
+ startTime: '10:30 AM',
timezone: 'America/New_York',
});
});
@@ -248,12 +244,12 @@ describe('', () => {
description: 'test description',
end: 'onDate',
endDate: '2020-03-26',
- endTime: '10:45:00',
+ endTime: '10:45 AM',
frequency: 'hour',
interval: 1,
name: 'Run every hour until date',
startDate: '2020-03-25',
- startTime: '10:45:00',
+ startTime: '10:45 AM',
timezone: 'America/New_York',
});
});
@@ -276,7 +272,7 @@ describe('', () => {
interval: 1,
name: 'Run daily',
startDate: '2020-03-25',
- startTime: '10:45:00',
+ startTime: '10:45 AM',
timezone: 'America/New_York',
});
});
@@ -299,7 +295,7 @@ describe('', () => {
name: 'Run weekly on mon/wed/fri',
occurrences: 1,
startDate: '2020-03-25',
- startTime: '10:45:00',
+ startTime: '10:45 AM',
timezone: 'America/New_York',
});
});
@@ -324,7 +320,7 @@ describe('', () => {
runOn: 'day',
runOnDayNumber: 1,
startDate: '2020-04-01',
- startTime: '10:45',
+ startTime: '10:45 AM',
timezone: 'America/New_York',
});
});
@@ -352,7 +348,7 @@ describe('', () => {
runOnTheDay: 'tuesday',
runOnTheOccurrence: -1,
startDate: '2020-03-31',
- startTime: '11:00',
+ startTime: '11:00 AM',
timezone: 'America/New_York',
});
});
@@ -380,7 +376,7 @@ describe('', () => {
runOn: 'day',
runOnDayMonth: 3,
runOnDayNumber: 1,
- startTime: '00:00',
+ startTime: '12:00 AM',
startDate: '2020-03-01',
timezone: 'America/New_York',
});
@@ -408,7 +404,7 @@ describe('', () => {
runOnTheOccurrence: 2,
runOnTheDay: 'friday',
runOnTheMonth: 4,
- startTime: '11:15',
+ startTime: '11:15 AM',
startDate: '2020-04-10',
timezone: 'America/New_York',
});
@@ -437,7 +433,7 @@ describe('', () => {
runOnTheOccurrence: 1,
runOnTheDay: 'weekday',
runOnTheMonth: 10,
- startTime: '11:15',
+ startTime: '11:15 AM',
startDate: '2020-04-10',
timezone: 'America/New_York',
});
@@ -528,7 +524,7 @@ describe('', () => {
name: mockSchedule.name,
end: 'never',
endDate: '2021-01-29',
- endTime: '14:15:00',
+ endTime: '2:15 PM',
frequency: 'none',
occurrences: 1,
runOn: 'day',
@@ -539,7 +535,7 @@ describe('', () => {
runOnTheOccurrence: 1,
skip_tags: '',
startDate: '2021-01-28',
- startTime: '14:15:00',
+ startTime: '2:15 PM',
timezone: 'America/New_York',
credentials: [
{ id: 3, name: 'Credential 3', kind: 'ssh', url: '' },
@@ -630,7 +626,7 @@ describe('', () => {
name: 'foo',
inventory: 702,
rrule:
- 'DTSTART;TZID=America/New_York:20200402T184500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
+ 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
});
});
@@ -732,7 +728,7 @@ describe('', () => {
interval: 1,
name: 'Run once schedule',
startDate: '2020-03-25',
- startTime: '10:00:00',
+ startTime: '10:00 AM',
timezone: 'America/New_York',
});
});
diff --git a/awx/ui/src/components/Schedule/ScheduleList/ScheduleListItem.js b/awx/ui/src/components/Schedule/ScheduleList/ScheduleListItem.js
index 4dbed67c12..1b33cf4875 100644
--- a/awx/ui/src/components/Schedule/ScheduleList/ScheduleListItem.js
+++ b/awx/ui/src/components/Schedule/ScheduleList/ScheduleListItem.js
@@ -103,7 +103,7 @@ function ScheduleListItem({
)}
diff --git a/awx/ui/src/components/Schedule/ScheduleOccurrences/ScheduleOccurrences.js b/awx/ui/src/components/Schedule/ScheduleOccurrences/ScheduleOccurrences.js
index 209b965ebe..d1c9ad0274 100644
--- a/awx/ui/src/components/Schedule/ScheduleOccurrences/ScheduleOccurrences.js
+++ b/awx/ui/src/components/Schedule/ScheduleOccurrences/ScheduleOccurrences.js
@@ -1,11 +1,11 @@
import 'styled-components/macro';
import React, { useState } from 'react';
-import { shape } from 'prop-types';
+import { shape, string } from 'prop-types';
import styled from 'styled-components';
import { t } from '@lingui/macro';
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
-import { formatDateString, formatDateStringUTC } from 'util/dates';
+import { formatDateString } from 'util/dates';
import { DetailName, DetailValue } from '../../DetailList';
import MultiButtonToggle from '../../MultiButtonToggle';
@@ -22,7 +22,7 @@ const OccurrencesLabel = styled.div`
}
`;
-function ScheduleOccurrences({ preview = { local: [], utc: [] } }) {
+function ScheduleOccurrences({ preview = { local: [], utc: [] }, tz }) {
const [mode, setMode] = useState('local');
if (preview.local.length < 2) {
@@ -64,8 +64,8 @@ function ScheduleOccurrences({ preview = { local: [], utc: [] } }) {
{preview[mode].map((dateStr) => (
{mode === 'local'
- ? formatDateString(dateStr)
- : formatDateStringUTC(dateStr)}
+ ? formatDateString(dateStr, tz)
+ : formatDateString(dateStr, 'UTC')}
))}
@@ -75,10 +75,12 @@ function ScheduleOccurrences({ preview = { local: [], utc: [] } }) {
ScheduleOccurrences.propTypes = {
preview: shape(),
+ tz: string,
};
ScheduleOccurrences.defaultProps = {
preview: { local: [], utc: [] },
+ tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
export default ScheduleOccurrences;
diff --git a/awx/ui/src/components/Schedule/shared/ScheduleForm.js b/awx/ui/src/components/Schedule/shared/ScheduleForm.js
index e788b84831..782c192b87 100644
--- a/awx/ui/src/components/Schedule/shared/ScheduleForm.js
+++ b/awx/ui/src/components/Schedule/shared/ScheduleForm.js
@@ -1,6 +1,7 @@
import React, { useEffect, useCallback, useState } from 'react';
import { shape, func } from 'prop-types';
+import { DateTime } from 'luxon';
import { t } from '@lingui/macro';
import { Formik, useField } from 'formik';
import { RRule } from 'rrule';
@@ -190,13 +191,11 @@ function ScheduleForm({
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
let rruleError;
- const now = new Date();
- const closestQuarterHour = new Date(
- Math.ceil(now.getTime() / 900000) * 900000
+ const now = DateTime.now();
+ const closestQuarterHour = DateTime.fromMillis(
+ Math.ceil(now.ts / 900000) * 900000
);
- const tomorrow = new Date(closestQuarterHour);
- tomorrow.setDate(tomorrow.getDate() + 1);
-
+ const tomorrow = closestQuarterHour.plus({ days: 1 });
const isTemplate =
resource.type === 'workflow_job_template' ||
resource.type === 'job_template';
@@ -376,9 +375,9 @@ function ScheduleForm({
) {
showPromptButton = true;
}
- const [currentDate, time] = dateToInputDateTime(closestQuarterHour);
+ const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
- const [tomorrowDate] = dateToInputDateTime(tomorrow);
+ const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO());
const initialValues = {
daysOfWeek: [],
description: schedule.description || '',
@@ -448,7 +447,10 @@ function ScheduleForm({
} = RRule.fromString(schedule.rrule.replace(' ', '\n'));
if (dtstart) {
- const [startDate, startTime] = dateToInputDateTime(schedule.dtstart);
+ const [startDate, startTime] = dateToInputDateTime(
+ schedule.dtstart,
+ schedule.timezone
+ );
overriddenValues.startDate = startDate;
overriddenValues.startTime = startTime;
@@ -457,7 +459,10 @@ function ScheduleForm({
if (schedule.until) {
overriddenValues.end = 'onDate';
- const [endDate, endTime] = dateToInputDateTime(schedule.until);
+ const [endDate, endTime] = dateToInputDateTime(
+ schedule.until,
+ schedule.timezone
+ );
overriddenValues.endDate = endDate;
overriddenValues.endTime = endTime;
@@ -550,7 +555,10 @@ function ScheduleForm({
startDate,
} = values;
- if (end === 'onDate' && new Date(startDate) >= new Date(endDate)) {
+ if (
+ end === 'onDate' &&
+ DateTime.fromISO(startDate) >= DateTime.fromISO(endDate)
+ ) {
errors.endDate = t`Please select an end date/time that comes after the start date/time.`;
}
diff --git a/awx/ui/src/components/Schedule/shared/ScheduleForm.test.js b/awx/ui/src/components/Schedule/shared/ScheduleForm.test.js
index 288f07cd08..1cb4e0a0b0 100644
--- a/awx/ui/src/components/Schedule/shared/ScheduleForm.test.js
+++ b/awx/ui/src/components/Schedule/shared/ScheduleForm.test.js
@@ -1,5 +1,6 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
+import { DateTime } from 'luxon';
import { dateToInputDateTime } from 'util/dates';
import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from 'api';
@@ -105,7 +106,7 @@ const nonRRuleValuesMatch = () => {
wrapper.find('DatePicker[aria-label="Start date"]').prop('value')
).toBe('2020-04-02');
expect(wrapper.find('TimePicker[aria-label="Start time"]').prop('time')).toBe(
- '6:45 PM'
+ '2:45 PM'
);
expect(wrapper.find('select#schedule-timezone').prop('value')).toBe(
'America/New_York'
@@ -474,11 +475,11 @@ describe('', () => {
});
test('initially renders expected fields and values', () => {
- const now = new Date();
- const closestQuarterHour = new Date(
- Math.ceil(now.getTime() / 900000) * 900000
+ const now = DateTime.now();
+ const closestQuarterHour = DateTime.fromMillis(
+ Math.ceil(now.ts / 900000) * 900000
);
- const [date, time] = dateToInputDateTime(closestQuarterHour);
+ const [date, time] = dateToInputDateTime(closestQuarterHour.toISO());
expect(wrapper.find('ScheduleForm').length).toBe(1);
defaultFieldsVisible();
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(0);
diff --git a/awx/ui/src/components/Schedule/shared/buildRuleObj.js b/awx/ui/src/components/Schedule/shared/buildRuleObj.js
index 7d6000e701..ae375c672f 100644
--- a/awx/ui/src/components/Schedule/shared/buildRuleObj.js
+++ b/awx/ui/src/components/Schedule/shared/buildRuleObj.js
@@ -1,14 +1,12 @@
import { t } from '@lingui/macro';
import { RRule } from 'rrule';
+import { DateTime } from 'luxon';
import { getRRuleDayConstants } from 'util/dates';
-const parseTime = (time) => {
- const [hour, minute, ampm] = time.split(/[: ]/);
- const timeHour =
- ampm === 'PM' && hour !== '12' ? `${parseInt(hour, 10) + 12}` : `${hour}`;
-
- return [timeHour, minute];
-};
+const parseTime = (time) => [
+ DateTime.fromFormat(time, 'h:mm a').hour,
+ DateTime.fromFormat(time, 'h:mm a').minute,
+];
export default function buildRuleObj(values) {
// Dates are formatted like "YYYY-MM-DD"
diff --git a/awx/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.js b/awx/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.js
index 7e52e9d936..83452f5fe8 100644
--- a/awx/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.js
+++ b/awx/ui/src/screens/Setting/Subscription/SubscriptionDetail/SubscriptionDetail.js
@@ -12,11 +12,7 @@ import RoutedTabs from 'components/RoutedTabs';
import { CardBody, CardActionsRow } from 'components/Card';
import { DetailList, Detail, NumberSinceDetail } from 'components/DetailList';
import { useConfig } from 'contexts/Config';
-import {
- formatDateString,
- formatDateStringUTC,
- secondsToDays,
-} from 'util/dates';
+import { formatDateString, secondsToDays } from 'util/dates';
function SubscriptionDetail() {
const { me = {}, license_info, version } = useConfig();
@@ -98,8 +94,9 @@ function SubscriptionDetail() {
label={t`Expires on UTC`}
value={
license_info.license_date &&
- formatDateStringUTC(
- new Date(license_info.license_date * 1000).toISOString()
+ formatDateString(
+ new Date(license_info.license_date * 1000).toISOString(),
+ 'UTC'
)
}
/>
diff --git a/awx/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionModal.js b/awx/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionModal.js
index 7701017f0a..54ecd612a6 100644
--- a/awx/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionModal.js
+++ b/awx/ui/src/screens/Setting/Subscription/SubscriptionEdit/SubscriptionModal.js
@@ -20,7 +20,7 @@ import {
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import { ConfigAPI } from 'api';
-import { formatDateStringUTC } from 'util/dates';
+import { formatDateString } from 'util/dates';
import useRequest from 'hooks/useRequest';
import useSelected from 'hooks/useSelected';
import ErrorDetail from 'components/ErrorDetail';
@@ -168,8 +168,9 @@ function SubscriptionModal({
{subscription.instance_count}
- {formatDateStringUTC(
- new Date(subscription.license_date * 1000).toISOString()
+ {formatDateString(
+ new Date(subscription.license_date * 1000).toISOString(),
+ 'UTC'
)}
|
diff --git a/awx/ui/src/util/dates.js b/awx/ui/src/util/dates.js
index 4c77d883c0..552ebc95d9 100644
--- a/awx/ui/src/util/dates.js
+++ b/awx/ui/src/util/dates.js
@@ -1,62 +1,43 @@
/* eslint-disable import/prefer-default-export */
import { t } from '@lingui/macro';
import { RRule } from 'rrule';
-import { getLanguage } from './language';
+import { DateTime, Duration } from 'luxon';
-const prependZeros = (value) => value.toString().padStart(2, 0);
-
-export function formatDateString(dateString, lang = getLanguage(navigator)) {
- if (dateString === null) {
+export function formatDateString(dateObj, tz = null) {
+ if (dateObj === null) {
return null;
}
- return new Date(dateString).toLocaleString(lang);
-}
-export function formatDateStringUTC(dateString, lang = getLanguage(navigator)) {
- if (dateString === null) {
- return null;
- }
- return new Date(dateString).toLocaleString(lang, { timeZone: 'UTC' });
+ return tz !== null
+ ? DateTime.fromISO(dateObj, { zone: tz }).toLocaleString(
+ DateTime.DATETIME_SHORT_WITH_SECONDS
+ )
+ : DateTime.fromISO(dateObj).toLocaleString(
+ DateTime.DATETIME_SHORT_WITH_SECONDS
+ );
}
export function secondsToHHMMSS(seconds) {
- return new Date(seconds * 1000).toISOString().substr(11, 8);
+ return Duration.fromObject({ seconds }).toFormat('hh:mm:ss');
}
export function secondsToDays(seconds) {
- let duration = Math.floor(parseInt(seconds, 10) / 86400);
- if (duration < 0) {
- duration = 0;
- }
- return duration.toString();
+ return Duration.fromObject({ seconds }).toFormat('d');
}
export function timeOfDay() {
- const date = new Date();
- const hour = date.getHours();
- const minute = prependZeros(date.getMinutes());
- const second = prependZeros(date.getSeconds());
- const time =
- hour > 12
- ? `${hour - 12}:${minute}:${second} PM`
- : `${hour}:${minute}:${second} AM`;
- return time;
+ const dateTime = DateTime.local();
+ return dateTime.toFormat('hh:mm a');
}
-export function dateToInputDateTime(dateObj) {
- let date = dateObj;
- if (typeof dateObj === 'string') {
- date = new Date(dateObj);
+export function dateToInputDateTime(dt, tz = null) {
+ let dateTime;
+ if (tz) {
+ dateTime = DateTime.fromISO(dt, { zone: tz });
+ } else {
+ dateTime = DateTime.fromISO(dt);
}
- const year = date.getFullYear();
- const month = prependZeros(date.getMonth() + 1);
- const day = prependZeros(date.getDate());
- const hour =
- date.getHours() > 12 ? parseInt(date.getHours(), 10) - 12 : date.getHours();
- const minute = prependZeros(date.getMinutes());
- const amPmText = date.getHours() > 11 ? 'PM' : 'AM';
-
- return [`${year}-${month}-${day}`, `${hour}:${minute} ${amPmText}`];
+ return [dateTime.toFormat('yyyy-LL-dd'), dateTime.toFormat('h:mm a')];
}
export function getRRuleDayConstants(dayString) {
diff --git a/awx/ui/src/util/dates.test.js b/awx/ui/src/util/dates.test.js
index ae62932ac8..fc71d4d85f 100644
--- a/awx/ui/src/util/dates.test.js
+++ b/awx/ui/src/util/dates.test.js
@@ -2,7 +2,6 @@ import { RRule } from 'rrule';
import {
dateToInputDateTime,
formatDateString,
- formatDateStringUTC,
getRRuleDayConstants,
secondsToDays,
secondsToHHMMSS,
@@ -21,35 +20,20 @@ const i18n = {
describe('formatDateString', () => {
test('it returns the expected value', () => {
- const lang = 'en-US';
- expect(formatDateString(null, lang)).toEqual(null);
- expect(formatDateString('', lang)).toEqual('Invalid Date');
- expect(formatDateString({}, lang)).toEqual('Invalid Date');
- expect(formatDateString(undefined, lang)).toEqual('Invalid Date');
- expect(formatDateString('foobar', lang)).toEqual('Invalid Date');
- expect(formatDateString('2018-011-31T01:14:52.969227Z', lang)).toEqual(
- 'Invalid Date'
+ expect(formatDateString(null)).toEqual(null);
+ expect(formatDateString('')).toEqual('Invalid DateTime');
+ expect(formatDateString({})).toEqual('Invalid DateTime');
+ expect(formatDateString(undefined)).toEqual('Invalid DateTime');
+ expect(formatDateString('foobar')).toEqual('Invalid DateTime');
+ expect(formatDateString('2018-011-31T01:14:52.969227Z', undefined)).toEqual(
+ 'Invalid DateTime'
);
- expect(formatDateString('2018-01-31T01:14:52.969227Z', lang)).toEqual(
- '1/31/2018, 1:14:52 AM'
- );
- });
-});
-
-describe('formatDateStringUTC', () => {
- test('it returns the expected value', () => {
- const lang = 'en-US';
- expect(formatDateStringUTC(null, lang)).toEqual(null);
- expect(formatDateStringUTC('', lang)).toEqual('Invalid Date');
- expect(formatDateStringUTC({}, lang)).toEqual('Invalid Date');
- expect(formatDateStringUTC(undefined, lang)).toEqual('Invalid Date');
- expect(formatDateStringUTC('foobar', lang)).toEqual('Invalid Date');
- expect(formatDateStringUTC('2018-011-31T01:14:52.969227Z', lang)).toEqual(
- 'Invalid Date'
- );
- expect(formatDateStringUTC('2018-01-31T01:14:52.969227Z', lang)).toEqual(
+ expect(formatDateString('2018-01-31T01:14:52.969227Z')).toEqual(
'1/31/2018, 1:14:52 AM'
);
+ expect(
+ formatDateString('2018-01-31T01:14:52.969227Z', 'America/Los_Angeles')
+ ).toEqual('1/30/2018, 5:14:52 PM');
});
});
@@ -68,9 +52,10 @@ describe('secondsToHHMMSS', () => {
describe('dateToInputDateTime', () => {
test('it returns the expected value', () => {
- expect(
- dateToInputDateTime(new Date('2018-01-31T01:14:52.969227Z'))
- ).toEqual(['2018-01-31', '1:14 AM']);
+ expect(dateToInputDateTime('2018-01-31T01:14:52.969227Z')).toEqual([
+ '2018-01-31',
+ '1:14 AM',
+ ]);
});
});