Skip to content

Commit 3f08949

Browse files
tirth23Tirth Patel
andauthored
Add Timezone support in datetimepicker and cards (#4087)
* feat: add timezone support * fix: review comments * fix: review comments --------- Co-authored-by: Tirth Patel <tirth.patel1@ibm.com>
1 parent bd3c3c7 commit 3f08949

19 files changed

Lines changed: 254 additions & 93 deletions

packages/react/src/components/BarChartCard/BarChartCard.jsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const defaultProps = {
5858
series: [],
5959
},
6060
locale: 'en',
61+
timeZone: undefined,
6162
showTimeInGMT: false,
6263
tooltipDateFormatPattern: DAYJS_INPUT_FORMATS.SECONDS,
6364
values: null,
@@ -72,6 +73,7 @@ const BarChartCard = ({
7273
values: initialValues,
7374
availableDimensions,
7475
locale,
76+
timeZone,
7577
i18n,
7678
isExpanded,
7779
isLazyLoading,
@@ -91,6 +93,8 @@ const BarChartCard = ({
9193
defaultDateFormatPattern,
9294
...others
9395
}) => {
96+
const effectiveTimezone = timeZone || dayjs.tz.guess();
97+
dayjs.tz.setDefault(effectiveTimezone);
9498
// need to deep merge the nested content default props as default props only uses a shallow merge natively
9599
const contentWithDefaults = useMemo(
96100
() => defaultsDeep({}, content, defaultProps.content),
@@ -351,7 +355,14 @@ const BarChartCard = ({
351355
valueFormatter: (tooltipValue) =>
352356
chartValueFormatter(tooltipValue, size, unit, locale, decimalPrecision),
353357
customHTML: (...args) =>
354-
handleTooltip(...args, timeDataSourceId, showTimeInGMT, tooltipDateFormatPattern, locale),
358+
handleTooltip(
359+
...args,
360+
timeDataSourceId,
361+
showTimeInGMT,
362+
tooltipDateFormatPattern,
363+
locale,
364+
effectiveTimezone
365+
),
355366
groupLabel: mergedI18n.tooltipGroupLabel,
356367
totalLabel: mergedI18n.tooltipTotalLabel,
357368
},
@@ -395,12 +406,15 @@ const BarChartCard = ({
395406
colors,
396407
mergedI18n.tooltipGroupLabel,
397408
mergedI18n.tooltipTotalLabel,
398-
zoomBar,
409+
zoomBar.enabled,
410+
zoomBar.initialZoomDomain,
411+
zoomBar.view,
399412
size,
400413
isExpanded,
401414
locale,
402415
showTimeInGMT,
403416
tooltipDateFormatPattern,
417+
effectiveTimezone,
404418
]
405419
);
406420

@@ -419,6 +433,7 @@ const BarChartCard = ({
419433
resizeHandles={resizeHandles}
420434
timeRange={timeRange}
421435
locale={locale}
436+
timeZone={effectiveTimezone}
422437
// TODO: remove deprecated testID in v3.
423438
testId={testID || testId}
424439
{...others}

packages/react/src/components/BarChartCard/barChartUtils.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export const formatChartData = (
255255
// grouped charts can't be time-based
256256
...(timeDataSourceId && type !== BAR_CHART_TYPES.GROUPED
257257
? {
258-
date: new Date(value[timeDataSourceId]), // timestamp
258+
date: dayjs.tz(value[timeDataSourceId]).toDate(),
259259
key: value[timeDataSourceId],
260260
}
261261
: { key: value[categoryDataSourceId] }),
@@ -292,12 +292,11 @@ export const formatChartData = (
292292
dataset.forEach((value) => {
293293
// if value is null, don't add it to the formatted chartData
294294
if (!isNil(value[series[0].dataSourceId])) {
295-
const dataDate = new Date(value[timeDataSourceId]);
296295
data.push({
297296
// Use the label if one exists
298297
group: series[0].label ? series[0].label : series[0].dataSourceId, // bar this data belongs to
299298
value: value[series[0].dataSourceId], // there should only be one series here because its a simple bar
300-
date: dataDate, // timestamp
299+
date: dayjs.tz(value[timeDataSourceId]).toDate(),
301300
});
302301
}
303302
});
@@ -421,6 +420,7 @@ export const formatColors = (series, datasetNames, isDashboardPreview, type) =>
421420
* @param {string} timeDatasourceId time-based attribute
422421
* @param {bool} showTimeInGMT
423422
* @param {string} tooltipDataFormatPattern
423+
* @param {string} timezone
424424
*/
425425
export const handleTooltip = (
426426
dataOrHoveredElement,
@@ -429,7 +429,8 @@ export const handleTooltip = (
429429
timeDataSourceId,
430430
showTimeInGMT,
431431
tooltipDateFormatPattern = DAYJS_INPUT_FORMATS.SECONDS,
432-
locale
432+
locale,
433+
timezone
433434
) => {
434435
dayjs.locale(locale);
435436
const data = dataOrHoveredElement.__data__ // eslint-disable-line no-underscore-dangle
@@ -446,10 +447,9 @@ export const handleTooltip = (
446447
const dateLabel = timestamp
447448
? `<li class='datapoint-tooltip'>
448449
<p class='label'>
449-
${(showTimeInGMT // show timestamp in gmt or local time
450-
? dayjs.utc(timestamp)
451-
: dayjs(timestamp)
452-
).format(tooltipDateFormatPattern)}</p>
450+
${(showTimeInGMT ? dayjs.utc(timestamp) : dayjs(timestamp).tz(timezone)).format(
451+
tooltipDateFormatPattern
452+
)}</p>
453453
</li>`
454454
: '';
455455

@@ -523,6 +523,8 @@ export const generateTableColumns = (
523523
* @param {string} type of chart i.e. simple, grouped, stacked
524524
* @param {Array<Object>} values values before they are formatted for charting
525525
* @param {Array<Object>} chartData values after they are formatted for charting
526+
* @param {string} defaultDateFormatPattern date format pattern
527+
* @param {string} timezone timezone to use for formatting dates
526528
*/
527529
export const formatTableData = (
528530
timeDataSourceId,
@@ -551,8 +553,8 @@ export const formatTableData = (
551553
id: `dataindex-${index}`,
552554
values: {
553555
...barTimeValue,
554-
// format the date locally
555-
[timeDataSourceId]: dayjs(timestamp).format(defaultDateFormatPattern),
556+
// format the date locally with timezone
557+
[timeDataSourceId]: dayjs.tz(timestamp).format(defaultDateFormatPattern),
556558
},
557559
isSelectable: false,
558560
});

packages/react/src/components/Card/Card.jsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
import { parseValue } from '../DateTimePicker/dateTimePickerUtils';
3030
import useSizeObserver from '../../hooks/useSizeObserver';
3131
import EmptyState from '../EmptyState/EmptyState';
32-
import { DAYJS_INPUT_FORMATS } from '../../utils/dayjs';
32+
import dayjs, { DAYJS_INPUT_FORMATS } from '../../utils/dayjs';
3333

3434
import CardTypeContent from './CardTypeContent';
3535
import CardToolbar from './CardToolbar';
@@ -326,10 +326,12 @@ const Card = (props) => {
326326
data,
327327
content,
328328
extraHeaderContent,
329+
timeZone,
329330
shouldUseTranslatedLabels,
330331
...others
331332
} = props;
332-
333+
const effectiveTimezone = timeZone || dayjs.tz.guess();
334+
dayjs.tz.setDefault(effectiveTimezone);
333335
// Get translated title if shouldUseTranslatedLabels is true
334336
const title = getTranslatedLabel(titleProp, shouldUseTranslatedLabels, i18n);
335337
const tooltip = getTranslatedLabel(tooltipProp, shouldUseTranslatedLabels, i18n);
@@ -391,13 +393,25 @@ const Card = (props) => {
391393
}
392394

393395
if (mergedAvailableActions.range === 'full' || mergedAvailableActions.range === 'iconOnly') {
394-
const { readableValue } = parseValue(timeRange, dateTimeMask, strings.toLabel);
395-
396+
const { readableValue } = parseValue(
397+
timeRange,
398+
dateTimeMask,
399+
strings.toLabel,
400+
true,
401+
effectiveTimezone
402+
);
396403
return readableValue;
397404
}
398405

399406
return undefined;
400-
}, [dateTimeMask, mergedAvailableActions.range, strings.toLabel, subtitleProp, timeRange]);
407+
}, [
408+
dateTimeMask,
409+
mergedAvailableActions.range,
410+
strings.toLabel,
411+
subtitleProp,
412+
timeRange,
413+
effectiveTimezone,
414+
]);
401415

402416
const [subtitle, setSubtitle] = useState(getTheSubtitle);
403417

@@ -467,6 +481,7 @@ const Card = (props) => {
467481
isExpanded={isExpanded}
468482
timeRange={timeRange}
469483
locale={others.locale}
484+
timeZone={effectiveTimezone}
470485
timeRangeOptions={timeRangeOptions}
471486
onCardAction={cachedOnCardAction}
472487
// TODO: remove deprecated testID prop in v3

packages/react/src/components/Card/CardToolbar.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const CardToolbar = ({
137137
extraActions,
138138
renderDateDropdownInPortal,
139139
id,
140+
timeZone,
140141
}) => {
141142
const mergedI18n = { ...defaultProps.i18n, ...i18n };
142143
const langDir = useLangDirection();
@@ -278,6 +279,7 @@ const CardToolbar = ({
278279
i18n={mergedI18n}
279280
dateTimeMask={dateTimeMask}
280281
locale={locale}
282+
timeZone={timeZone}
281283
hasIconOnly
282284
presets={Object.entries(timeRangeOptions).reduce(
283285
(acc, [timeRangeOptionKey, timeRangeOption]) => {

packages/react/src/components/DateTimePicker/DateTimePicker.jsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { v4 as uuidv4 } from 'uuid';
2020

2121
import TimePickerSpinner from '../TimePickerSpinner/TimePickerSpinner';
2222
import { settings } from '../../constants/Settings';
23-
import dayjs, { DAYJS_INPUT_FORMATS } from '../../utils/dayjs';
23+
import dayjs, { DAYJS_INPUT_FORMATS, detectDateTimeFormat } from '../../utils/dayjs';
2424
import { handleSpecificKeyDown, useOnClickOutside } from '../../utils/componentUtilityFunctions';
2525
import { Tooltip } from '../Tooltip';
2626

@@ -200,6 +200,8 @@ const propTypes = {
200200
light: PropTypes.bool,
201201
/** The language locale used to format the days of the week, months, and numbers. */
202202
locale: PropTypes.string,
203+
/** IANA timezone string to interpret datetime values in (e.g., 'America/New_York') */
204+
timeZone: PropTypes.string,
203205
/** Unique id of the component */
204206
id: PropTypes.string,
205207
style: PropTypes.objectOf(PropTypes.string),
@@ -292,6 +294,7 @@ const defaultProps = {
292294
},
293295
light: false,
294296
locale: 'en',
297+
timeZone: undefined,
295298
id: undefined,
296299
style: {},
297300
};
@@ -315,6 +318,7 @@ const DateTimePicker = ({
315318
i18n,
316319
light,
317320
locale,
321+
timeZone,
318322
style,
319323
...others
320324
}) => {
@@ -323,7 +327,8 @@ const DateTimePicker = ({
323327
...defaultProps.i18n,
324328
...i18n,
325329
};
326-
330+
const effectiveTimezone = timeZone || dayjs.tz.guess();
331+
dayjs.tz.setDefault(effectiveTimezone);
327332
dayjs.locale(locale);
328333

329334
// State
@@ -428,7 +433,13 @@ const DateTimePicker = ({
428433
}
429434

430435
setCurrentValue(value);
431-
const parsedValue = parseValue(value, dateTimeMask, mergedI18n.toLabel);
436+
const parsedValue = parseValue(
437+
value,
438+
dateTimeMask,
439+
mergedI18n.toLabel,
440+
hasTimeInput,
441+
effectiveTimezone
442+
);
432443
setHumanValue(parsedValue.readableValue);
433444

434445
return {
@@ -498,15 +509,21 @@ const DateTimePicker = ({
498509
setIsCustomRange(true);
499510
setCustomRangeKind(PICKER_KINDS.ABSOLUTE);
500511
if (!absolute.hasOwnProperty('start')) {
501-
absolute.start = dayjs(`${absolute.startDate} ${absolute.startTime}`).valueOf();
512+
const startDateTime = `${absolute.startDate} ${absolute.startTime}`;
513+
absolute.start = dayjs
514+
.tz(startDateTime, detectDateTimeFormat(startDateTime), effectiveTimezone)
515+
.valueOf();
502516
}
503517
if (!absolute.hasOwnProperty('end')) {
504-
absolute.end = dayjs(`${absolute.endDate} ${absolute.endTime}`).valueOf();
518+
const endDateTime = `${absolute.endDate} ${absolute.endTime}`;
519+
absolute.end = dayjs
520+
.tz(endDateTime, detectDateTimeFormat(endDateTime), effectiveTimezone)
521+
.valueOf();
505522
}
506-
absolute.startDate = dayjs(absolute.start).format('MM/DD/YYYY');
507-
absolute.startTime = dayjs(absolute.start).format('HH:mm');
508-
absolute.endDate = dayjs(absolute.end).format('MM/DD/YYYY');
509-
absolute.endTime = dayjs(absolute.end).format('HH:mm');
523+
absolute.startDate = dayjs.tz(absolute.start).format('MM/DD/YYYY');
524+
absolute.startTime = dayjs.tz(absolute.start).format('HH:mm');
525+
absolute.endDate = dayjs.tz(absolute.end).format('MM/DD/YYYY');
526+
absolute.endTime = dayjs.tz(absolute.end).format('HH:mm');
510527
setAbsoluteValue(absolute);
511528
}
512529
} else {
@@ -610,7 +627,8 @@ const DateTimePicker = ({
610627
customRangeKind === PICKER_KINDS.ABSOLUTE &&
611628
(absoluteStartTimeInvalid ||
612629
absoluteEndTimeInvalid ||
613-
(absoluteValue.startDate === '' && absoluteValue.endDate === '') ||
630+
!absoluteValue.startDate ||
631+
!absoluteValue.endDate ||
614632
(hasTimeInput ? !absoluteValue.startTime || !absoluteValue.endTime : false));
615633

616634
const disableApply = disableRelativeApply || disableAbsoluteApply;

packages/react/src/components/DateTimePicker/DateTimePicker.story.jsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export const LightVersion = () => {
238238

239239
LightVersion.storyName = 'Light version';
240240

241-
export const Locale = () => {
241+
export const LocaleAndTimezone = () => {
242242
const size = select('size', Object.keys(CARD_SIZES), CARD_SIZES.MEDIUMWIDE);
243243
return (
244244
<div
@@ -250,10 +250,27 @@ export const Locale = () => {
250250
<DateTimePicker
251251
id="datetimepicker25"
252252
dateTimeMask={text('dateTimeMask', 'L HH:mm')}
253-
locale={select('locale', ['en', 'fr', 'ja'], 'fr')}
253+
locale={select('locale', ['en', 'fr', 'ja', 'de', 'es'], 'fr')}
254+
timeZone={select(
255+
'timeZone',
256+
[
257+
'America/Chicago',
258+
'America/Los_Angeles',
259+
'Europe/London',
260+
'Europe/Paris',
261+
'Asia/Tokyo',
262+
'Asia/Kolkata',
263+
'Australia/Sydney',
264+
],
265+
'Europe/Paris'
266+
)}
254267
defaultValue={defaultAbsoluteValue}
255268
hasTimeInput={boolean('hasTimeInput', true)}
269+
onApply={action('onApply')}
270+
onCancel={action('onCancel')}
256271
/>
257272
</div>
258273
);
259274
};
275+
276+
LocaleAndTimezone.storyName = 'Locale and timezone';

packages/react/src/components/DateTimePicker/DateTimePickerV2WithTimeSpinner.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -888,8 +888,8 @@ const DateTimePicker = ({
888888
...value.absolute,
889889
humanValue,
890890
tooltipValue,
891-
ISOStart: value.absolute.start?.toISOString(),
892-
ISOEnd: value.absolute.end?.toISOString(),
891+
ISOStart: value.absolute.start ? new Date(value.absolute.start).toISOString() : undefined,
892+
ISOEnd: value.absolute.end ? new Date(value.absolute.end).toISOString() : undefined,
893893
};
894894
break;
895895
case PICKER_KINDS.SINGLE:

0 commit comments

Comments
 (0)