Skip to content

Commit

Permalink
feat(uptime): Support enable / disable on the frontend (#84025)
Browse files Browse the repository at this point in the history
Allows monitors to be enabled / disabled from the details page
  • Loading branch information
evanpurkhiser authored Jan 24, 2025
1 parent 8fe530e commit e881b51
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 4 deletions.
44 changes: 44 additions & 0 deletions static/app/actionCreators/uptime.tsx
Original file line number Diff line number Diff line change
@@ -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<UptimeRule>
): Promise<UptimeRule | null> {
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;
}
2 changes: 1 addition & 1 deletion static/app/views/alerts/list/rules/combinedAlertBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function CombinedAlertBadge({rule}: Props) {
const {statusText, incidentStatus} = UptimeStatusText[rule.uptimeStatus];
return (
<Tooltip title={tct('Uptime Alert Status: [statusText]', {statusText})}>
<AlertBadge status={incidentStatus} />
<AlertBadge status={incidentStatus} isDisabled={rule.status === 'disabled'} />
</Tooltip>
);
}
Expand Down
55 changes: 54 additions & 1 deletion static/app/views/alerts/rules/uptime/details.spec.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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(
<UptimeAlertDetails
{...routerProps}
params={{...routerProps.params, uptimeRuleId: '1'}}
/>,
{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'}})
);
});
});
26 changes: 25 additions & 1 deletion static/app/views/alerts/rules/uptime/details.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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]});
Expand Down Expand Up @@ -69,6 +80,14 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) {
);
}

const handleUpdate = async (data: Partial<UptimeRule>) => {
const resp = await updateUptimeRule(api, organization.slug, uptimeRule, data);

if (resp !== null) {
setApiQueryData(queryClient, queryKey, resp);
}
};

return (
<Layout.Page>
<SentryDocumentTitle title={`${uptimeRule.name} — Alerts`} />
Expand Down Expand Up @@ -97,6 +116,11 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) {
</Layout.HeaderContent>
<Layout.HeaderActions>
<ButtonBar gap={1}>
<StatusToggleButton
uptimeRule={uptimeRule}
onToggleStatus={status => handleUpdate({status})}
size="sm"
/>
<LinkButton
size="sm"
icon={<IconEdit />}
Expand Down
42 changes: 42 additions & 0 deletions static/app/views/alerts/rules/uptime/statusToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -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<BaseButtonProps, 'onClick'> {
onToggleStatus: (status: ObjectStatus) => Promise<void>;
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 (
<Button
icon={<Icon />}
aria-label={label}
title={label}
onClick={async () => {
await onToggleStatus(isDisabled ? 'active' : 'disabled');
// TODO(epurkhiser): We'll need a hook here to trigger subscription
// refesh in getsentry when toggling uptime monitors since it will
// consume quota.
}}
{...props}
/>
);
}
3 changes: 2 additions & 1 deletion static/app/views/alerts/rules/uptime/types.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {Actor} from 'sentry/types/core';
import type {Actor, ObjectStatus} from 'sentry/types/core';

export enum UptimeMonitorStatus {
OK = 1,
Expand All @@ -22,6 +22,7 @@ export interface UptimeRule {
name: string;
owner: Actor;
projectSlug: string;
status: ObjectStatus;
timeoutMs: number;
traceSampling: boolean;
uptimeStatus: UptimeMonitorStatus;
Expand Down
1 change: 1 addition & 0 deletions tests/js/fixtures/uptimeRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function UptimeRuleFixture(params: Partial<UptimeRule> = {}): UptimeRule
projectSlug: 'project-slug',
environment: 'prod',
uptimeStatus: UptimeMonitorStatus.OK,
status: 'active',
timeoutMs: 5000,
url: 'https://sentry.io/',
headers: [],
Expand Down

0 comments on commit e881b51

Please sign in to comment.