Skip to content

Commit 17c0f3a

Browse files
committed
feat(aci): set up monitor Disable action
1 parent f4ba853 commit 17c0f3a

File tree

10 files changed

+178
-115
lines changed

10 files changed

+178
-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: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
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 {Link} from 'sentry/components/core/link';
48
import {IconEdit} from 'sentry/icons';
59
import {t, tct} from 'sentry/locale';
610
import type {Detector} from 'sentry/types/workflowEngine/detectors';
11+
import {useNavigate} from 'sentry/utils/useNavigate';
712
import useOrganization from 'sentry/utils/useOrganization';
8-
import {makeMonitorDetailsPathname} from 'sentry/views/detectors/pathnames';
13+
import {useUpdateDetector} from 'sentry/views/detectors/hooks';
14+
import {useDeleteDetectorMutation} from 'sentry/views/detectors/hooks/useDeleteDetectorMutation';
15+
import {
16+
makeMonitorBasePathname,
17+
makeMonitorDetailsPathname,
18+
} from 'sentry/views/detectors/pathnames';
919
import {useCanEditDetector} from 'sentry/views/detectors/utils/useCanEditDetector';
1020

1121
export function DisableDetectorAction({detector}: {detector: Detector}) {
22+
const {mutate: updateDetector, isPending: isUpdating} = useUpdateDetector();
23+
24+
const toggleDisabled = useCallback(() => {
25+
const newEnabled = !detector.enabled;
26+
updateDetector(
27+
{
28+
detectorId: detector.id,
29+
enabled: newEnabled,
30+
},
31+
{
32+
onSuccess: () => {
33+
addSuccessMessage(newEnabled ? t('Monitor enabled') : t('Monitor disabled'));
34+
},
35+
}
36+
);
37+
}, [updateDetector, detector.enabled, detector.id]);
38+
1239
const canEdit = useCanEditDetector({
1340
detectorType: detector.type,
1441
projectId: detector.projectId,
@@ -18,12 +45,9 @@ export function DisableDetectorAction({detector}: {detector: Detector}) {
1845
return null;
1946
}
2047

21-
/**
22-
* TODO: Implement disable detector
23-
*/
2448
return (
25-
<Button size="sm" onClick={() => {}}>
26-
{detector.disabled ? t('Enable') : t('Disable')}
49+
<Button size="sm" onClick={toggleDisabled} busy={isUpdating}>
50+
{detector.enabled ? t('Disable') : t('Enable')}
2751
</Button>
2852
);
2953
}
@@ -54,3 +78,45 @@ export function EditDetectorAction({detector}: {detector: Detector}) {
5478
</LinkButton>
5579
);
5680
}
81+
82+
export function DeleteDetectorAction({detector}: {detector: Detector}) {
83+
const organization = useOrganization();
84+
const navigate = useNavigate();
85+
const {mutate: deleteDetector, isPending: isDeleting} = useDeleteDetectorMutation();
86+
87+
const handleDelete = useCallback(() => {
88+
openConfirmModal({
89+
message: t('Are you sure you want to delete this monitor?'),
90+
confirmText: t('Delete'),
91+
priority: 'danger',
92+
onConfirm: () => {
93+
deleteDetector(detector.id, {
94+
onSuccess: () => {
95+
navigate(makeMonitorBasePathname(organization.slug));
96+
},
97+
});
98+
},
99+
});
100+
}, [deleteDetector, detector.id, navigate, organization.slug]);
101+
102+
const canEdit = useCanEditDetector({
103+
detectorType: detector.type,
104+
projectId: detector.projectId,
105+
});
106+
107+
if (!canEdit) {
108+
return null;
109+
}
110+
111+
return (
112+
<Button
113+
priority="danger"
114+
onClick={handleDelete}
115+
disabled={isDeleting}
116+
busy={isDeleting}
117+
size="sm"
118+
>
119+
{t('Delete')}
120+
</Button>
121+
);
122+
}

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',

0 commit comments

Comments
 (0)