Skip to content

Commit 723fddb

Browse files
feat(detectors): Add disabled alert to uptime and cron detector details (#103165)
Adds a reusable DisabledAlert component that displays a muted alert when a detector is disabled, with a one-click enable button. The alert automatically hides when the detector becomes enabled. Uptime detectors show: "This monitor is disabled and not recording uptime checks." Cron detectors show: "This monitor is disabled and not accepting check-ins." Fixes [NEW-565: Disabled monitors should show a indicator that disabling means not accepting check-ins](https://linear.app/getsentry/issue/NEW-565/disabled-monitors-should-show-a-indicator-that-disabling-means-not) Fixes [NEW-616: Indicate that the monitor is disabled](https://linear.app/getsentry/issue/NEW-616/indicate-that-the-monitor-is-disabled)
1 parent 753774b commit 723fddb

File tree

6 files changed

+150
-0
lines changed

6 files changed

+150
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {UptimeDetectorFixture} from 'sentry-fixture/detectors';
2+
3+
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
4+
5+
import {DisabledAlert} from './disabledAlert';
6+
7+
describe('DisabledAlert', () => {
8+
it('does not render when detector is enabled', () => {
9+
const detector = UptimeDetectorFixture({enabled: true});
10+
11+
const {container} = render(
12+
<DisabledAlert detector={detector} message="Test disabled message" />
13+
);
14+
15+
expect(container).toBeEmptyDOMElement();
16+
});
17+
18+
it('renders alert with message and enable button when detector is disabled', () => {
19+
const detector = UptimeDetectorFixture({enabled: false});
20+
21+
render(<DisabledAlert detector={detector} message="Test disabled message" />);
22+
23+
expect(screen.getByText('Test disabled message')).toBeInTheDocument();
24+
expect(screen.getByRole('button', {name: 'Enable'})).toBeInTheDocument();
25+
});
26+
27+
it('enables detector when enable button is clicked', async () => {
28+
const detector = UptimeDetectorFixture({id: '123', enabled: false});
29+
30+
const updateRequest = MockApiClient.addMockResponse({
31+
url: '/organizations/org-slug/detectors/123/',
32+
method: 'PUT',
33+
body: {...detector, enabled: true},
34+
});
35+
36+
render(<DisabledAlert detector={detector} message="Test message" />);
37+
38+
const enableButton = screen.getByRole('button', {name: 'Enable'});
39+
await userEvent.click(enableButton);
40+
41+
await waitFor(() => {
42+
expect(updateRequest).toHaveBeenCalledWith(
43+
expect.anything(),
44+
expect.objectContaining({
45+
method: 'PUT',
46+
data: {detectorId: '123', enabled: true},
47+
})
48+
);
49+
});
50+
});
51+
52+
it('button is clickable when detector is disabled', () => {
53+
const detector = UptimeDetectorFixture({id: '123', enabled: false});
54+
55+
render(<DisabledAlert detector={detector} message="Test message" />);
56+
57+
const enableButton = screen.getByRole('button', {name: 'Enable'});
58+
expect(enableButton).toBeEnabled();
59+
});
60+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {Alert} from 'sentry/components/core/alert';
2+
import {Button} from 'sentry/components/core/button';
3+
import {IconPlay} from 'sentry/icons';
4+
import {t} from 'sentry/locale';
5+
import type {Detector} from 'sentry/types/workflowEngine/detectors';
6+
import {useUpdateDetector} from 'sentry/views/detectors/hooks';
7+
8+
type DisabledAlertProps = {
9+
detector: Detector;
10+
message: string;
11+
};
12+
13+
/**
14+
* Use this component on detector detail pages when you want users to quickly understand
15+
* that a detector is disabled and not actively monitoring, and give them a one-click way
16+
* to enable it. The alert automatically hides when the detector is enabled.
17+
*/
18+
export function DisabledAlert({detector, message}: DisabledAlertProps) {
19+
const {mutate: updateDetector, isPending: isEnabling} = useUpdateDetector();
20+
21+
if (detector.enabled) {
22+
return null;
23+
}
24+
25+
const handleEnable = () => {
26+
updateDetector({detectorId: detector.id, enabled: true});
27+
};
28+
29+
return (
30+
<Alert.Container>
31+
<Alert
32+
type="muted"
33+
trailingItems={
34+
<Button
35+
size="xs"
36+
icon={<IconPlay />}
37+
onClick={handleEnable}
38+
disabled={isEnabling}
39+
aria-label={t('Enable')}
40+
>
41+
{t('Enable')}
42+
</Button>
43+
}
44+
>
45+
{message}
46+
</Alert>
47+
</Alert.Container>
48+
);
49+
}

static/app/views/detectors/components/details/cron/index.spec.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,22 @@ describe('CronDetectorDetails - check-ins', () => {
324324
expect(screen.queryByText(expectedText)).not.toBeInTheDocument();
325325
});
326326
});
327+
328+
describe('disabled alert', () => {
329+
it('displays disabled alert with enable button when detector is disabled', async () => {
330+
const disabledDetector = CronDetectorFixture({
331+
id: '1',
332+
projectId: project.id,
333+
enabled: false,
334+
dataSources: [cronDataSource],
335+
});
336+
337+
render(<CronDetectorDetails detector={disabledDetector} project={project} />);
338+
339+
expect(
340+
await screen.findByText('This monitor is disabled and not accepting check-ins.')
341+
).toBeInTheDocument();
342+
expect(screen.getByRole('button', {name: 'Enable'})).toBeInTheDocument();
343+
});
344+
});
327345
});

static/app/views/detectors/components/details/cron/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
3232
import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations';
3333
import {DetectorDetailsDescription} from 'sentry/views/detectors/components/details/common/description';
34+
import {DisabledAlert} from 'sentry/views/detectors/components/details/common/disabledAlert';
3435
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
3536
import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
3637
import {DetectorDetailsOpenPeriodIssues} from 'sentry/views/detectors/components/details/common/openPeriodIssues';
@@ -156,6 +157,10 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp
156157
onTimezoneSelected={setTimezoneOverride}
157158
/>
158159
</Flex>
160+
<DisabledAlert
161+
detector={detector}
162+
message={t('This monitor is disabled and not accepting check-ins.')}
163+
/>
159164
{!!checkinErrors?.length && (
160165
<MonitorProcessingErrors
161166
checkinErrors={checkinErrors}

static/app/views/detectors/components/details/uptime/index.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,17 @@ describe('UptimeDetectorDetails', () => {
112112

113113
expect(await screen.findByText('95%')).toBeInTheDocument();
114114
});
115+
116+
it('displays disabled alert with enable button when detector is disabled', async () => {
117+
const detector = UptimeDetectorFixture({id: '3', enabled: false});
118+
119+
render(<UptimeDetectorDetails detector={detector} project={project} />, {
120+
organization,
121+
});
122+
123+
expect(
124+
await screen.findByText('This monitor is disabled and not recording uptime checks.')
125+
).toBeInTheDocument();
126+
expect(screen.getByRole('button', {name: 'Enable'})).toBeInTheDocument();
127+
});
115128
});

static/app/views/detectors/components/details/uptime/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {UptimeChecksTable} from 'sentry/views/alerts/rules/uptime/uptimeChecksTa
2222
import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
2323
import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations';
2424
import {DetectorDetailsDescription} from 'sentry/views/detectors/components/details/common/description';
25+
import {DisabledAlert} from 'sentry/views/detectors/components/details/common/disabledAlert';
2526
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
2627
import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
2728
import {UptimeDuration} from 'sentry/views/insights/uptime/components/duration';
@@ -59,6 +60,10 @@ export function UptimeDetectorDetails({detector, project}: UptimeDetectorDetails
5960
<DetailLayout.Body>
6061
<DetailLayout.Main>
6162
<DatePageFilter />
63+
<DisabledAlert
64+
detector={detector}
65+
message={t('This monitor is disabled and not recording uptime checks.')}
66+
/>
6267
<DetailsTimeline uptimeDetector={detector} onStatsLoaded={checkHasUnknown} />
6368
<Section title={t('Recent Check-Ins')}>
6469
<div>

0 commit comments

Comments
 (0)