Skip to content

Commit 38f73a8

Browse files
ameliahsuroaga
authored andcommitted
feat(aci): add environment selector to automation creation (#92077)
environment selector groups environments by the user's projects, then by all other projects. dropdown is limited to 10 options shown at a time <img width="684" alt="Screenshot 2025-05-21 at 2 54 17 PM" src="https://github.com/user-attachments/assets/80d3b301-eede-4c04-b15a-905a46de70ee" />
1 parent 1759ff5 commit 38f73a8

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {initializeOrg} from 'sentry-test/initializeOrg';
2+
import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
3+
4+
import {EnvironmentSelector} from 'sentry/components/workflowEngine/form/environmentSelector';
5+
import ProjectsStore from 'sentry/stores/projectsStore';
6+
7+
describe('EnvironmentSelector', function () {
8+
it('renders & handles selection', async function () {
9+
const {projects} = initializeOrg({
10+
projects: [
11+
{id: '1', slug: 'project-1', environments: ['prod', 'staging'], isMember: true},
12+
{id: '2', slug: 'project-2', environments: ['prod', 'stage'], isMember: false},
13+
],
14+
});
15+
ProjectsStore.loadInitialData(projects);
16+
17+
const mockOnChange = jest.fn();
18+
19+
render(<EnvironmentSelector value={''} onChange={mockOnChange} />);
20+
21+
// Open list
22+
await userEvent.click(screen.getByRole('button', {name: 'Environment None'}));
23+
24+
// Get groups
25+
const userProjectEnvironments = screen.getByRole('group', {
26+
name: 'Environments in My Projects',
27+
});
28+
const otherProjectEnvironments = screen.getByRole('group', {
29+
name: 'Other Environments',
30+
});
31+
32+
// Environments are correctly grouped
33+
expect(
34+
within(userProjectEnvironments).getByRole('option', {name: 'prod'})
35+
).toBeInTheDocument();
36+
expect(
37+
within(userProjectEnvironments).getByRole('option', {name: 'staging'})
38+
).toBeInTheDocument();
39+
expect(
40+
within(otherProjectEnvironments).getByRole('option', {name: 'stage'})
41+
).toBeInTheDocument();
42+
// "prod" should not be shown twice
43+
expect(
44+
within(otherProjectEnvironments).queryByRole('option', {name: 'prod'})
45+
).not.toBeInTheDocument();
46+
47+
// Select "prod"
48+
await userEvent.click(screen.getByRole('option', {name: 'prod'}));
49+
50+
// Trigger label is updated
51+
expect(screen.getByRole('button', {name: 'Environment prod'})).toBeInTheDocument();
52+
expect(mockOnChange).toHaveBeenCalledWith('prod');
53+
});
54+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {useMemo} from 'react';
2+
3+
import type {
4+
SelectOption,
5+
SelectOptionOrSection,
6+
} from 'sentry/components/core/compactSelect';
7+
import {CompactSelect} from 'sentry/components/core/compactSelect';
8+
import {t} from 'sentry/locale';
9+
import useProjects from 'sentry/utils/useProjects';
10+
11+
interface EnvironmentSelectorProps {
12+
onChange: (value: string) => void;
13+
value: string;
14+
}
15+
16+
export function EnvironmentSelector({value, onChange}: EnvironmentSelectorProps) {
17+
const {projects, initiallyLoaded: projectsLoaded} = useProjects();
18+
19+
const options = useMemo<Array<SelectOptionOrSection<string>>>(() => {
20+
const userEnvs = new Set<string>();
21+
const otherEnvs = new Set<string>();
22+
23+
projects.forEach(project => {
24+
if (project.isMember) {
25+
project.environments.forEach(env => userEnvs.add(env));
26+
} else {
27+
project.environments.forEach(env => otherEnvs.add(env));
28+
}
29+
});
30+
31+
return [
32+
{
33+
key: 'my-projects',
34+
label: t('Environments in My Projects'),
35+
options: setToOptions(userEnvs),
36+
},
37+
{
38+
key: 'other-projects',
39+
label: t('Other Environments'),
40+
options: setToOptions(otherEnvs.difference(userEnvs)),
41+
},
42+
];
43+
}, [projects]);
44+
45+
return (
46+
<CompactSelect<string>
47+
size="md"
48+
triggerProps={{prefix: t('Environment')}}
49+
options={options}
50+
searchable
51+
disabled={!projectsLoaded}
52+
sizeLimit={10}
53+
multiple={false}
54+
value={value}
55+
onChange={selected => onChange(selected.value)}
56+
/>
57+
);
58+
}
59+
60+
const setToOptions = (set: Set<string>): Array<SelectOption<string>> =>
61+
Array.from(set).map(item => ({
62+
value: item,
63+
label: item,
64+
}));

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import useDrawer from 'sentry/components/globalDrawer';
1212
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
1313
import {useDocumentTitle} from 'sentry/components/sentryDocumentTitle';
1414
import {DebugForm} from 'sentry/components/workflowEngine/form/debug';
15+
import {EnvironmentSelector} from 'sentry/components/workflowEngine/form/environmentSelector';
1516
import {Card} from 'sentry/components/workflowEngine/ui/card';
1617
import {IconAdd, IconEdit} from 'sentry/icons';
1718
import {t} from 'sentry/locale';
@@ -83,6 +84,8 @@ export default function AutomationForm() {
8384
}
8485
};
8586

87+
const [environment, setEnvironment] = useState<string>('');
88+
8689
return (
8790
<Form
8891
hideFooter
@@ -110,6 +113,17 @@ export default function AutomationForm() {
110113
</Button>
111114
</ButtonWrapper>
112115
</Card>
116+
<Card>
117+
<Flex column gap={space(0.5)}>
118+
<Heading>{t('Choose Environment')}</Heading>
119+
<Description>
120+
{t(
121+
'If you select environments different than your monitors then the automation will not fire.'
122+
)}
123+
</Description>
124+
</Flex>
125+
<EnvironmentSelector value={environment} onChange={setEnvironment} />
126+
</Card>
113127
<Card>
114128
<Heading>{t('Automation Builder')}</Heading>
115129
<AutomationBuilder />
@@ -135,6 +149,13 @@ const Heading = styled('h2')`
135149
margin: 0;
136150
`;
137151

152+
const Description = styled('span')`
153+
font-size: ${p => p.theme.fontSizeMedium};
154+
color: ${p => p.theme.subText};
155+
margin: 0;
156+
padding: 0;
157+
`;
158+
138159
const ButtonWrapper = styled(Flex)`
139160
border-top: 1px solid ${p => p.theme.border};
140161
padding: ${space(2)};

0 commit comments

Comments
 (0)