diff --git a/static/app/actionCreators/uptime.tsx b/static/app/actionCreators/uptime.tsx new file mode 100644 index 00000000000000..bf61ff38bc1840 --- /dev/null +++ b/static/app/actionCreators/uptime.tsx @@ -0,0 +1,44 @@ +import * as Sentry from '@sentry/react'; + +import { + addErrorMessage, + addLoadingMessage, + clearIndicators, +} from 'sentry/actionCreators/indicator'; +import type {Client} from 'sentry/api'; +import {t} from 'sentry/locale'; +import type RequestError from 'sentry/utils/requestError/requestError'; +import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types'; + +export async function updateUptimeRule( + api: Client, + orgId: string, + uptimeMonitor: UptimeRule, + data: Partial +): Promise { + addLoadingMessage(); + + try { + const resp = await api.requestPromise( + `/projects/${orgId}/${uptimeMonitor.projectSlug}/uptime/${uptimeMonitor.id}/`, + {method: 'PUT', data} + ); + clearIndicators(); + return resp; + } catch (err) { + const respError: RequestError = err; + const updateKeys = Object.keys(data); + + // If we are updating a single value in the monitor we can read the + // validation error for that key, otherwise fallback to the default error + const validationError = + updateKeys.length === 1 + ? (respError.responseJSON?.[updateKeys[0]!] as any)?.[0] + : undefined; + + Sentry.captureException(err); + addErrorMessage(validationError ?? t('Unable to update uptime monitor.')); + } + + return null; +} diff --git a/static/app/views/alerts/list/rules/combinedAlertBadge.tsx b/static/app/views/alerts/list/rules/combinedAlertBadge.tsx index e14ba061bf8d5a..ee515b71d53ad9 100644 --- a/static/app/views/alerts/list/rules/combinedAlertBadge.tsx +++ b/static/app/views/alerts/list/rules/combinedAlertBadge.tsx @@ -61,7 +61,7 @@ export default function CombinedAlertBadge({rule}: Props) { const {statusText, incidentStatus} = UptimeStatusText[rule.uptimeStatus]; return ( - + ); } diff --git a/static/app/views/alerts/rules/uptime/details.spec.tsx b/static/app/views/alerts/rules/uptime/details.spec.tsx index e2e9775f5d90c2..f566dde55b3f38 100644 --- a/static/app/views/alerts/rules/uptime/details.spec.tsx +++ b/static/app/views/alerts/rules/uptime/details.spec.tsx @@ -1,7 +1,7 @@ import {UptimeRuleFixture} from 'sentry-fixture/uptimeRule'; import {initializeOrg} from 'sentry-test/initializeOrg'; -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import UptimeAlertDetails from './details'; @@ -56,4 +56,57 @@ describe('UptimeAlertDetails', function () { await screen.findByText('The uptime alert rule you were looking for was not found.') ).toBeInTheDocument(); }); + + it('disables and enables the rule', async function () { + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/uptime/2/`, + statusCode: 404, + }); + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + body: UptimeRuleFixture({name: 'Uptime Test Rule'}), + }); + + render( + , + {organization} + ); + await screen.findByText('Uptime Test Rule'); + + const disableMock = MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + method: 'PUT', + body: UptimeRuleFixture({name: 'Uptime Test Rule', status: 'disabled'}), + }); + + await userEvent.click( + await screen.findByRole('button', { + name: 'Disable this uptime rule and stop performing checks', + }) + ); + + expect(disableMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({data: {status: 'disabled'}}) + ); + + const enableMock = MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + method: 'PUT', + body: UptimeRuleFixture({name: 'Uptime Test Rule', status: 'active'}), + }); + + // Button now re-enables the monitor + await userEvent.click( + await screen.findByRole('button', {name: 'Enable this uptime rule'}) + ); + + expect(enableMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({data: {status: 'active'}}) + ); + }); }); diff --git a/static/app/views/alerts/rules/uptime/details.tsx b/static/app/views/alerts/rules/uptime/details.tsx index 9a72cb71fb1429..f1458f5fb441c0 100644 --- a/static/app/views/alerts/rules/uptime/details.tsx +++ b/static/app/views/alerts/rules/uptime/details.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; +import {updateUptimeRule} from 'sentry/actionCreators/uptime'; import ActorAvatar from 'sentry/components/avatar/actorAvatar'; import Breadcrumbs from 'sentry/components/breadcrumbs'; import {LinkButton} from 'sentry/components/button'; @@ -20,18 +21,28 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import getDuration from 'sentry/utils/duration/getDuration'; -import {type ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient'; +import { + type ApiQueryKey, + setApiQueryData, + useApiQuery, + useQueryClient, +} from 'sentry/utils/queryClient'; +import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types'; +import {StatusToggleButton} from './statusToggleButton'; import {UptimeIssues} from './uptimeIssues'; interface UptimeAlertDetailsProps extends RouteComponentProps<{projectId: string; uptimeRuleId: string}, {}> {} export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { + const api = useApi(); const organization = useOrganization(); + const queryClient = useQueryClient(); + const {projectId, uptimeRuleId} = params; const {projects, fetching: loadingProject} = useProjects({slugs: [projectId]}); @@ -69,6 +80,14 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { ); } + const handleUpdate = async (data: Partial) => { + const resp = await updateUptimeRule(api, organization.slug, uptimeRule, data); + + if (resp !== null) { + setApiQueryData(queryClient, queryKey, resp); + } + }; + return ( @@ -97,6 +116,11 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { + handleUpdate({status})} + size="sm" + /> } diff --git a/static/app/views/alerts/rules/uptime/statusToggleButton.tsx b/static/app/views/alerts/rules/uptime/statusToggleButton.tsx new file mode 100644 index 00000000000000..40792fce7ac09a --- /dev/null +++ b/static/app/views/alerts/rules/uptime/statusToggleButton.tsx @@ -0,0 +1,42 @@ +import type {BaseButtonProps} from 'sentry/components/button'; +import {Button} from 'sentry/components/button'; +import {IconPause, IconPlay} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import type {ObjectStatus} from 'sentry/types/core'; + +import type {UptimeRule} from './types'; + +interface StatusToggleButtonProps extends Omit { + onToggleStatus: (status: ObjectStatus) => Promise; + uptimeRule: UptimeRule; +} + +export function StatusToggleButton({ + uptimeRule, + onToggleStatus, + ...props +}: StatusToggleButtonProps) { + const {status} = uptimeRule; + const isDisabled = status === 'disabled'; + + const Icon = isDisabled ? IconPlay : IconPause; + + const label = isDisabled + ? t('Enable this uptime rule') + : t('Disable this uptime rule and stop performing checks'); + + return ( +