Skip to content

Commit 3e6203e

Browse files
authored
feat(aci): Implement connected monitors drawer in automation edit page (#95622)
1 parent a176683 commit 3e6203e

File tree

10 files changed

+492
-275
lines changed

10 files changed

+492
-275
lines changed

static/app/views/automations/components/automationForm.tsx

Lines changed: 13 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
1-
import {useState} from 'react';
1+
import {useCallback, useState} from 'react';
22
import styled from '@emotion/styled';
33

4-
import {Button} from 'sentry/components/core/button';
5-
import {LinkButton} from 'sentry/components/core/button/linkButton';
64
import {Flex} from 'sentry/components/core/layout';
75
import SelectField from 'sentry/components/forms/fields/selectField';
86
import type FormModel from 'sentry/components/forms/model';
9-
import useDrawer from 'sentry/components/globalDrawer';
107
import {DebugForm} from 'sentry/components/workflowEngine/form/debug';
118
import {EnvironmentSelector} from 'sentry/components/workflowEngine/form/environmentSelector';
129
import {useFormField} from 'sentry/components/workflowEngine/form/useFormField';
1310
import {Card} from 'sentry/components/workflowEngine/ui/card';
14-
import {IconAdd, IconEdit} from 'sentry/icons';
1511
import {t} from 'sentry/locale';
1612
import {space} from 'sentry/styles/space';
17-
import useOrganization from 'sentry/utils/useOrganization';
13+
import type {Automation} from 'sentry/types/workflowEngine/automations';
1814
import AutomationBuilder from 'sentry/views/automations/components/automationBuilder';
19-
import ConnectedMonitorsList from 'sentry/views/automations/components/connectedMonitorsList';
20-
import {EditConnectedMonitorsDrawer} from 'sentry/views/automations/components/editConnectedMonitorsDrawer';
21-
import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
22-
import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
15+
import EditConnectedMonitors from 'sentry/views/automations/components/editConnectedMonitors';
2316

2417
const FREQUENCY_OPTIONS = [
2518
{value: 5, label: t('5 minutes')},
@@ -34,40 +27,13 @@ const FREQUENCY_OPTIONS = [
3427
];
3528

3629
export default function AutomationForm({model}: {model: FormModel}) {
37-
const organization = useOrganization();
38-
39-
const {data: monitors = []} = useDetectorsQuery();
40-
const initialConnectedIds = useFormField('detectorIds');
41-
const [connectedIds, setConnectedIds] = useState<Set<string>>(
42-
initialConnectedIds ? new Set(initialConnectedIds) : new Set<string>()
30+
const initialConnectedIds = useFormField<Automation['detectorIds']>('detectorIds');
31+
const setConnectedIds = useCallback(
32+
(ids: Automation['detectorIds']) => {
33+
model.setValue('detectorIds', ids);
34+
},
35+
[model]
4336
);
44-
const connectedMonitors = monitors.filter(monitor => connectedIds.has(monitor.id));
45-
const updateConnectedIds = (ids: Set<string>) => {
46-
setConnectedIds(ids);
47-
model.setValue('detectorIds', Array.from(ids));
48-
};
49-
50-
const {openDrawer, isDrawerOpen, closeDrawer} = useDrawer();
51-
52-
const showEditMonitorsDrawer = () => {
53-
if (!isDrawerOpen) {
54-
openDrawer(
55-
() => (
56-
<EditConnectedMonitorsDrawer
57-
initialIds={connectedIds}
58-
onSave={ids => {
59-
updateConnectedIds(ids);
60-
closeDrawer();
61-
}}
62-
/>
63-
),
64-
{
65-
ariaLabel: 'Edit Monitors Drawer',
66-
drawerKey: 'edit-monitors-drawer',
67-
}
68-
);
69-
}
70-
};
7137

7238
const [environment, setEnvironment] = useState<string>('');
7339
const updateEnvironment = (env: string) => {
@@ -77,21 +43,10 @@ export default function AutomationForm({model}: {model: FormModel}) {
7743

7844
return (
7945
<Flex direction="column" gap={space(1.5)}>
80-
<Card>
81-
<Heading>{t('Connect Monitors')}</Heading>
82-
<ConnectedMonitorsList monitors={connectedMonitors} />
83-
<ButtonWrapper justify="space-between">
84-
<LinkButton
85-
icon={<IconAdd />}
86-
to={`${makeMonitorBasePathname(organization.slug)}new/`}
87-
>
88-
{t('Create New Monitor')}
89-
</LinkButton>
90-
<Button icon={<IconEdit />} onClick={showEditMonitorsDrawer}>
91-
{t('Edit Monitors')}
92-
</Button>
93-
</ButtonWrapper>
94-
</Card>
46+
<EditConnectedMonitors
47+
connectedIds={initialConnectedIds || []}
48+
setConnectedIds={setConnectedIds}
49+
/>
9550
<Card>
9651
<Flex direction="column" gap={space(0.5)}>
9752
<Heading>{t('Choose Environment')}</Heading>
@@ -134,12 +89,6 @@ const Description = styled('span')`
13489
padding: 0;
13590
`;
13691

137-
const ButtonWrapper = styled(Flex)`
138-
border-top: 1px solid ${p => p.theme.border};
139-
padding: ${space(2)};
140-
margin: -${space(2)};
141-
`;
142-
14392
const EmbeddedSelectField = styled(SelectField)`
14493
padding: 0;
14594
font-weight: ${p => p.theme.fontWeight.normal};

static/app/views/automations/components/connectedMonitorsList.tsx

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,77 @@
1-
import type {Dispatch, SetStateAction} from 'react';
1+
import {Fragment} from 'react';
22
import styled from '@emotion/styled';
33

44
import {Button} from 'sentry/components/core/button';
5+
import LoadingError from 'sentry/components/loadingError';
6+
import Placeholder from 'sentry/components/placeholder';
57
import {SimpleTable} from 'sentry/components/tables/simpleTable';
68
import {IssueCell} from 'sentry/components/workflowEngine/gridCell/issueCell';
79
import {t} from 'sentry/locale';
810
import {space} from 'sentry/styles/space';
11+
import type {Automation} from 'sentry/types/workflowEngine/automations';
912
import type {Detector} from 'sentry/types/workflowEngine/detectors';
1013
import {DetectorLink} from 'sentry/views/detectors/components/detectorLink';
1114
import {DetectorAssigneeCell} from 'sentry/views/detectors/components/detectorListTable/detectorAssigneeCell';
1215
import {DetectorTypeCell} from 'sentry/views/detectors/components/detectorListTable/detectorTypeCell';
1316

14-
type Props = {
15-
monitors: Detector[];
16-
connectedIds?: Set<string>;
17-
setConnectedIds?: Dispatch<SetStateAction<Set<string>>>;
17+
type Props = React.HTMLAttributes<HTMLDivElement> & {
18+
/**
19+
* If null, all detectors will be fetched.
20+
*/
21+
detectors: Detector[];
22+
isError: boolean;
23+
isLoading: boolean;
24+
connectedDetectorIds?: Automation['detectorIds'];
25+
emptyMessage?: string;
26+
numSkeletons?: number;
27+
toggleConnected?: (params: {detector: Detector}) => void;
1828
};
1929

30+
function Skeletons({canEdit, numberOfRows}: {canEdit: boolean; numberOfRows: number}) {
31+
return (
32+
<Fragment>
33+
{Array.from({length: numberOfRows}).map((_, index) => (
34+
<SimpleTable.Row key={index}>
35+
<SimpleTable.RowCell>
36+
<div style={{width: '100%'}}>
37+
<Placeholder height="20px" width="50%" style={{marginBottom: '4px'}} />
38+
<Placeholder height="16px" width="20%" />
39+
</div>
40+
</SimpleTable.RowCell>
41+
<SimpleTable.RowCell data-column-name="type">
42+
<Placeholder height="20px" />
43+
</SimpleTable.RowCell>
44+
<SimpleTable.RowCell data-column-name="last-issue">
45+
<Placeholder height="20px" />
46+
</SimpleTable.RowCell>
47+
<SimpleTable.RowCell data-column-name="owner">
48+
<Placeholder height="20px" />
49+
</SimpleTable.RowCell>
50+
{canEdit && (
51+
<SimpleTable.RowCell data-column-name="connected">
52+
<Placeholder height="20px" />
53+
</SimpleTable.RowCell>
54+
)}
55+
</SimpleTable.Row>
56+
))}
57+
</Fragment>
58+
);
59+
}
60+
2061
export default function ConnectedMonitorsList({
21-
monitors,
22-
connectedIds,
23-
setConnectedIds,
62+
detectors,
63+
isLoading,
64+
isError,
65+
connectedDetectorIds,
66+
toggleConnected,
67+
emptyMessage = t('No monitors connected'),
68+
numSkeletons = 10,
69+
...props
2470
}: Props) {
25-
const canEdit = connectedIds && !!setConnectedIds;
26-
27-
const toggleConnected = (id: string) => {
28-
setConnectedIds?.(prev => {
29-
const newSet = new Set(prev);
30-
if (newSet.has(id)) {
31-
newSet.delete(id);
32-
} else {
33-
newSet.add(id);
34-
}
35-
return newSet;
36-
});
37-
};
71+
const canEdit = Boolean(connectedDetectorIds && typeof toggleConnected === 'function');
3872

3973
return (
40-
<Container>
74+
<Container {...props}>
4175
<SimpleTableWithColumns>
4276
<SimpleTable.Header>
4377
<SimpleTable.HeaderCell>{t('Name')}</SimpleTable.HeaderCell>
@@ -52,27 +86,31 @@ export default function ConnectedMonitorsList({
5286
</SimpleTable.HeaderCell>
5387
{canEdit && <SimpleTable.HeaderCell data-column-name="connected" />}
5488
</SimpleTable.Header>
55-
{monitors.length === 0 && (
56-
<SimpleTable.Empty>{t('No monitors connected')}</SimpleTable.Empty>
89+
{isLoading && <Skeletons canEdit={canEdit} numberOfRows={numSkeletons} />}
90+
{isError && <LoadingError />}
91+
{!isLoading && !isError && detectors.length === 0 && (
92+
<SimpleTable.Empty>{emptyMessage}</SimpleTable.Empty>
5793
)}
58-
{monitors.map(monitor => (
59-
<SimpleTable.Row key={monitor.id}>
94+
{detectors.map(detector => (
95+
<SimpleTable.Row key={detector.id}>
6096
<SimpleTable.RowCell>
61-
<DetectorLink detector={monitor} />
97+
<DetectorLink detector={detector} />
6298
</SimpleTable.RowCell>
6399
<SimpleTable.RowCell data-column-name="type">
64-
<DetectorTypeCell type={monitor.type} />
100+
<DetectorTypeCell type={detector.type} />
65101
</SimpleTable.RowCell>
66102
<SimpleTable.RowCell data-column-name="last-issue">
67103
<IssueCell group={undefined} />
68104
</SimpleTable.RowCell>
69105
<SimpleTable.RowCell data-column-name="owner">
70-
<DetectorAssigneeCell assignee={monitor.owner} />
106+
<DetectorAssigneeCell assignee={detector.owner} />
71107
</SimpleTable.RowCell>
72108
{canEdit && (
73109
<SimpleTable.RowCell data-column-name="connected" justify="flex-end">
74-
<Button onClick={() => toggleConnected(monitor.id)} size="sm">
75-
{connectedIds?.has(monitor.id) ? t('Disconnect') : t('Connect')}
110+
<Button onClick={() => toggleConnected?.({detector})} size="sm">
111+
{connectedDetectorIds?.includes(detector.id)
112+
? t('Disconnect')
113+
: t('Connect')}
76114
</Button>
77115
</SimpleTable.RowCell>
78116
)}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {MetricDetectorFixture} from 'sentry-fixture/detectors';
2+
3+
import {
4+
render,
5+
screen,
6+
userEvent,
7+
waitFor,
8+
within,
9+
} from 'sentry-test/reactTestingLibrary';
10+
11+
import EditConnectedMonitors from './editConnectedMonitors';
12+
13+
describe('EditConnectedMonitors', function () {
14+
const detector1 = MetricDetectorFixture({
15+
id: '1',
16+
name: 'Metric Monitor 1',
17+
type: 'metric_issue',
18+
});
19+
20+
beforeEach(() => {
21+
jest.resetAllMocks();
22+
MockApiClient.clearMockResponses();
23+
24+
MockApiClient.addMockResponse({
25+
url: '/organizations/org-slug/detectors/',
26+
method: 'GET',
27+
body: [detector1],
28+
});
29+
});
30+
31+
it('can connect an existing monitor', async function () {
32+
const setConnectedIds = jest.fn();
33+
render(<EditConnectedMonitors connectedIds={[]} setConnectedIds={setConnectedIds} />);
34+
35+
expect(screen.getByText('Connected Monitors')).toBeInTheDocument();
36+
37+
await userEvent.click(screen.getByText('Connect Monitors'));
38+
39+
const drawer = await screen.findByRole('complementary', {
40+
name: 'Connect Monitors',
41+
});
42+
43+
await within(drawer).findByText(detector1.name);
44+
45+
const allMonitorsList = await screen.findByTestId('drawer-all-monitors-list');
46+
47+
expect(within(allMonitorsList).getByText(detector1.name)).toBeInTheDocument();
48+
49+
// Clicking connect should add the automation to the connected list
50+
await userEvent.click(within(drawer).getByRole('button', {name: 'Connect'}));
51+
const connectedMonitorsList = await screen.findByTestId(
52+
'drawer-connected-monitors-list'
53+
);
54+
expect(within(connectedMonitorsList).getByText(detector1.name)).toBeInTheDocument();
55+
56+
expect(setConnectedIds).toHaveBeenCalledWith([detector1.id]);
57+
});
58+
59+
it('can disconnect an existing monitor', async function () {
60+
const setConnectedIds = jest.fn();
61+
render(
62+
<EditConnectedMonitors
63+
connectedIds={[detector1.id]}
64+
setConnectedIds={setConnectedIds}
65+
/>
66+
);
67+
68+
// Should display automation as connected
69+
expect(screen.getByText('Connected Monitors')).toBeInTheDocument();
70+
expect(await screen.findByText(detector1.name)).toBeInTheDocument();
71+
72+
await userEvent.click(screen.getByText('Edit Monitors'));
73+
const drawer = await screen.findByRole('complementary', {
74+
name: 'Connect Monitors',
75+
});
76+
77+
const connectedMonitorsList = await screen.findByTestId(
78+
'drawer-connected-monitors-list'
79+
);
80+
expect(within(connectedMonitorsList).getByText(detector1.name)).toBeInTheDocument();
81+
82+
// Clicking disconnect should remove the automation from the connected list
83+
await userEvent.click(
84+
within(drawer).getAllByRole('button', {name: 'Disconnect'})[0]!
85+
);
86+
await waitFor(() => {
87+
expect(
88+
screen.queryByTestId('drawer-connected-monitors-list')
89+
).not.toBeInTheDocument();
90+
});
91+
92+
expect(setConnectedIds).toHaveBeenCalledWith([]);
93+
});
94+
});

0 commit comments

Comments
 (0)