Skip to content

Commit b32d9e2

Browse files
feat(crons): Add timezone override picker to details (#91843)
It's often useful to view cron details in either your timezone, the timezone of the monitor itself, or just in UTC. This also drops the tooltips on the check-ins, since you can now change the timezone completely. <img alt="clipboard.png" width="1392" src="https://i.imgur.com/gXPc9Qp.png" />
1 parent ea84093 commit b32d9e2

File tree

3 files changed

+154
-80
lines changed

3 files changed

+154
-80
lines changed

static/app/views/alerts/rules/crons/details.tsx

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
1414
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
1515
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
1616
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
17+
import {TimezoneProvider, useTimezone} from 'sentry/components/timezoneProvider';
1718
import {t} from 'sentry/locale';
1819
import {space} from 'sentry/styles/space';
1920
import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
@@ -30,6 +31,7 @@ import {MonitorOnboarding} from 'sentry/views/insights/crons/components/onboardi
3031
import {MonitorProcessingErrors} from 'sentry/views/insights/crons/components/processingErrors/monitorProcessingErrors';
3132
import {makeMonitorErrorsQueryKey} from 'sentry/views/insights/crons/components/processingErrors/utils';
3233
import {StatusToggleButton} from 'sentry/views/insights/crons/components/statusToggleButton';
34+
import {TimezoneOverride} from 'sentry/views/insights/crons/components/timezoneOverride';
3335
import type {
3436
CheckinProcessingError,
3537
Monitor,
@@ -114,6 +116,9 @@ function MonitorDetails({params, location}: Props) {
114116
refetchErrors();
115117
}
116118

119+
const userTimezone = useTimezone();
120+
const [timezoneOverride, setTimezoneOverride] = useState(userTimezone);
121+
117122
// Only display the unknown legend when there are visible unknown check-ins
118123
// in the timeline
119124
const [showUnknownLegend, setShowUnknownLegend] = useState(false);
@@ -146,61 +151,77 @@ function MonitorDetails({params, location}: Props) {
146151
<SentryDocumentTitle title={`${monitor.name} — Alerts`} />
147152
<MonitorHeader monitor={monitor} orgSlug={organization.slug} onUpdate={onUpdate} />
148153
<Layout.Body>
149-
<Layout.Main>
150-
<StyledPageFilterBar condensed>
151-
<DatePageFilter maxPickableDays={30} />
152-
<EnvironmentPageFilter />
153-
</StyledPageFilterBar>
154-
{monitor.status === 'disabled' && (
155-
<Alert.Container>
156-
<Alert
157-
type="muted"
158-
showIcon
159-
trailingItems={
160-
<StatusToggleButton
161-
monitor={monitor}
162-
size="xs"
163-
onToggleStatus={status => handleUpdate({status})}
164-
>
165-
{t('Enable')}
166-
</StatusToggleButton>
167-
}
154+
<TimezoneProvider timezone={timezoneOverride}>
155+
<Layout.Main>
156+
<MainActions>
157+
<StyledPageFilterBar condensed>
158+
<DatePageFilter maxPickableDays={30} />
159+
<EnvironmentPageFilter />
160+
</StyledPageFilterBar>
161+
<TimezoneOverride
162+
monitor={monitor}
163+
userTimezone={userTimezone}
164+
onTimezoneSelected={setTimezoneOverride}
165+
/>
166+
</MainActions>
167+
{monitor.status === 'disabled' && (
168+
<Alert.Container>
169+
<Alert
170+
type="muted"
171+
showIcon
172+
trailingItems={
173+
<StatusToggleButton
174+
monitor={monitor}
175+
size="xs"
176+
onToggleStatus={status => handleUpdate({status})}
177+
>
178+
{t('Enable')}
179+
</StatusToggleButton>
180+
}
181+
>
182+
{t('This monitor is disabled and is not accepting check-ins.')}
183+
</Alert>
184+
</Alert.Container>
185+
)}
186+
{!!checkinErrors?.length && (
187+
<MonitorProcessingErrors
188+
checkinErrors={checkinErrors}
189+
onDismiss={handleDismissError}
168190
>
169-
{t('This monitor is disabled and is not accepting check-ins.')}
170-
</Alert>
171-
</Alert.Container>
172-
)}
173-
{!!checkinErrors?.length && (
174-
<MonitorProcessingErrors
175-
checkinErrors={checkinErrors}
176-
onDismiss={handleDismissError}
177-
>
178-
{t('Errors were encountered while ingesting check-ins for this monitor')}
179-
</MonitorProcessingErrors>
180-
)}
181-
{hasLastCheckIn(monitor) ? (
182-
<Fragment>
183-
<DetailsTimeline monitor={monitor} onStatsLoaded={checkHasUnknown} />
184-
<MonitorStats monitor={monitor} monitorEnvs={monitor.environments} />
185-
<MonitorIssues monitor={monitor} monitorEnvs={monitor.environments} />
186-
<MonitorCheckIns monitor={monitor} monitorEnvs={monitor.environments} />
187-
</Fragment>
188-
) : (
189-
<MonitorOnboarding monitor={monitor} />
190-
)}
191-
</Layout.Main>
192-
<Layout.Side>
193-
<DetailsSidebar
194-
monitorEnv={envsSortedByLastCheck[envsSortedByLastCheck.length - 1]}
195-
monitor={monitor}
196-
showUnknownLegend={showUnknownLegend}
197-
/>
198-
</Layout.Side>
191+
{t('Errors were encountered while ingesting check-ins for this monitor')}
192+
</MonitorProcessingErrors>
193+
)}
194+
{hasLastCheckIn(monitor) ? (
195+
<Fragment>
196+
<DetailsTimeline monitor={monitor} onStatsLoaded={checkHasUnknown} />
197+
<MonitorStats monitor={monitor} monitorEnvs={monitor.environments} />
198+
<MonitorIssues monitor={monitor} monitorEnvs={monitor.environments} />
199+
<MonitorCheckIns monitor={monitor} monitorEnvs={monitor.environments} />
200+
</Fragment>
201+
) : (
202+
<MonitorOnboarding monitor={monitor} />
203+
)}
204+
</Layout.Main>
205+
<Layout.Side>
206+
<DetailsSidebar
207+
monitorEnv={envsSortedByLastCheck[envsSortedByLastCheck.length - 1]}
208+
monitor={monitor}
209+
showUnknownLegend={showUnknownLegend}
210+
/>
211+
</Layout.Side>
212+
</TimezoneProvider>
199213
</Layout.Body>
200214
</Layout.Page>
201215
);
202216
}
203217

218+
const MainActions = styled('div')`
219+
display: flex;
220+
gap: ${space(1)};
221+
justify-content: space-between;
222+
align-items: center;
223+
`;
224+
204225
const StyledPageFilterBar = styled(PageFilterBar)`
205226
margin-bottom: ${space(2)};
206227
`;

static/app/views/insights/crons/components/monitorCheckIns.tsx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {space} from 'sentry/styles/space';
2323
import {defined} from 'sentry/utils';
2424
import {useLocation} from 'sentry/utils/useLocation';
2525
import useOrganization from 'sentry/utils/useOrganization';
26-
import {useUser} from 'sentry/utils/useUser';
2726
import {QuickContextHovercard} from 'sentry/views/discover/table/quickContext/quickContextHovercard';
2827
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
2928
import type {Monitor, MonitorEnvironment} from 'sentry/views/insights/crons/types';
@@ -53,7 +52,6 @@ const checkStatusToIndicatorStatus: Record<
5352
const PER_PAGE = 10;
5453

5554
export function MonitorCheckIns({monitor, monitorEnvs}: Props) {
56-
const user = useUser();
5755
const location = useLocation();
5856
const organization = useOrganization();
5957

@@ -95,9 +93,6 @@ export function MonitorCheckIns({monitor, monitorEnvs}: Props) {
9593
t('Expected At'),
9694
];
9795

98-
const customTimezone =
99-
monitor.config.timezone && monitor.config.timezone !== user.options.timezone;
100-
10196
return (
10297
<Fragment>
10398
<SectionHeading>{t('Recent Check-Ins')}</SectionHeading>
@@ -127,19 +122,7 @@ export function MonitorCheckIns({monitor, monitorEnvs}: Props) {
127122
emptyCell
128123
) : (
129124
<div>
130-
<Tooltip
131-
disabled={!customTimezone}
132-
title={
133-
<DateTime
134-
date={checkIn.dateAdded}
135-
forcedTimezone={monitor.config.timezone ?? 'UTC'}
136-
timeZone
137-
seconds
138-
/>
139-
}
140-
>
141-
<DateTime date={checkIn.dateAdded} timeZone seconds />
142-
</Tooltip>
125+
<DateTime date={checkIn.dateAdded} timeZone seconds />
143126
</div>
144127
)}
145128
{defined(checkIn.duration) ? (
@@ -196,19 +179,7 @@ export function MonitorCheckIns({monitor, monitorEnvs}: Props) {
196179
{hasMultiEnv ? <div>{checkIn.environment}</div> : null}
197180
<div>
198181
{checkIn.expectedTime ? (
199-
<Tooltip
200-
disabled={!customTimezone}
201-
title={
202-
<DateTime
203-
date={checkIn.expectedTime}
204-
forcedTimezone={monitor.config.timezone ?? 'UTC'}
205-
timeZone
206-
seconds
207-
/>
208-
}
209-
>
210-
<Timestamp date={checkIn.expectedTime} timeZone seconds />
211-
</Tooltip>
182+
<Timestamp date={checkIn.expectedTime} timeZone seconds />
212183
) : (
213184
emptyCell
214185
)}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {useCallback, useMemo, useState} from 'react';
2+
import styled from '@emotion/styled';
3+
import moment from 'moment-timezone';
4+
5+
import {CompactSelect} from 'sentry/components/core/compactSelect';
6+
import {t} from 'sentry/locale';
7+
import type {Monitor} from 'sentry/views/insights/crons/types';
8+
9+
interface TimezoneOverrideProps {
10+
monitor: Monitor;
11+
onTimezoneSelected: (timezone: string) => void;
12+
userTimezone: string;
13+
}
14+
15+
type Mode = 'user' | 'monitor' | 'utc';
16+
17+
export function TimezoneOverride({
18+
monitor,
19+
onTimezoneSelected,
20+
userTimezone,
21+
}: TimezoneOverrideProps) {
22+
const monitorTimezone = monitor.config.timezone ?? 'UTC';
23+
24+
const [mode, setMode] = useState<Mode>('user');
25+
26+
const timezoneMapping = useMemo<Record<Mode, string>>(
27+
() => ({
28+
user: userTimezone,
29+
monitor: monitorTimezone,
30+
utc: 'UTC',
31+
}),
32+
[monitorTimezone, userTimezone]
33+
);
34+
35+
const handleChange = useCallback(
36+
(newMode: Mode) => {
37+
setMode(newMode);
38+
onTimezoneSelected(timezoneMapping[newMode]);
39+
},
40+
[onTimezoneSelected, timezoneMapping]
41+
);
42+
43+
return (
44+
<CompactSelect<Mode>
45+
size="xs"
46+
value={mode}
47+
position="bottom-end"
48+
onChange={option => handleChange(option.value)}
49+
triggerProps={{prefix: t('Date Display')}}
50+
options={[
51+
{
52+
value: 'user',
53+
label: 'My Timezone',
54+
trailingItems: <TimezoneLabel timezone={userTimezone} />,
55+
},
56+
{
57+
value: 'monitor',
58+
label: 'Monitor',
59+
trailingItems: <TimezoneLabel timezone={monitorTimezone} />,
60+
},
61+
{
62+
value: 'utc',
63+
label: 'UTC',
64+
trailingItems: <TimezoneLabel timezone="UTC" />,
65+
},
66+
]}
67+
/>
68+
);
69+
}
70+
71+
function TimezoneLabel({timezone}: {timezone: string}) {
72+
return <TimezoneName>{moment.tz(timezone).format('z Z')}</TimezoneName>;
73+
}
74+
75+
const TimezoneName = styled('div')`
76+
color: ${p => p.theme.subText};
77+
font-weight: ${p => p.theme.fontWeightBold};
78+
display: flex;
79+
align-items: center;
80+
font-size: ${p => p.theme.fontSizeSmall};
81+
width: max-content;
82+
`;

0 commit comments

Comments
 (0)