Skip to content

Commit c866d64

Browse files
committed
feat(aci): set up monitor Disable action
1 parent 64505ca commit c866d64

File tree

10 files changed

+169
-115
lines changed

10 files changed

+169
-115
lines changed

static/app/types/workflowEngine/detectors.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ type BaseDetector = Readonly<{
112112
createdBy: string | null;
113113
dateCreated: string;
114114
dateUpdated: string;
115-
disabled: boolean;
115+
enabled: boolean;
116116
id: string;
117117
lastTriggered: string;
118118
name: string;
@@ -172,6 +172,7 @@ interface UpdateUptimeDataSourcePayload {
172172
}
173173

174174
export interface BaseDetectorUpdatePayload {
175+
enabled: boolean;
175176
name: string;
176177
owner: Detector['owner'];
177178
projectId: Detector['projectId'];

static/app/views/detectors/components/details/common/actions.tsx

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
1+
import {useCallback} from 'react';
2+
3+
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
4+
import {openConfirmModal} from 'sentry/components/confirm';
15
import {Button} from 'sentry/components/core/button';
26
import {LinkButton} from 'sentry/components/core/button/linkButton';
37
import {IconEdit} from 'sentry/icons';
48
import {t} from 'sentry/locale';
59
import type {Detector} from 'sentry/types/workflowEngine/detectors';
10+
import {useNavigate} from 'sentry/utils/useNavigate';
611
import useOrganization from 'sentry/utils/useOrganization';
7-
import {makeMonitorDetailsPathname} from 'sentry/views/detectors/pathnames';
12+
import {useUpdateDetector} from 'sentry/views/detectors/hooks';
13+
import {useDeleteDetectorMutation} from 'sentry/views/detectors/hooks/useDeleteDetectorMutation';
14+
import {
15+
makeMonitorBasePathname,
16+
makeMonitorDetailsPathname,
17+
} from 'sentry/views/detectors/pathnames';
818

919
export function DisableDetectorAction({detector}: {detector: Detector}) {
10-
/**
11-
* TODO: Implement disable detector
12-
*/
20+
const {mutate: updateDetector, isPending: isUpdating} = useUpdateDetector();
21+
22+
const toggleDisabled = useCallback(() => {
23+
const newEnabled = !detector.enabled;
24+
updateDetector(
25+
{
26+
detectorId: detector.id,
27+
enabled: newEnabled,
28+
},
29+
{
30+
onSuccess: () => {
31+
addSuccessMessage(newEnabled ? t('Monitor enabled') : t('Monitor disabled'));
32+
},
33+
}
34+
);
35+
}, [updateDetector, detector.enabled, detector.id]);
36+
1337
return (
14-
<Button size="sm" onClick={() => {}}>
15-
{detector.disabled ? t('Enable') : t('Disable')}
38+
<Button size="sm" onClick={toggleDisabled} busy={isUpdating}>
39+
{detector.enabled ? t('Disable') : t('Enable')}
1640
</Button>
1741
);
1842
}
@@ -31,3 +55,36 @@ export function EditDetectorAction({detector}: {detector: Detector}) {
3155
</LinkButton>
3256
);
3357
}
58+
59+
export function DeleteDetectorAction({detector}: {detector: Detector}) {
60+
const organization = useOrganization();
61+
const navigate = useNavigate();
62+
const {mutate: deleteDetector, isPending: isDeleting} = useDeleteDetectorMutation();
63+
64+
const handleDelete = useCallback(() => {
65+
openConfirmModal({
66+
message: t('Are you sure you want to delete this monitor?'),
67+
confirmText: t('Delete'),
68+
priority: 'danger',
69+
onConfirm: () => {
70+
deleteDetector(detector.id, {
71+
onSuccess: () => {
72+
navigate(makeMonitorBasePathname(organization.slug));
73+
},
74+
});
75+
},
76+
});
77+
}, [deleteDetector, detector.id, navigate, organization.slug]);
78+
79+
return (
80+
<Button
81+
priority="danger"
82+
onClick={handleDelete}
83+
disabled={isDeleting}
84+
busy={isDeleting}
85+
size="sm"
86+
>
87+
{t('Delete')}
88+
</Button>
89+
);
90+
}

static/app/views/detectors/components/detectorListTable/detectorListRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function DetectorListRow({detector}: DetectorListRowProps) {
1919

2020
return (
2121
<DetectorSimpleTableRow
22-
variant={detector.disabled ? 'faded' : 'default'}
22+
variant={detector.enabled ? 'default' : 'faded'}
2323
data-test-id="detector-list-row"
2424
>
2525
<SimpleTable.RowCell>

static/app/views/detectors/components/forms/editDetectorActions.spec.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

static/app/views/detectors/components/forms/editDetectorActions.tsx

Lines changed: 0 additions & 58 deletions
This file was deleted.

static/app/views/detectors/components/forms/editDetectorLayout.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import {useMemo} from 'react';
22

3+
import {Button} from 'sentry/components/core/button';
34
import type {Data} from 'sentry/components/forms/types';
45
import EditLayout from 'sentry/components/workflowEngine/layout/edit';
6+
import {t} from 'sentry/locale';
57
import type {
68
BaseDetectorUpdatePayload,
79
Detector,
810
} from 'sentry/types/workflowEngine/detectors';
11+
import {
12+
DeleteDetectorAction,
13+
DisableDetectorAction,
14+
} from 'sentry/views/detectors/components/details/common/actions';
915
import {EditDetectorBreadcrumbs} from 'sentry/views/detectors/components/forms/common/breadcrumbs';
1016
import {DetectorBaseFields} from 'sentry/views/detectors/components/forms/detectorBaseFields';
11-
import {EditDetectorActions} from 'sentry/views/detectors/components/forms/editDetectorActions';
1217
import {useEditDetectorFormSubmit} from 'sentry/views/detectors/hooks/useEditDetectorFormSubmit';
1318

1419
type EditDetectorLayoutProps<TDetector, TFormData, TUpdatePayload> = {
@@ -52,7 +57,11 @@ export function EditDetectorLayout<
5257
</EditLayout.HeaderContent>
5358

5459
<EditLayout.Actions>
55-
<EditDetectorActions detectorId={detector.id} />
60+
<DisableDetectorAction detector={detector} />
61+
<DeleteDetectorAction detector={detector} />
62+
<Button type="submit" priority="primary" size="sm">
63+
{t('Save')}
64+
</Button>
5665
</EditLayout.Actions>
5766

5867
<EditLayout.HeaderFields>

static/app/views/detectors/components/forms/error/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function EditExistingErrorDetectorForm({detector}: {detector: ErrorDetect
117117
type: 'error',
118118
name: detector.name,
119119
owner: detector.owner,
120+
enabled: detector.enabled,
120121
projectId: detector.projectId,
121122
workflowIds: data.workflowIds,
122123
dataSource: {},

static/app/views/detectors/edit.spec.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {ProjectFixture} from 'sentry-fixture/project';
66

77
import {
88
render,
9+
renderGlobalModal,
910
screen,
1011
userEvent,
1112
waitFor,
@@ -87,6 +88,88 @@ describe('DetectorEdit', () => {
8788
});
8889
});
8990

91+
describe('EditDetectorActions', () => {
92+
const mockDetector = MetricDetectorFixture();
93+
94+
it('calls delete mutation when deletion is confirmed', async () => {
95+
MockApiClient.addMockResponse({
96+
url: `/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
97+
body: mockDetector,
98+
});
99+
100+
const mockDeleteDetector = MockApiClient.addMockResponse({
101+
url: `/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
102+
method: 'DELETE',
103+
});
104+
105+
const {router} = render(<DetectorEdit />, {organization, initialRouterConfig});
106+
renderGlobalModal();
107+
108+
expect(
109+
await screen.findByRole('link', {name: mockDetector.name})
110+
).toBeInTheDocument();
111+
112+
await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
113+
114+
// Confirm the deletion
115+
const dialog = await screen.findByRole('dialog');
116+
await userEvent.click(within(dialog).getByRole('button', {name: 'Delete'}));
117+
118+
expect(mockDeleteDetector).toHaveBeenCalledWith(
119+
`/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
120+
expect.anything()
121+
);
122+
123+
// Redirect to the monitors list
124+
expect(router.location.pathname).toBe(
125+
`/organizations/${organization.slug}/issues/monitors/`
126+
);
127+
});
128+
129+
it('calls update mutation when enabling/disabling automation', async () => {
130+
MockApiClient.addMockResponse({
131+
url: `/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
132+
body: mockDetector,
133+
});
134+
135+
const mockUpdateDetector = MockApiClient.addMockResponse({
136+
url: `/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
137+
method: 'PUT',
138+
body: {detectorId: mockDetector.id, enabled: !mockDetector.enabled},
139+
});
140+
141+
render(<DetectorEdit />, {organization, initialRouterConfig});
142+
143+
expect(
144+
await screen.findByRole('link', {name: mockDetector.name})
145+
).toBeInTheDocument();
146+
147+
// Wait for the component to load and display automation actions
148+
expect(await screen.findByRole('button', {name: 'Disable'})).toBeInTheDocument();
149+
150+
MockApiClient.addMockResponse({
151+
url: `/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
152+
body: {...mockDetector, enabled: !mockDetector.enabled},
153+
});
154+
155+
// Click the toggle button to enable/disable the automation
156+
await userEvent.click(screen.getByRole('button', {name: 'Disable'}));
157+
158+
// Verify the mutation was called with correct data
159+
await waitFor(() => {
160+
expect(mockUpdateDetector).toHaveBeenCalledWith(
161+
`/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
162+
expect.objectContaining({
163+
data: {detectorId: mockDetector.id, enabled: !mockDetector.enabled},
164+
})
165+
);
166+
});
167+
168+
// Verify the button text has changed to "Enable"
169+
expect(await screen.findByRole('button', {name: 'Enable'})).toBeInTheDocument();
170+
});
171+
});
172+
90173
describe('Error', () => {
91174
const name = 'Test Error Detector';
92175
const mockDetector = ErrorDetectorFixture({id: '1', name, projectId: project.id});

static/app/views/detectors/hooks/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ export function useUpdateDetector() {
9595
const api = useApi({persistInFlight: true});
9696
const queryClient = useQueryClient();
9797

98-
return useMutation<Detector, void, {detectorId: string} & BaseDetectorUpdatePayload>({
98+
return useMutation<
99+
Detector,
100+
void,
101+
{detectorId: string} & Partial<BaseDetectorUpdatePayload>
102+
>({
99103
mutationFn: data =>
100104
api.requestPromise(`/organizations/${org.slug}/detectors/${data.detectorId}/`, {
101105
method: 'PUT',

tests/js/fixtures/detectors.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function MetricDetectorFixture(
2828
thresholdPeriod: 1,
2929
},
3030
type: 'metric_issue',
31-
disabled: false,
31+
enabled: true,
3232
conditionGroup: params.conditionGroup ?? DataConditionGroupFixture(),
3333
dataSources: params.dataSources ?? [SnubaQueryDataSourceFixture()],
3434
owner: null,
@@ -43,7 +43,7 @@ export function ErrorDetectorFixture(params: Partial<ErrorDetector> = {}): Error
4343
createdBy: null,
4444
dateCreated: '2025-01-01T00:00:00.000Z',
4545
dateUpdated: '2025-01-01T00:00:00.000Z',
46-
disabled: false,
46+
enabled: true,
4747
id: '2',
4848
lastTriggered: '2025-01-01T00:00:00.000Z',
4949
owner: null,
@@ -62,7 +62,7 @@ export function UptimeDetectorFixture(
6262
createdBy: null,
6363
dateCreated: '2025-01-01T00:00:00.000Z',
6464
dateUpdated: '2025-01-01T00:00:00.000Z',
65-
disabled: false,
65+
enabled: true,
6666
id: '3',
6767
lastTriggered: '2025-01-01T00:00:00.000Z',
6868
owner: null,

0 commit comments

Comments
 (0)