From bcc3e94902bd737ffb64612006fb9551f468d4ad Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Thu, 13 Nov 2025 17:16:00 -0500 Subject: [PATCH] ref(crons): Disable mute button when monitor has no environments [NEW-564: There needs to be some way to mute the entire cron detector](https://linear.app/getsentry/issue/NEW-564/there-needs-to-be-some-way-to-mute-the-entire-cron-detector) Since muting now operates at the environment level, disallow muting a monitor when it has no environments (as it would do nothing). Shows a helpful tooltip explaining that muting is only available when there are monitor environments. --- .../components/monitorHeaderActions.spec.tsx | 82 +++++++++++++++++++ .../crons/components/monitorHeaderActions.tsx | 12 ++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 static/app/views/insights/crons/components/monitorHeaderActions.spec.tsx diff --git a/static/app/views/insights/crons/components/monitorHeaderActions.spec.tsx b/static/app/views/insights/crons/components/monitorHeaderActions.spec.tsx new file mode 100644 index 00000000000000..cf03b155a47953 --- /dev/null +++ b/static/app/views/insights/crons/components/monitorHeaderActions.spec.tsx @@ -0,0 +1,82 @@ +import {MonitorFixture} from 'sentry-fixture/monitor'; +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import MonitorHeaderActions from 'sentry/views/insights/crons/components/monitorHeaderActions'; + +describe('MonitorHeaderActions', () => { + const organization = OrganizationFixture(); + + it('disables mute button when monitor has no environments', async () => { + const monitor = MonitorFixture({ + environments: [], + }); + + render( + , + {organization} + ); + + const muteButton = screen.getByRole('button', {name: 'Mute'}); + expect(muteButton).toBeDisabled(); + + await userEvent.hover(muteButton); + expect( + await screen.findByText( + 'Muting is only available when there are monitor environments' + ) + ).toBeInTheDocument(); + }); + + it('enables mute button when monitor has environments', async () => { + const monitor = MonitorFixture(); + + const updateMock = MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${monitor.project.slug}/monitors/${monitor.slug}/`, + method: 'PUT', + body: {...monitor, isMuted: true}, + }); + + render( + , + {organization} + ); + + const muteButton = screen.getByRole('button', {name: 'Mute'}); + expect(muteButton).toBeEnabled(); + + await userEvent.click(muteButton); + expect(updateMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + data: {isMuted: true}, + }) + ); + }); + + it('shows unmute button when monitor is muted', () => { + const monitor = MonitorFixture({ + isMuted: true, + }); + + render( + , + {organization} + ); + + expect(screen.getByRole('button', {name: 'Unmute'})).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/insights/crons/components/monitorHeaderActions.tsx b/static/app/views/insights/crons/components/monitorHeaderActions.tsx index a8e04a3639fc2f..e870df97278563 100644 --- a/static/app/views/insights/crons/components/monitorHeaderActions.tsx +++ b/static/app/views/insights/crons/components/monitorHeaderActions.tsx @@ -71,6 +71,16 @@ function MonitorHeaderActions({monitor, orgSlug, onUpdate}: Props) { disableProps.title = permissionTooltipText; } + const hasEnvironments = monitor.environments.length > 0; + const muteDisableProps = {...disableProps}; + + if (!hasEnvironments) { + muteDisableProps.disabled = true; + muteDisableProps.title = t( + 'Muting is only available when there are monitor environments' + ); + } + return ( @@ -78,7 +88,7 @@ function MonitorHeaderActions({monitor, orgSlug, onUpdate}: Props) { size="sm" icon={monitor.isMuted ? : } onClick={() => handleUpdate({isMuted: !monitor.isMuted})} - {...disableProps} + {...muteDisableProps} > {monitor.isMuted ? t('Unmute') : t('Mute')}