Skip to content

Commit 6560288

Browse files
committed
feat(js): Add TimezoneProvider
1 parent eedde2f commit 6560288

File tree

7 files changed

+248
-49
lines changed

7 files changed

+248
-49
lines changed

static/app/components/dateTime.spec.tsx

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
1-
import {ConfigFixture} from 'sentry-fixture/config';
21
import {UserFixture} from 'sentry-fixture/user';
32

43
import {render, screen} from 'sentry-test/reactTestingLibrary';
54

6-
import {DateTime} from 'sentry/components/dateTime';
75
import ConfigStore from 'sentry/stores/configStore';
86

9-
describe('DateTime', () => {
10-
const user = UserFixture({
11-
options: {
12-
...UserFixture().options,
13-
clock24Hours: false,
14-
timezone: 'America/Los_Angeles',
15-
},
16-
});
7+
import {DateTime} from './dateTime';
8+
import {TimezoneProvider} from './timezoneProvider';
179

18-
beforeAll(() => {
19-
ConfigStore.loadInitialData(ConfigFixture({user}));
20-
});
10+
describe('DateTime', () => {
11+
function renderPDT(child: React.ReactElement) {
12+
return render(
13+
<TimezoneProvider timezone="America/Los_Angeles">{child}</TimezoneProvider>
14+
);
15+
}
2116

2217
it('renders a date', () => {
23-
render(<DateTime date={new Date()} />);
18+
renderPDT(<DateTime date={new Date()} />);
2419
expect(screen.getByText('Oct 16, 7:41 PM')).toBeInTheDocument();
2520
});
2621

@@ -30,73 +25,75 @@ describe('DateTime', () => {
3025
date.setMonth(11);
3126
date.setDate(31);
3227

33-
render(<DateTime date={date} />);
28+
renderPDT(<DateTime date={date} />);
3429
expect(screen.getByText('Dec 31, 2016 7:41 PM')).toBeInTheDocument();
3530
});
3631

3732
it('renders only the time', () => {
38-
render(<DateTime date={new Date()} timeOnly />);
33+
renderPDT(<DateTime date={new Date()} timeOnly />);
3934
expect(screen.getByText('7:41 PM')).toBeInTheDocument();
4035
});
4136

4237
it('renders only the date', () => {
43-
render(<DateTime date={new Date()} dateOnly />);
38+
renderPDT(<DateTime date={new Date()} dateOnly />);
4439
expect(screen.getByText('Oct 16')).toBeInTheDocument();
4540
});
4641

4742
it('renders a date with year', () => {
48-
render(<DateTime date={new Date()} year />);
43+
renderPDT(<DateTime date={new Date()} year />);
4944
expect(screen.getByText('Oct 16, 2017 7:41 PM')).toBeInTheDocument();
5045
});
5146

5247
it('renders a date with seconds', () => {
53-
render(<DateTime date={new Date()} seconds />);
48+
renderPDT(<DateTime date={new Date()} seconds />);
5449
expect(screen.getByText('Oct 16, 7:41:20 PM')).toBeInTheDocument();
5550
});
5651

5752
it('renders a date with the time zone', () => {
58-
render(<DateTime date={new Date()} timeZone />);
53+
renderPDT(<DateTime date={new Date()} timeZone />);
5954
expect(screen.getByText('Oct 16, 7:41 PM PDT')).toBeInTheDocument();
6055
});
6156

6257
it('renders date with forced utc', () => {
63-
render(<DateTime date={new Date()} utc />);
58+
renderPDT(<DateTime date={new Date()} utc />);
6459
expect(screen.getByText('Oct 17, 2:41 AM UTC')).toBeInTheDocument();
6560
});
6661

6762
it('renders date with forced timezone', () => {
68-
render(<DateTime date={new Date()} forcedTimezone="America/Toronto" />);
63+
renderPDT(<DateTime date={new Date()} forcedTimezone="America/Toronto" />);
6964
expect(screen.getByText('Oct 16, 10:41 PM')).toBeInTheDocument();
7065
});
7166

7267
describe('24 Hours', () => {
7368
beforeAll(() => {
69+
const user = UserFixture();
7470
user.options.clock24Hours = true;
7571
ConfigStore.set('user', user);
7672
});
7773

7874
afterAll(() => {
75+
const user = UserFixture();
7976
user.options.clock24Hours = false;
8077
ConfigStore.set('user', user);
8178
});
8279

8380
it('renders a date', () => {
84-
render(<DateTime date={new Date()} />);
81+
renderPDT(<DateTime date={new Date()} />);
8582
expect(screen.getByText('Oct 16, 19:41')).toBeInTheDocument();
8683
});
8784

8885
it('renders only the time', () => {
89-
render(<DateTime date={new Date()} timeOnly />);
86+
renderPDT(<DateTime date={new Date()} timeOnly />);
9087
expect(screen.getByText('19:41')).toBeInTheDocument();
9188
});
9289

9390
it('renders date with forced utc', () => {
94-
render(<DateTime date={new Date()} utc />);
91+
renderPDT(<DateTime date={new Date()} utc />);
9592
expect(screen.getByText('Oct 17, 02:41 UTC')).toBeInTheDocument();
9693
});
9794

9895
it('renders date with forced timezone', () => {
99-
render(<DateTime date={new Date()} forcedTimezone="America/Toronto" />);
96+
renderPDT(<DateTime date={new Date()} forcedTimezone="America/Toronto" />);
10097
expect(screen.getByText('Oct 16, 22:41')).toBeInTheDocument();
10198
});
10299
});

static/app/components/dateTime.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import moment from 'moment-timezone';
22

3+
import {useTimezone} from 'sentry/components/timezoneProvider';
34
import {getFormat} from 'sentry/utils/dates';
45
import {useUser} from 'sentry/utils/useUser';
56

@@ -60,7 +61,9 @@ export function DateTime({
6061
...props
6162
}: DateTimeProps) {
6263
const user = useUser();
63-
const options = user?.options;
64+
const currentTimezone = useTimezone();
65+
66+
const tz = forcedTimezone ?? currentTimezone;
6467

6568
const formatString =
6669
format ??
@@ -69,19 +72,19 @@ export function DateTime({
6972
timeOnly,
7073
// If the year prop is defined, then use it. Otherwise only show the year if `date`
7174
// is in the current year.
72-
year: year ?? moment().year() !== moment(date).year(),
75+
year: year ?? moment.tz(tz).year() !== moment.tz(date, tz).year(),
7376
// If timeZone is defined, use it. Otherwise only show the time zone if we're using
7477
// UTC time.
7578
timeZone: timeZone ?? utc,
7679
seconds,
77-
...options,
80+
clock24Hours: user?.options.clock24Hours,
7881
});
7982

8083
return (
8184
<time {...props}>
8285
{utc
8386
? moment.utc(date).format(formatString)
84-
: moment.tz(date, forcedTimezone ?? options?.timezone ?? '').format(formatString)}
87+
: moment.tz(date, tz).format(formatString)}
8588
</time>
8689
);
8790
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {Fragment} from 'react';
2+
import {UserFixture} from 'sentry-fixture/user';
3+
4+
import {act, render, screen} from 'sentry-test/reactTestingLibrary';
5+
6+
import ConfigStore from 'sentry/stores/configStore';
7+
8+
import {
9+
OverrideTimezoneProvider,
10+
UserTimezoneProvider,
11+
useTimezone,
12+
useTimezoneOverride,
13+
} from './timezoneProvider';
14+
15+
describe('timezoneProvider', function () {
16+
function setConfigStoreTimezone(tz: string) {
17+
const user = UserFixture();
18+
user.options.timezone = tz;
19+
act(() => ConfigStore.set('user', user));
20+
}
21+
22+
function ShowTimezone(props: React.ComponentProps<'div'>) {
23+
const timezone = useTimezone();
24+
return <div {...props}>{timezone}</div>;
25+
}
26+
27+
function ChangeUserTimezone({tz}: {tz: string}) {
28+
return <button onClick={() => setConfigStoreTimezone(tz)}>Change Timezone</button>;
29+
}
30+
31+
function OverrideTimezone({tz}: {tz: string}) {
32+
const {setOverride, clearOverride} = useTimezoneOverride();
33+
34+
return (
35+
<Fragment>
36+
<button onClick={() => setOverride(tz)}>Override Timezone</button>
37+
<button onClick={() => clearOverride()}>Reset Timezone</button>
38+
</Fragment>
39+
);
40+
}
41+
42+
beforeEach(() => setConfigStoreTimezone('America/New_York'));
43+
44+
describe('UserTimezoneProvider', function () {
45+
it('provides timezone for the user', function () {
46+
render(
47+
<UserTimezoneProvider>
48+
<ShowTimezone data-test-id="tz" />
49+
</UserTimezoneProvider>
50+
);
51+
52+
expect(screen.getByTestId('tz')).toHaveTextContent('America/New_York');
53+
});
54+
55+
it('updates when the user timezone changes', function () {
56+
render(
57+
<UserTimezoneProvider>
58+
<ShowTimezone data-test-id="tz" />
59+
<ChangeUserTimezone tz="America/Los_Angeles" />
60+
</UserTimezoneProvider>
61+
);
62+
63+
expect(screen.getByTestId('tz')).toHaveTextContent('America/New_York');
64+
65+
screen.getByRole('button', {name: 'Change Timezone'}).click();
66+
expect(screen.getByTestId('tz')).toHaveTextContent('America/Los_Angeles');
67+
});
68+
});
69+
70+
describe('OverrideTimezoneProvider', function () {
71+
it('can override timezones', function () {
72+
render(
73+
<UserTimezoneProvider>
74+
<ShowTimezone data-test-id="tz-outer" />
75+
<OverrideTimezoneProvider>
76+
<ShowTimezone data-test-id="tz-inner" />
77+
<OverrideTimezone tz="America/Los_Angeles" />
78+
</OverrideTimezoneProvider>
79+
</UserTimezoneProvider>
80+
);
81+
82+
expect(screen.getByTestId('tz-outer')).toHaveTextContent('America/New_York');
83+
84+
// Act because useOverrideTimezone is an effect
85+
act(() => screen.getByRole('button', {name: 'Override Timezone'}).click());
86+
expect(screen.getByTestId('tz-outer')).toHaveTextContent('America/New_York');
87+
expect(screen.getByTestId('tz-inner')).toHaveTextContent('America/Los_Angeles');
88+
89+
// Act because useOverrideTimezone is an effect
90+
act(() => screen.getByRole('button', {name: 'Reset Timezone'}).click());
91+
expect(screen.getByTestId('tz-outer')).toHaveTextContent('America/New_York');
92+
expect(screen.getByTestId('tz-inner')).toHaveTextContent('America/New_York');
93+
});
94+
});
95+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {createContext, useContext, useEffect, useMemo, useState} from 'react';
2+
3+
import {useUser} from 'sentry/utils/useUser';
4+
5+
interface TimezoneProviderValue {
6+
timezone: string;
7+
setOverride?: (timezone: string | null) => void;
8+
}
9+
10+
interface CommonProps {
11+
children: NonNullable<React.ReactNode>;
12+
}
13+
14+
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
15+
16+
const Provider = createContext<TimezoneProviderValue>({timezone: browserTimezone});
17+
18+
interface TimezoneProviderProps {
19+
children: NonNullable<React.ReactNode>;
20+
timezone: string;
21+
}
22+
23+
/**
24+
* Provide the specified timezone to components that useTimezone.
25+
*
26+
* See OverrideTimezoneProvider for a implementation of the timezone provider
27+
* that allows for overriding the timezone using hooks.
28+
*/
29+
export function TimezoneProvider({children, timezone}: TimezoneProviderProps) {
30+
const value = useMemo(() => ({timezone}), [timezone]);
31+
32+
return <Provider value={value}>{children}</Provider>;
33+
}
34+
35+
/**
36+
* Provides the user's configured timezone to components that use useTimezone.
37+
*/
38+
export function UserTimezoneProvider({children}: CommonProps) {
39+
const user = useUser();
40+
const timezone = user?.options.timezone ?? browserTimezone;
41+
42+
return <TimezoneProvider timezone={timezone}>{children}</TimezoneProvider>;
43+
}
44+
45+
/**
46+
* Allows components that use useTimezone (such as <DateTime />) that are
47+
* within this provider to be overridden using the useTimezoneOverride hook.
48+
*/
49+
export function OverrideTimezoneProvider({children}: CommonProps) {
50+
const parentTimezone = useTimezone();
51+
const [override, setOverride] = useState<string | null>(null);
52+
53+
const timezone = override ?? parentTimezone;
54+
const value = useMemo(() => ({timezone, setOverride}), [timezone]);
55+
56+
return <Provider value={value}>{children}</Provider>;
57+
}
58+
59+
/**
60+
* Get the currently configured timezone.
61+
*/
62+
export function useTimezone() {
63+
return useContext(Provider).timezone;
64+
}
65+
66+
/**
67+
* This hook may be used to override the result of useTimezone in the nearest
68+
* OverrideTimezoneProvider. The result is a pair of {setOverride,
69+
* clearOverride} functions.
70+
*
71+
* It is the callers responsibility to call clearOverride to restore the
72+
* timezone in the OverrideTimezoneProvider.
73+
*/
74+
export function useTimezoneOverride() {
75+
const {setOverride} = useContext(Provider);
76+
77+
if (setOverride === undefined) {
78+
throw new Error('useTimezoneOverride requires a OverrideTimezoneProvider');
79+
}
80+
81+
const result = useMemo(
82+
() => ({
83+
setOverride: (timezone: string) => setOverride(timezone),
84+
clearOverride: () => setOverride(null),
85+
}),
86+
[setOverride]
87+
);
88+
89+
return result;
90+
}

static/app/utils/profiling/hooks/useRelativeDateTime.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {useMemo} from 'react';
22

3+
import {useTimezone} from 'sentry/components/timezoneProvider';
34
import type {PageFilters} from 'sentry/types/core';
4-
import {getUserTimezone} from 'sentry/utils/dates';
55

66
const DAY = 24 * 60 * 60 * 1000;
77

@@ -16,6 +16,8 @@ export function useRelativeDateTime({
1616
relativeDays,
1717
retentionDays,
1818
}: UseRelativeDateTimeOptions): PageFilters['datetime'] {
19+
const timezone = useTimezone();
20+
1921
const anchorTime = anchor * 1000;
2022

2123
// Make sure to memo this. Otherwise, each re-render will have
@@ -34,7 +36,7 @@ export function useRelativeDateTime({
3436
return {
3537
start: beforeDateTime,
3638
end: afterDateTime,
37-
utc: getUserTimezone() === 'UTC',
39+
utc: timezone.includes('UTC'),
3840
period: null,
3941
};
4042
}

0 commit comments

Comments
 (0)