Skip to content

Commit 51198ac

Browse files
authored
feat(aci): Add connect automation drawer to edit monitor page (#95485)
1 parent 5c9c651 commit 51198ac

File tree

11 files changed

+395
-51
lines changed

11 files changed

+395
-51
lines changed

static/app/types/workflowEngine/detectors.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export interface BaseDetectorUpdatePayload {
174174
name: string;
175175
owner: Detector['owner'];
176176
projectId: Detector['projectId'];
177+
workflowIds: string[];
177178
}
178179

179180
export interface UptimeDetectorUpdatePayload extends BaseDetectorUpdatePayload {

static/app/views/automations/hooks/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {useApiQuery, useMutation, useQueryClient} from 'sentry/utils/queryClient
1212
import useApi from 'sentry/utils/useApi';
1313
import useOrganization from 'sentry/utils/useOrganization';
1414

15-
const makeAutomationsQueryKey = ({
15+
export const makeAutomationsQueryKey = ({
1616
orgSlug,
1717
query,
1818
sortBy,

static/app/views/detectors/components/connectedAutomationList.tsx

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
33

44
import {Button} from 'sentry/components/core/button';
55
import LoadingError from 'sentry/components/loadingError';
6+
import type {CursorHandler} from 'sentry/components/pagination';
67
import Pagination from 'sentry/components/pagination';
78
import Placeholder from 'sentry/components/placeholder';
89
import {SimpleTable} from 'sentry/components/tables/simpleTable';
@@ -11,18 +12,24 @@ import AutomationTitleCell from 'sentry/components/workflowEngine/gridCell/autom
1112
import {TimeAgoCell} from 'sentry/components/workflowEngine/gridCell/timeAgoCell';
1213
import {t} from 'sentry/locale';
1314
import {space} from 'sentry/styles/space';
15+
import type {Automation} from 'sentry/types/workflowEngine/automations';
1416
import type {Detector} from 'sentry/types/workflowEngine/detectors';
15-
import {useLocation} from 'sentry/utils/useLocation';
16-
import {useNavigate} from 'sentry/utils/useNavigate';
1717
import {useAutomationsQuery} from 'sentry/views/automations/hooks';
1818
import {getAutomationActions} from 'sentry/views/automations/hooks/utils';
1919

20-
const AUTOMATIONS_PER_PAGE = 10;
20+
const DEFAULT_AUTOMATIONS_PER_PAGE = 10;
2121

22-
type Props = {
23-
automationIds: Detector['workflowIds'];
22+
type Props = React.HTMLAttributes<HTMLDivElement> & {
23+
/**
24+
* If null, all automations will be fetched.
25+
*/
26+
automationIds: Detector['workflowIds'] | null;
27+
cursor: string | undefined;
28+
onCursor: CursorHandler;
2429
connectedAutomationIds?: Set<string>;
25-
toggleConnected?: (id: string) => void;
30+
emptyMessage?: string;
31+
limit?: number | null;
32+
toggleConnected?: (params: {automation: Automation}) => void;
2633
};
2734

2835
function Skeletons({canEdit, numberOfRows}: {canEdit: boolean; numberOfRows: number}) {
@@ -54,12 +61,15 @@ export function ConnectedAutomationsList({
5461
automationIds,
5562
connectedAutomationIds,
5663
toggleConnected,
64+
emptyMessage = t('No automations connected'),
65+
cursor,
66+
onCursor,
67+
limit = DEFAULT_AUTOMATIONS_PER_PAGE,
68+
...props
5769
}: Props) {
5870
const canEdit = Boolean(
5971
connectedAutomationIds && typeof toggleConnected === 'function'
6072
);
61-
const navigate = useNavigate();
62-
const location = useLocation();
6373

6474
const {
6575
data: automations,
@@ -69,16 +79,17 @@ export function ConnectedAutomationsList({
6979
getResponseHeader,
7080
} = useAutomationsQuery(
7181
{
72-
ids: automationIds,
73-
limit: AUTOMATIONS_PER_PAGE,
74-
cursor:
75-
typeof location.query.cursor === 'string' ? location.query.cursor : undefined,
82+
ids: automationIds ?? undefined,
83+
limit: limit ?? undefined,
84+
cursor,
7685
},
77-
{enabled: automationIds.length > 0}
86+
{enabled: automationIds === null || automationIds.length > 0}
7887
);
7988

89+
const pageLinks = getResponseHeader?.('Link');
90+
8091
return (
81-
<Container>
92+
<Container {...props}>
8293
<SimpleTableWithColumns>
8394
<SimpleTable.Header>
8495
<SimpleTable.HeaderCell>{t('Name')}</SimpleTable.HeaderCell>
@@ -93,12 +104,17 @@ export function ConnectedAutomationsList({
93104
{isLoading && (
94105
<Skeletons
95106
canEdit={canEdit}
96-
numberOfRows={Math.min(automationIds.length, AUTOMATIONS_PER_PAGE)}
107+
numberOfRows={
108+
automationIds === null
109+
? (limit ?? DEFAULT_AUTOMATIONS_PER_PAGE)
110+
: Math.min(automationIds?.length ?? 0, DEFAULT_AUTOMATIONS_PER_PAGE)
111+
}
97112
/>
98113
)}
99114
{isError && <LoadingError />}
100-
{automationIds.length === 0 && (
101-
<SimpleTable.Empty>{t('No automations connected')}</SimpleTable.Empty>
115+
{((isSuccess && automations.length === 0) ||
116+
(automationIds !== null && automationIds.length === 0)) && (
117+
<SimpleTable.Empty>{emptyMessage}</SimpleTable.Empty>
102118
)}
103119
{isSuccess &&
104120
automations.map(automation => (
@@ -117,7 +133,7 @@ export function ConnectedAutomationsList({
117133
</SimpleTable.RowCell>
118134
{canEdit && (
119135
<SimpleTable.RowCell data-column-name="connected" justify="flex-end">
120-
<Button onClick={() => toggleConnected?.(automation.id)} size="sm">
136+
<Button onClick={() => toggleConnected?.({automation})} size="sm">
121137
{connectedAutomationIds?.has(automation.id)
122138
? t('Disconnect')
123139
: t('Connect')}
@@ -127,18 +143,7 @@ export function ConnectedAutomationsList({
127143
</SimpleTable.Row>
128144
))}
129145
</SimpleTableWithColumns>
130-
<Pagination
131-
onCursor={cursor => {
132-
navigate({
133-
pathname: location.pathname,
134-
query: {
135-
...location.query,
136-
cursor,
137-
},
138-
});
139-
}}
140-
pageLinks={getResponseHeader?.('Link')}
141-
/>
146+
{limit === null ? null : <Pagination onCursor={onCursor} pageLinks={pageLinks} />}
142147
</Container>
143148
);
144149
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,37 @@ import ErrorBoundary from 'sentry/components/errorBoundary';
22
import Section from 'sentry/components/workflowEngine/ui/section';
33
import {t} from 'sentry/locale';
44
import type {Detector} from 'sentry/types/workflowEngine/detectors';
5+
import {useLocation} from 'sentry/utils/useLocation';
6+
import {useNavigate} from 'sentry/utils/useNavigate';
57
import {ConnectedAutomationsList} from 'sentry/views/detectors/components/connectedAutomationList';
68

79
type Props = {
810
detector: Detector;
911
};
1012

1113
export function DetectorDetailsAutomations({detector}: Props) {
14+
const location = useLocation();
15+
const navigate = useNavigate();
16+
17+
const cursor =
18+
typeof location.query.cursor === 'string' ? location.query.cursor : undefined;
19+
1220
return (
1321
<Section title={t('Connected Automations')}>
1422
<ErrorBoundary mini>
15-
<ConnectedAutomationsList automationIds={detector.workflowIds} />
23+
<ConnectedAutomationsList
24+
automationIds={detector.workflowIds}
25+
cursor={cursor}
26+
onCursor={newCursor => {
27+
navigate({
28+
pathname: location.pathname,
29+
query: {
30+
...location.query,
31+
cursor: newCursor,
32+
},
33+
});
34+
}}
35+
/>
1636
</ErrorBoundary>
1737
</Section>
1838
);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {AutomationFixture} from 'sentry-fixture/automations';
2+
3+
import {
4+
render,
5+
screen,
6+
userEvent,
7+
waitFor,
8+
within,
9+
} from 'sentry-test/reactTestingLibrary';
10+
11+
import Form from 'sentry/components/forms/form';
12+
13+
import {AutomateSection} from './automateSection';
14+
15+
describe('AutomateSection', function () {
16+
const automation1 = AutomationFixture();
17+
18+
beforeEach(() => {
19+
jest.resetAllMocks();
20+
MockApiClient.clearMockResponses();
21+
22+
MockApiClient.addMockResponse({
23+
url: '/organizations/org-slug/workflows/',
24+
method: 'GET',
25+
match: [MockApiClient.matchQuery({ids: [automation1.id]})],
26+
body: [automation1],
27+
});
28+
29+
MockApiClient.addMockResponse({
30+
url: '/organizations/org-slug/workflows/',
31+
method: 'GET',
32+
body: [automation1],
33+
});
34+
});
35+
36+
it('can connect an existing automation', async function () {
37+
render(
38+
<Form>
39+
<AutomateSection />
40+
</Form>
41+
);
42+
43+
expect(screen.getByText('Automate')).toBeInTheDocument();
44+
45+
await userEvent.click(screen.getByText('Connect an Automation'));
46+
47+
const drawer = await screen.findByRole('complementary', {
48+
name: 'Connect Automations',
49+
});
50+
51+
await within(drawer).findByText(automation1.name);
52+
53+
const connectedAutomationsList = await screen.findByTestId(
54+
'drawer-connected-automations-list'
55+
);
56+
const allAutomationsList = await screen.findByTestId('drawer-all-automations-list');
57+
58+
expect(within(allAutomationsList).getByText(automation1.name)).toBeInTheDocument();
59+
60+
// Clicking connect should add the automation to the connected list
61+
await userEvent.click(within(drawer).getByRole('button', {name: 'Connect'}));
62+
await waitFor(() => {
63+
expect(
64+
within(connectedAutomationsList).getByText(automation1.name)
65+
).toBeInTheDocument();
66+
});
67+
});
68+
69+
it('can disconnect an existing automation', async function () {
70+
render(
71+
<Form initialData={{workflowIds: [automation1.id]}}>
72+
<AutomateSection />
73+
</Form>
74+
);
75+
76+
// Should display automation as connected
77+
expect(screen.getByText('Connected Automations')).toBeInTheDocument();
78+
expect(await screen.findByText(automation1.name)).toBeInTheDocument();
79+
80+
await userEvent.click(screen.getByText('Edit Automations'));
81+
const drawer = await screen.findByRole('complementary', {
82+
name: 'Connect Automations',
83+
});
84+
85+
const connectedAutomationsList = await screen.findByTestId(
86+
'drawer-connected-automations-list'
87+
);
88+
expect(
89+
within(connectedAutomationsList).getByText(automation1.name)
90+
).toBeInTheDocument();
91+
92+
// Clicking disconnect should remove the automation from the connected list
93+
await userEvent.click(
94+
within(drawer).getAllByRole('button', {name: 'Disconnect'})[0]!
95+
);
96+
await waitFor(() => {
97+
expect(
98+
within(connectedAutomationsList).queryByText(automation1.name)
99+
).not.toBeInTheDocument();
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)