Skip to content

Commit e794584

Browse files
authored
fix: allow RFC3339 as indicated in the placeholder for tick start (#3549)
1 parent f155979 commit e794584

File tree

7 files changed

+181
-36
lines changed

7 files changed

+181
-36
lines changed

src/utils/datetime/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const DEFAULT_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
22
export const STRICT_ISO8061_TIME_FORMAT = 'STRICT_ISO8061_TIME_FORMAT'
3+
export const RFC3339_PATTERN = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(\s|T)?(\d\d)(:)?(\d\d)(:)?(\d\d)(\.\d+)?(Z|([+-])(\d\d)(:)?(\d\d))/

src/utils/datetime/formatters.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
convertDateToRFC3339,
23
createDateTimeFormatter,
34
createRelativeFormatter,
45
} from 'src/utils/datetime/formatters'
@@ -392,6 +393,33 @@ describe('the DateTime formatter', () => {
392393
expect(formatter.format(date)).toBe(`${hour}:00:00.000 PM`)
393394
})
394395
})
396+
397+
describe('convert date to local time in RFC3339 format', () => {
398+
it('can reject invalid dates', () => {
399+
expect(convertDateToRFC3339(new Date('abcd'), 'Local')).toEqual(
400+
'Invalid Date'
401+
)
402+
expect(convertDateToRFC3339(new Date('abcd'), 'UTC')).toEqual(
403+
'Invalid Date'
404+
)
405+
})
406+
407+
it('can use a converted date as the argument to create a valid Date', () => {
408+
let convertedDateString = convertDateToRFC3339(new Date(), 'Local')
409+
let date = new Date(convertedDateString)
410+
expect(date.toDateString()).not.toEqual('Invalid Date')
411+
expect(() => {
412+
date.toISOString()
413+
}).not.toThrow()
414+
415+
convertedDateString = convertDateToRFC3339(new Date(), 'UTC')
416+
date = new Date(convertedDateString)
417+
expect(date.toDateString()).not.toEqual('Invalid Date')
418+
expect(() => {
419+
date.toISOString()
420+
}).not.toThrow()
421+
})
422+
})
395423
})
396424

397425
describe('the relative DateTime formatter', () => {

src/utils/datetime/formatters.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// TODO: handle these any types
22

33
import {TimeZone} from 'src/types'
4+
import {STRICT_ISO8061_TIME_FORMAT} from 'src/utils/datetime/constants'
45

56
const dateTimeOptions: any = {
67
hourCycle: 'h23',
@@ -69,6 +70,12 @@ export const createDateTimeFormatter = (
6970
break
7071
}
7172

73+
case STRICT_ISO8061_TIME_FORMAT: {
74+
return {
75+
format: date => date,
76+
}
77+
}
78+
7279
case 'YYYY-MM-DD': {
7380
const options = {
7481
...dateTimeOptions,
@@ -778,3 +785,26 @@ export const createDateTimeFormatter = (
778785
}
779786
}
780787
}
788+
789+
export const convertDateToRFC3339 = (date: Date, timeZone: string): string => {
790+
if (!date || date.toDateString() === 'Invalid Date') {
791+
return date.toDateString()
792+
}
793+
794+
if (timeZone === 'Local') {
795+
const year = date.getFullYear()
796+
const month =
797+
date.getMonth() + 1 < 10
798+
? `0${date.getMonth() + 1}`
799+
: `${date.getMonth() + 1}`
800+
const dayOfMonth =
801+
date.getDate() < 10 ? `0${date.getDate()}` : `${date.getDate()}`
802+
803+
const timeStringParsed = date.toTimeString().split(' ')
804+
const localTime = timeStringParsed[0]
805+
const utcOffset = timeStringParsed[1].replace('GMT', '')
806+
807+
return `${year}-${month}-${dayOfMonth} ${localTime}${utcOffset}`
808+
}
809+
return date.toISOString()
810+
}

src/utils/datetime/validator.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isValid, isValidStrictly} from './validator'
1+
import {isValid, isValidRFC3339, isValidStrictly} from './validator'
22

33
describe('the datetime validator', () => {
44
it('should return true on valid date formats', function() {
@@ -55,6 +55,7 @@ describe('the datetime validator', () => {
5555
)
5656
).toBeTruthy()
5757
})
58+
5859
it('should return false on invalid date formats', function() {
5960
expect(isValid('1999-02-09 23:00', 'YYYY-MM-DD HH:mm:ss')).toBeFalsy()
6061
expect(
@@ -98,6 +99,7 @@ describe('the datetime validator', () => {
9899
)
99100
).toBeFalsy()
100101
})
102+
101103
it('should be strict on date formats', function() {
102104
expect(
103105
isValidStrictly('1999-02-09 23:00:0', 'YYYY-MM-DD HH:mm:ss')
@@ -150,4 +152,54 @@ describe('the datetime validator', () => {
150152
)
151153
).toBeFalsy()
152154
})
155+
156+
describe('can validate RFC3339 format', () => {
157+
it('can identify valid RFC3339 format', () => {
158+
expect(isValidRFC3339(new Date().toISOString())).toBeTruthy()
159+
160+
expect(isValidRFC3339('2022-01-11T23:00:00Z')).toBeTruthy()
161+
expect(isValidRFC3339('2022-01-11T00:00:00Z')).toBeTruthy()
162+
expect(isValidRFC3339('2022-01-11T23:00:00+0800')).toBeTruthy()
163+
expect(isValidRFC3339('2022-01-11T23:00:00+08:00')).toBeTruthy()
164+
expect(isValidRFC3339('2022-01-11T23:00:00-0800')).toBeTruthy()
165+
expect(isValidRFC3339('2022-01-11T23:00:00-08:00')).toBeTruthy()
166+
expect(isValidRFC3339('2022-01-11T23:00:00+00:00')).toBeTruthy()
167+
expect(isValidRFC3339('2022-01-11T23:00:00-00:00')).toBeTruthy()
168+
expect(isValidRFC3339('2022-01-11T23:00:00+0000')).toBeTruthy()
169+
expect(isValidRFC3339('2022-01-11T23:00:00-0000')).toBeTruthy()
170+
expect(isValidRFC3339('2022-01-11T23:00:00+10:30')).toBeTruthy()
171+
172+
expect(isValidRFC3339('2022-01-11 23:00:00Z')).toBeTruthy()
173+
expect(isValidRFC3339('2022-01-11 00:00:00Z')).toBeTruthy()
174+
expect(isValidRFC3339('2022-01-11 23:00:00+0800')).toBeTruthy()
175+
expect(isValidRFC3339('2022-01-11 23:00:00+08:00')).toBeTruthy()
176+
expect(isValidRFC3339('2022-01-11 23:00:00-0800')).toBeTruthy()
177+
expect(isValidRFC3339('2022-01-11 23:00:00-08:00')).toBeTruthy()
178+
expect(isValidRFC3339('2022-01-11 23:00:00+00:00')).toBeTruthy()
179+
expect(isValidRFC3339('2022-01-11 23:00:00-00:00')).toBeTruthy()
180+
expect(isValidRFC3339('2022-01-11 23:00:00+0000')).toBeTruthy()
181+
expect(isValidRFC3339('2022-01-11 23:00:00-0000')).toBeTruthy()
182+
expect(isValidRFC3339('2022-01-11 23:00:00+10:30')).toBeTruthy()
183+
})
184+
185+
it('can identify invalid RFC3339 format', () => {
186+
expect(isValidRFC3339('22-01-11T23:00:00')).toBeFalsy()
187+
expect(isValidRFC3339('01-11T23:00:00')).toBeFalsy()
188+
expect(isValidRFC3339('20220111T230000Z')).toBeFalsy()
189+
expect(isValidRFC3339('2022-344T23:00:00Z')).toBeFalsy()
190+
expect(isValidRFC3339('2022-01-11T23:00:00')).toBeFalsy()
191+
expect(isValidRFC3339('2022-01-11T23:00:00+Z')).toBeFalsy()
192+
expect(isValidRFC3339('2022-01-11T23:00:00+0')).toBeFalsy()
193+
expect(isValidRFC3339('2022-01-11T23:00:00+8')).toBeFalsy()
194+
195+
expect(isValidRFC3339('22-01-11 23:00:00')).toBeFalsy()
196+
expect(isValidRFC3339('01-11 23:00:00')).toBeFalsy()
197+
expect(isValidRFC3339('20220111230000Z')).toBeFalsy()
198+
expect(isValidRFC3339('2022-344 23:00:00Z')).toBeFalsy()
199+
expect(isValidRFC3339('2022-01-11 23:00:00')).toBeFalsy()
200+
expect(isValidRFC3339('2022-01-11 23:00:00+Z')).toBeFalsy()
201+
expect(isValidRFC3339('2022-01-11 23:00:00+0')).toBeFalsy()
202+
expect(isValidRFC3339('2022-01-11 23:00:00+8')).toBeFalsy()
203+
})
204+
})
153205
})

src/utils/datetime/validator.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {DateTime} from 'luxon'
2-
import {DEFAULT_TIME_FORMAT} from 'src/utils/datetime/constants'
2+
import {
3+
DEFAULT_TIME_FORMAT,
4+
RFC3339_PATTERN,
5+
} from 'src/utils/datetime/constants'
36

47
const formatToLuxonMap = {
58
[DEFAULT_TIME_FORMAT]: {
@@ -114,3 +117,9 @@ export const isValidStrictly = (
114117
DateTime.fromFormat(formattedDateTimeString, dateFnsFormatString).isValid
115118
)
116119
}
120+
121+
export const isValidRFC3339 = (input: string) => {
122+
return (
123+
RFC3339_PATTERN.test(input) && new Date(input).toString() !== 'Invalid Date'
124+
)
125+
}

src/visualization/components/internal/AxisTicksGenerator.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,6 @@ const AxisTicksGenerator: FC<Props> = ({
110110
update={update}
111111
elementStylingClass="value-tick-input--custom"
112112
/>
113-
<TimeTickInput
114-
axisName={axisName}
115-
tickPropertyName="start"
116-
tickOptions={generateAxisTicks}
117-
initialTickOptionValue={tickStart}
118-
update={update}
119-
/>
120113
<ValueTickInput
121114
axisName={axisName}
122115
tickPropertyName="step"
@@ -126,6 +119,13 @@ const AxisTicksGenerator: FC<Props> = ({
126119
placeholder="milliseconds"
127120
update={update}
128121
/>
122+
<TimeTickInput
123+
axisName={axisName}
124+
tickPropertyName="start"
125+
tickOptions={generateAxisTicks}
126+
initialTickOptionValue={tickStart}
127+
update={update}
128+
/>
129129
</Grid.Row>
130130
)
131131
} else {
@@ -142,18 +142,18 @@ const AxisTicksGenerator: FC<Props> = ({
142142
/>
143143
<ValueTickInput
144144
axisName={axisName}
145-
tickPropertyName="start"
145+
tickPropertyName="step"
146146
tickOptions={generateAxisTicks}
147-
initialTickOptionValue={tickStart}
148-
label="Start Tick Marks At"
147+
initialTickOptionValue={tickStep}
148+
label="Tick Mark Interval"
149149
update={update}
150150
/>
151151
<ValueTickInput
152152
axisName={axisName}
153-
tickPropertyName="step"
153+
tickPropertyName="start"
154154
tickOptions={generateAxisTicks}
155-
initialTickOptionValue={tickStep}
156-
label="Tick Mark Interval"
155+
initialTickOptionValue={tickStart}
156+
label="Start Tick Marks At"
157157
update={update}
158158
/>
159159
</Grid.Row>

src/visualization/components/internal/TimeTickInput.tsx

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@ import React, {
55
CSSProperties,
66
FC,
77
RefObject,
8+
useContext,
89
useState,
910
} from 'react'
1011

11-
// Utils
12-
import {useOneWayState} from 'src/shared/utils/useOneWayState'
13-
import {convertUserInputValueToNumOrNaN} from 'src/shared/utils/convertUserInput'
14-
1512
// Components
1613
import DatePicker from 'src/shared/components/dateRangePicker/DatePicker'
1714
import {ClickOutside} from 'src/shared/components/ClickOutside'
@@ -32,8 +29,14 @@ import {
3229
PopoverPosition,
3330
} from '@influxdata/clockface'
3431

35-
import {TICK_PROPERTY_PREFIX} from 'src/visualization/constants'
32+
// Context
33+
import {AppSettingContext} from 'src/shared/contexts/app'
34+
35+
// Utils
36+
import {isValidRFC3339} from 'src/utils/datetime/validator'
37+
import {convertDateToRFC3339} from 'src/utils/datetime/formatters'
3638

39+
import {TICK_PROPERTY_PREFIX} from 'src/visualization/constants'
3740
interface TimeTickInputProps {
3841
axisName: string
3942
tickPropertyName: string
@@ -45,20 +48,33 @@ interface TimeTickInputProps {
4548

4649
const noOp = () => {}
4750

51+
const getInitialTimeTick = (
52+
initialTick: string | number,
53+
timeZone: string
54+
): string => {
55+
if (typeof initialTick === 'number' && initialTick === initialTick) {
56+
const initialDate = new Date(initialTick)
57+
if (isValidRFC3339(initialDate.toISOString())) {
58+
return convertDateToRFC3339(initialDate, timeZone)
59+
}
60+
}
61+
return ''
62+
}
63+
4864
export const TimeTickInput: FC<TimeTickInputProps> = props => {
4965
const {
5066
axisName,
5167
tickPropertyName,
5268
tickOptions,
5369
initialTickOptionValue,
54-
dateFormatPlaceholder = 'RFC3339',
70+
dateFormatPlaceholder = 'RFC 3339 or YYYY-MM-DD HH:MM:SSZ',
5571
update,
5672
} = props
5773

58-
const [tickOptionInput, setTickOptionInput] = useOneWayState(
59-
initialTickOptionValue === initialTickOptionValue
60-
? initialTickOptionValue
61-
: ''
74+
const {timeZone} = useContext(AppSettingContext)
75+
76+
const [tickOptionInput, setTickOptionInput] = useState<string>(
77+
getInitialTimeTick(initialTickOptionValue, timeZone)
6278
)
6379

6480
const [tickOptionInputStatus, setTickOptionInputStatus] = useState<
@@ -74,10 +90,9 @@ export const TimeTickInput: FC<TimeTickInputProps> = props => {
7490

7591
const triggerRef: RefObject<ButtonRef> = createRef()
7692

77-
const updateTickOption = (value?: string | number) => {
78-
const convertedValue = convertUserInputValueToNumOrNaN(
79-
value === undefined ? tickOptionInput : value
80-
)
93+
const updateTickOption = (value?: string) => {
94+
const dateString = value === undefined ? tickOptionInput : value
95+
const convertedValue = new Date(dateString).valueOf()
8196
const tickOptionNameWithoutAxis = `${TICK_PROPERTY_PREFIX}${tickPropertyName
8297
.slice(0, 1)
8398
.toUpperCase()}${tickPropertyName.slice(1).toLowerCase()}`
@@ -99,7 +114,11 @@ export const TimeTickInput: FC<TimeTickInputProps> = props => {
99114

100115
const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
101116
setTickOptionInput(event.target.value)
102-
setTickOptionInputStatus(ComponentStatus.Default)
117+
if (isValidRFC3339(event.target.value)) {
118+
setTickOptionInputStatus(ComponentStatus.Default)
119+
} else {
120+
setTickOptionInputStatus(ComponentStatus.Error)
121+
}
103122
}
104123

105124
const handleBlur = () => updateTickOption()
@@ -121,6 +140,7 @@ export const TimeTickInput: FC<TimeTickInputProps> = props => {
121140
const handleReset = () => {
122141
setTickOptionInput('')
123142
updateTickOption('')
143+
setTickOptionInputStatus(ComponentStatus.Default)
124144
}
125145

126146
const showDatePicker = () => setIsDatePickerOpen(true)
@@ -135,10 +155,15 @@ export const TimeTickInput: FC<TimeTickInputProps> = props => {
135155
}
136156

137157
const handleSelectDate = (date: string) => {
138-
const dateRFC3339 = new Date(date).valueOf()
139-
setTickOptionInput(dateRFC3339 === dateRFC3339 ? String(dateRFC3339) : '')
140-
setTickOptionInputStatus(ComponentStatus.Default)
141-
updateTickOption(dateRFC3339)
158+
if (isValidRFC3339(date)) {
159+
const dateRFC3339 = convertDateToRFC3339(new Date(date), timeZone)
160+
setTickOptionInput(dateRFC3339)
161+
setTickOptionInputStatus(ComponentStatus.Default)
162+
updateTickOption(dateRFC3339)
163+
} else {
164+
setTickOptionInput('')
165+
setTickOptionInputStatus(ComponentStatus.Error)
166+
}
142167
}
143168

144169
const onClickOutside = () => {
@@ -160,7 +185,7 @@ export const TimeTickInput: FC<TimeTickInputProps> = props => {
160185

161186
return (
162187
<>
163-
<Grid.Column widthXS={Columns.Six}>
188+
<Grid.Column widthXS={Columns.Twelve}>
164189
<Form.Element label="Start Tick Marks At">
165190
<Input
166191
placeholder={dateFormatPlaceholder}
@@ -173,7 +198,7 @@ export const TimeTickInput: FC<TimeTickInputProps> = props => {
173198
/>
174199
</Form.Element>
175200
</Grid.Column>
176-
<Grid.Column widthXS={Columns.Six}>
201+
<Grid.Column widthXS={Columns.Twelve}>
177202
<Form.Element label="Date Picker">
178203
<Popover
179204
appearance={Appearance.Outline}

0 commit comments

Comments
 (0)