Skip to content

Commit 9d33159

Browse files
authored
ref(aci): ref editConnectedMonitors to accept list of connected ids (#92129)
Needed to lift state out of the `editConnectedMonitors` component since the Edit Monitors drawer (`editConnectedMonitorsDrawer`) does not rerender when the state is updated, since it is only rendered once when the drawer is opened. To fix this, I added a footer to the drawer where changes to connected monitors can be saved. After the user has saved their changes, these changes are reflected on the config page. https://github.com/user-attachments/assets/48ef6411-64b9-4520-a12c-4b300631f49b Also added a temporary filter to the detector query to only fetch metric detectors, since uptime monitors are erroring
1 parent d16fbf0 commit 9d33159

File tree

8 files changed

+141
-52
lines changed

8 files changed

+141
-52
lines changed

static/app/components/workflowEngine/ui/footer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const StickyFooter = styled('div')`
1616
display: flex;
1717
align-items: center;
1818
justify-content: space-between;
19+
z-index: ${p => p.theme.zIndex.initial};
1920
`;
2021

2122
export const StickyFooterLabel = styled('p')`

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

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@ import {Button} from 'sentry/components/core/button';
77
import {LinkButton} from 'sentry/components/core/button/linkButton';
88
import SelectField from 'sentry/components/forms/fields/selectField';
99
import Form from 'sentry/components/forms/form';
10-
import FormModel from 'sentry/components/forms/model';
10+
import type FormModel from 'sentry/components/forms/model';
1111
import useDrawer from 'sentry/components/globalDrawer';
12-
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
1312
import {useDocumentTitle} from 'sentry/components/sentryDocumentTitle';
1413
import {DebugForm} from 'sentry/components/workflowEngine/form/debug';
1514
import {EnvironmentSelector} from 'sentry/components/workflowEngine/form/environmentSelector';
1615
import {Card} from 'sentry/components/workflowEngine/ui/card';
1716
import {IconAdd, IconEdit} from 'sentry/icons';
1817
import {t} from 'sentry/locale';
1918
import {space} from 'sentry/styles/space';
20-
import type {Detector} from 'sentry/types/workflowEngine/detectors';
2119
import useOrganization from 'sentry/utils/useOrganization';
2220
import AutomationBuilder from 'sentry/views/automations/components/automationBuilder';
2321
import {
@@ -26,11 +24,9 @@ import {
2624
useAutomationBuilderReducer,
2725
} from 'sentry/views/automations/components/automationBuilderContext';
2826
import ConnectedMonitorsList from 'sentry/views/automations/components/connectedMonitorsList';
29-
import EditConnectedMonitors from 'sentry/views/automations/components/editConnectedMonitors';
30-
import {
31-
NEW_AUTOMATION_CONNECTED_IDS_KEY,
32-
useConnectedIds,
33-
} from 'sentry/views/automations/hooks/utils';
27+
import {EditConnectedMonitorsDrawer} from 'sentry/views/automations/components/editConnectedMonitorsDrawer';
28+
import {NEW_AUTOMATION_CONNECTED_IDS_KEY} from 'sentry/views/automations/hooks/utils';
29+
import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
3430
import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
3531

3632
const FREQUENCY_OPTIONS = [
@@ -45,36 +41,36 @@ const FREQUENCY_OPTIONS = [
4541
{value: '43200', label: t('30 days')},
4642
];
4743

48-
export default function AutomationForm() {
44+
export default function AutomationForm({model}: {model: FormModel}) {
4945
const organization = useOrganization();
5046
const title = useDocumentTitle();
5147
const {state, actions} = useAutomationBuilderReducer();
52-
const [model] = useState(() => new FormModel());
5348

5449
useEffect(() => {
5550
model.setValue('name', title);
5651
}, [title, model]);
5752

58-
const monitors: Detector[] = []; // TODO: Fetch monitors from API
53+
const {data: monitors = []} = useDetectorsQuery();
5954
const storageKey = NEW_AUTOMATION_CONNECTED_IDS_KEY; // TODO: use automation id for storage key when editing an existing automation
60-
const {connectedIds, toggleConnected} = useConnectedIds({
61-
storageKey,
62-
});
55+
const [connectedIds, setConnectedIds] = useState<Set<string>>(
56+
() => new Set(JSON.parse(localStorage.getItem(storageKey) || '[]'))
57+
);
6358
const connectedMonitors = monitors.filter(monitor => connectedIds.has(monitor.id));
6459

65-
const {openDrawer: openEditMonitorsDrawer, isDrawerOpen: isEditMonitorsDrawerOpen} =
66-
useDrawer();
60+
const {openDrawer, isDrawerOpen, closeDrawer} = useDrawer();
6761

6862
const showEditMonitorsDrawer = () => {
69-
if (!isEditMonitorsDrawerOpen) {
70-
openEditMonitorsDrawer(
63+
if (!isDrawerOpen) {
64+
openDrawer(
7165
() => (
72-
<div>
73-
<DrawerHeader />
74-
<DrawerBody>
75-
<EditConnectedMonitors storageKey={storageKey} />
76-
</DrawerBody>
77-
</div>
66+
<EditConnectedMonitorsDrawer
67+
initialIds={connectedIds}
68+
onSave={ids => {
69+
setConnectedIds(ids);
70+
localStorage.setItem(storageKey, JSON.stringify(Array.from(ids)));
71+
closeDrawer();
72+
}}
73+
/>
7874
),
7975
{
8076
ariaLabel: 'Edit Monitors Drawer',
@@ -98,8 +94,8 @@ export default function AutomationForm() {
9894
<Heading>{t('Connect Monitors')}</Heading>
9995
<ConnectedMonitorsList
10096
monitors={connectedMonitors}
101-
connectedMonitorIds={connectedIds}
102-
toggleConnected={toggleConnected}
97+
connectedIds={connectedIds}
98+
setConnectedIds={setConnectedIds}
10399
/>
104100
<ButtonWrapper justify="space-between">
105101
<LinkButton

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type {Dispatch, SetStateAction} from 'react';
2+
13
import {Button} from 'sentry/components/core/button';
24
import {IssueCell} from 'sentry/components/workflowEngine/gridCell/issueCell';
35
import {TitleCell} from 'sentry/components/workflowEngine/gridCell/titleCell';
@@ -12,17 +14,29 @@ import {makeMonitorDetailsPathname} from 'sentry/views/detectors/pathnames';
1214

1315
type Props = {
1416
monitors: Detector[];
15-
connectedMonitorIds?: Set<string>;
16-
toggleConnected?: (id: string) => void;
17+
connectedIds?: Set<string>;
18+
setConnectedIds?: Dispatch<SetStateAction<Set<string>>>;
1719
};
1820

1921
export default function ConnectedMonitorsList({
2022
monitors,
21-
connectedMonitorIds,
22-
toggleConnected,
23+
connectedIds,
24+
setConnectedIds,
2325
}: Props) {
2426
const organization = useOrganization();
25-
const canEdit = connectedMonitorIds && !!toggleConnected;
27+
const canEdit = connectedIds && !!setConnectedIds;
28+
29+
const toggleConnected = (id: string) => {
30+
setConnectedIds?.(prev => {
31+
const newSet = new Set(prev);
32+
if (newSet.has(id)) {
33+
newSet.delete(id);
34+
} else {
35+
newSet.add(id);
36+
}
37+
return newSet;
38+
});
39+
};
2640

2741
const data = monitors.map(monitor => ({
2842
title: {
@@ -35,7 +49,7 @@ export default function ConnectedMonitorsList({
3549
createdBy: monitor.createdBy,
3650
connected: canEdit
3751
? {
38-
isConnected: connectedMonitorIds?.has(monitor.id),
52+
isConnected: connectedIds?.has(monitor.id),
3953
toggleConnected: () => toggleConnected?.(monitor.id),
4054
}
4155
: undefined,

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1+
import type {Dispatch, SetStateAction} from 'react';
12
import {Fragment} from 'react';
23
import styled from '@emotion/styled';
34

45
import SearchBar from 'sentry/components/searchBar';
56
import {t} from 'sentry/locale';
67
import {space} from 'sentry/styles/space';
7-
import type {Detector} from 'sentry/types/workflowEngine/detectors';
88
import ConnectedMonitorsList from 'sentry/views/automations/components/connectedMonitorsList';
9-
import {useConnectedIds} from 'sentry/views/automations/hooks/utils';
9+
import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
1010

1111
interface Props {
12-
storageKey: string;
12+
connectedIds: Set<string>;
13+
setConnectedIds: Dispatch<SetStateAction<Set<string>>>;
1314
}
1415

15-
export default function EditConnectedMonitors({storageKey}: Props) {
16-
const monitors: Detector[] = []; // TODO: Fetch monitors from API
17-
const {connectedIds, toggleConnected} = useConnectedIds({storageKey});
16+
export default function EditConnectedMonitors({connectedIds, setConnectedIds}: Props) {
17+
const {data: monitors = []} = useDetectorsQuery();
1818

1919
const connectedMonitors = monitors.filter(monitor => connectedIds.has(monitor.id));
2020
const unconnectedMonitors = monitors.filter(monitor => !connectedIds.has(monitor.id));
@@ -26,8 +26,8 @@ export default function EditConnectedMonitors({storageKey}: Props) {
2626
<Heading>{t('Connected Monitors')}</Heading>
2727
<ConnectedMonitorsList
2828
monitors={connectedMonitors}
29-
connectedMonitorIds={connectedIds}
30-
toggleConnected={toggleConnected}
29+
connectedIds={connectedIds}
30+
setConnectedIds={setConnectedIds}
3131
/>
3232
</Fragment>
3333
)}
@@ -39,8 +39,8 @@ export default function EditConnectedMonitors({storageKey}: Props) {
3939
</div>
4040
<ConnectedMonitorsList
4141
monitors={unconnectedMonitors}
42-
connectedMonitorIds={connectedIds}
43-
toggleConnected={toggleConnected}
42+
connectedIds={connectedIds}
43+
setConnectedIds={setConnectedIds}
4444
/>
4545
</div>
4646
);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {useState} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {Flex} from 'sentry/components/container/flex';
5+
import {Button} from 'sentry/components/core/button';
6+
import {LinkButton} from 'sentry/components/core/button/linkButton';
7+
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
8+
import {StickyFooter} from 'sentry/components/workflowEngine/ui/footer';
9+
import {IconAdd} from 'sentry/icons';
10+
import {t} from 'sentry/locale';
11+
import useOrganization from 'sentry/utils/useOrganization';
12+
import EditConnectedMonitors from 'sentry/views/automations/components/editConnectedMonitors';
13+
import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
14+
15+
interface Props {
16+
initialIds: Set<string>;
17+
onSave: (ids: Set<string>) => void;
18+
}
19+
20+
export function EditConnectedMonitorsDrawer({initialIds, onSave}: Props) {
21+
const organization = useOrganization();
22+
const [connectedIds, setConnectedIds] = useState<Set<string>>(initialIds);
23+
24+
return (
25+
<DrawerWrapper>
26+
<DrawerHeader />
27+
<StyledDrawerBody>
28+
<EditConnectedMonitors
29+
connectedIds={connectedIds}
30+
setConnectedIds={setConnectedIds}
31+
/>
32+
</StyledDrawerBody>
33+
<StickyFooter>
34+
<Flex justify="space-between" flex={1}>
35+
<LinkButton
36+
icon={<IconAdd />}
37+
to={`${makeMonitorBasePathname(organization.slug)}new/`}
38+
>
39+
{t('Create New Monitor')}
40+
</LinkButton>
41+
<Button priority="primary" onClick={() => onSave(connectedIds)}>
42+
{t('Save')}
43+
</Button>
44+
</Flex>
45+
</StickyFooter>
46+
</DrawerWrapper>
47+
);
48+
}
49+
50+
const DrawerWrapper = styled('div')`
51+
display: flex;
52+
flex-direction: column;
53+
height: 100%;
54+
overflow: hidden;
55+
`;
56+
57+
const StyledDrawerBody = styled(DrawerBody)`
58+
flex: 1;
59+
overflow: auto;
60+
`;

static/app/views/automations/new-settings.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import {useMemo} from 'react';
2+
13
import {Flex} from 'sentry/components/container/flex';
24
import {Button} from 'sentry/components/core/button';
35
import {LinkButton} from 'sentry/components/core/button/linkButton';
6+
import FormModel from 'sentry/components/forms/model';
47
import {
58
StickyFooter,
69
StickyFooterLabel,
@@ -16,10 +19,11 @@ import {makeAutomationBasePathname} from 'sentry/views/automations/pathnames';
1619
export default function AutomationNewSettings() {
1720
const organization = useOrganization();
1821
useWorkflowEngineFeatureGate({redirect: true});
22+
const model = useMemo(() => new FormModel(), []);
1923

2024
return (
2125
<NewAutomationLayout>
22-
<AutomationForm />
26+
<AutomationForm model={model} />
2327
<StickyFooter>
2428
<StickyFooterLabel>{t('Step 2 of 2')}</StickyFooterLabel>
2529
<Flex gap={space(1)}>

static/app/views/automations/new.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {useState} from 'react';
12
import styled from '@emotion/styled';
23

34
import {Flex} from 'sentry/components/container/flex';
@@ -22,40 +23,51 @@ export default function AutomationNew() {
2223
const organization = useOrganization();
2324
useWorkflowEngineFeatureGate({redirect: true});
2425

26+
const storageKey = NEW_AUTOMATION_CONNECTED_IDS_KEY;
27+
const [connectedIds, setConnectedIds] = useState<Set<string>>(
28+
new Set(JSON.parse(localStorage.getItem(storageKey) || '[]'))
29+
);
30+
const saveConnectedIds = () => {
31+
localStorage.setItem(storageKey, JSON.stringify(Array.from(connectedIds)));
32+
};
33+
const clearConnectedIds = () => {
34+
localStorage.removeItem(storageKey);
35+
};
36+
2537
return (
2638
<NewAutomationLayout>
2739
<ContentWrapper>
2840
<Flex column gap={space(1.5)} style={{padding: space(2)}}>
2941
<Card>
30-
<EditConnectedMonitors storageKey={NEW_AUTOMATION_CONNECTED_IDS_KEY} />
42+
<EditConnectedMonitors
43+
connectedIds={connectedIds}
44+
setConnectedIds={setConnectedIds}
45+
/>
3146
</Card>
3247
<span>
3348
<Button icon={<IconAdd />}>{t('Create New Monitor')}</Button>
3449
</span>
3550
</Flex>
3651
</ContentWrapper>
37-
<StyledStickyFooter>
52+
<StickyFooter>
3853
<StickyFooterLabel>{t('Step 1 of 2')}</StickyFooterLabel>
3954
<Flex gap={space(1)}>
4055
<LinkButton
4156
priority="default"
4257
to={makeAutomationBasePathname(organization.slug)}
58+
onClick={clearConnectedIds}
4359
>
4460
{t('Cancel')}
4561
</LinkButton>
46-
<LinkButton priority="primary" to="settings">
62+
<LinkButton priority="primary" to="settings" onClick={saveConnectedIds}>
4763
{t('Next')}
4864
</LinkButton>
4965
</Flex>
50-
</StyledStickyFooter>
66+
</StickyFooter>
5167
</NewAutomationLayout>
5268
);
5369
}
5470

5571
const ContentWrapper = styled('div')`
5672
position: relative;
5773
`;
58-
59-
const StyledStickyFooter = styled(StickyFooter)`
60-
z-index: ${p => p.theme.zIndex.initial};
61-
`;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {t} from 'sentry/locale';
22
import AlertStore from 'sentry/stores/alertStore';
33
import type {Detector} from 'sentry/types/workflowEngine/detectors';
4+
import type {ApiQueryKey} from 'sentry/utils/queryClient';
45
import {
56
useApiQueries,
67
useApiQuery,
@@ -22,8 +23,9 @@ export function useDetectorsQuery(_options: UseDetectorsQueryOptions = {}) {
2223
});
2324
}
2425

25-
export const makeDetectorQueryKey = (orgSlug: string, detectorId = ''): [url: string] => [
26+
export const makeDetectorQueryKey = (orgSlug: string, detectorId = ''): ApiQueryKey => [
2627
`/organizations/${orgSlug}/detectors/${detectorId ? `${detectorId}/` : ''}`,
28+
{query: {query: 'type:metric_issue'}}, // TODO: remove this when backend is ready
2729
];
2830

2931
export function useCreateDetector() {

0 commit comments

Comments
 (0)