Skip to content

Commit a352a67

Browse files
committed
feat(js): Add TimezoneProvider
This implements a new top-level provider that provides the timezone to components like <DateTime />. This provider exposes two hooks - `useTimezone` - Get the currently set timezone by the user - `useOverrideTimezone` - Overrides the configured timezone to a different timezone. The effect will only persist while the component that rendered the hook is mounted. Only affects the closest TimezoneProvider. This can be useful in scenarios where it may be useful to provide the user the option to change the timezone of a specific part of a view. For example, when looking at a Cron Monitor the user may want to view the times of check-ins in their timezone, the monitors timezone, or the UTC timezone.
1 parent eedde2f commit a352a67

File tree

9 files changed

+258
-53
lines changed

9 files changed

+258
-53
lines changed

static/app/components/dateTime.spec.tsx

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,99 @@
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

27-
it('renders a date and shows the year if it is outside the current year', () => {
22+
it.only('renders a date and shows the year if it is outside the current year', () => {
2823
const date = new Date();
2924
date.setFullYear(2016);
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
}

static/app/components/timeRangeSelector/index.spec.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
55
import {fireEvent, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
66

77
import {TimeRangeSelector} from 'sentry/components/timeRangeSelector';
8+
import {TimezoneProvider} from 'sentry/components/timezoneProvider';
89
import ConfigStore from 'sentry/stores/configStore';
910

1011
const {organization} = initializeOrg({
@@ -27,7 +28,11 @@ describe('TimeRangeSelector', function () {
2728
const onChange = jest.fn();
2829

2930
function getComponent(props = {}) {
30-
return <TimeRangeSelector showAbsolute showRelative onChange={onChange} {...props} />;
31+
return (
32+
<TimezoneProvider timezone="America/Los_Angeles">
33+
<TimeRangeSelector showAbsolute showRelative onChange={onChange} {...props} />
34+
</TimezoneProvider>
35+
);
3136
}
3237

3338
function renderComponent(props = {}) {

static/app/components/timeRangeSelector/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {Item} from 'sentry/components/dropdownAutoComplete/types';
88
import DropdownButton from 'sentry/components/dropdownButton';
99
import HookOrDefault from 'sentry/components/hookOrDefault';
1010
import {DesyncedFilterIndicator} from 'sentry/components/organizations/pageFilters/desyncedFilter';
11+
import {useTimezone} from 'sentry/components/timezoneProvider';
1112
import {DEFAULT_RELATIVE_PERIODS, DEFAULT_STATS_PERIOD} from 'sentry/constants';
1213
import {IconArrow} from 'sentry/icons';
1314
import {t} from 'sentry/locale';
@@ -19,7 +20,6 @@ import {
1920
getInternalDate,
2021
getLocalToSystem,
2122
getPeriodAgo,
22-
getUserTimezone,
2323
getUtcToSystem,
2424
} from 'sentry/utils/dates';
2525
import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
@@ -169,14 +169,15 @@ export function TimeRangeSelector({
169169
}: TimeRangeSelectorProps) {
170170
const router = useRouter();
171171
const organization = useOrganization({allowNull: true});
172+
const timezone = useTimezone();
172173

173174
const [search, setSearch] = useState('');
174175
const [hasChanges, setHasChanges] = useState(false);
175176
const [hasDateRangeErrors, setHasDateRangeErrors] = useState(false);
176177
const [showAbsoluteSelector, setShowAbsoluteSelector] = useState(!showRelative);
177178

178179
const [internalValue, setInternalValue] = useState<ChangeData>(() => {
179-
const internalUtc = utc ?? getUserTimezone() === 'UTC';
180+
const internalUtc = utc ?? timezone.includes('UTC');
180181

181182
return {
182183
start: start ? getInternalDate(start, internalUtc) : undefined,
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+
});

0 commit comments

Comments
 (0)