Skip to content

Commit f5dcbe4

Browse files
authored
feat(alerts): New project issue alert options (#16697)
* Add frontend work (Failing) Tests need updating * Refactor feature flag to parent * Add additional checks for disabling submit form button * Refactor code * Wrap strings for translation * Let issueAlertOptions be organization sensitive Endpoint now requires org id * Remove snapshot * Fix bugs and add tests * Address comments * Fix spacing * Add snapshot * Fix controlled input being initially uncontrolled * Make radioline have equal height * Address comments * Remove redundant !important
1 parent e3508d3 commit f5dcbe4

File tree

9 files changed

+2131
-1213
lines changed

9 files changed

+2131
-1213
lines changed

src/sentry/static/sentry/app/views/projectInstall/createProject.jsx

+198-98
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import space from 'app/styles/space';
2121
import withApi from 'app/utils/withApi';
2222
import withOrganization from 'app/utils/withOrganization';
2323
import withTeams from 'app/utils/withTeams';
24+
import IssueAlertOptions from 'app/views/projectInstall/issueAlertOptions';
2425

2526
class CreateProject extends React.Component {
2627
static propTypes = {
2728
api: PropTypes.object,
2829
teams: PropTypes.arrayOf(SentryTypes.Team),
2930
organization: SentryTypes.Organization,
31+
hasIssueAlertOptionsEnabled: PropTypes.bool,
3032
};
3133

3234
static contextTypes = {
@@ -50,13 +52,126 @@ class CreateProject extends React.Component {
5052
platform,
5153
inFlight: false,
5254
};
55+
56+
if (this.props.hasIssueAlertOptionsEnabled) {
57+
this.state = {
58+
...this.state,
59+
...{
60+
dataFragment: {},
61+
},
62+
};
63+
}
5364
}
5465

55-
createProject = e => {
66+
renderProjectForm = (
67+
projectName,
68+
team,
69+
teams,
70+
platform,
71+
hasIssueAlertOptionsEnabled,
72+
organization,
73+
canSubmitForm
74+
) => {
75+
const createProjectFormCaptured = (
76+
<CreateProjectForm onSubmit={this.createProject}>
77+
<div>
78+
<FormLabel>
79+
{hasIssueAlertOptionsEnabled
80+
? t('Project name')
81+
: t('Give your project a name')}
82+
</FormLabel>
83+
<ProjectNameInput>
84+
<ProjectPlatformIcon monoTone platform={platform} />
85+
<input
86+
type="text"
87+
name="name"
88+
label={t('Project Name')}
89+
placeholder={t('Project name')}
90+
autoComplete="off"
91+
value={projectName}
92+
onChange={e => this.setState({projectName: e.target.value})}
93+
/>
94+
</ProjectNameInput>
95+
</div>
96+
<div>
97+
<FormLabel>
98+
{hasIssueAlertOptionsEnabled ? t('Name') : t('Assign a Team')}
99+
</FormLabel>
100+
<TeamSelectInput>
101+
<SelectControl
102+
deprecatedSelectControl
103+
name="select-team"
104+
clearable={false}
105+
value={team}
106+
placeholder={t('Select a Team')}
107+
onChange={choice => this.setState({team: choice.value})}
108+
options={teams.map(({slug}) => ({
109+
label: `#${slug}`,
110+
value: slug,
111+
}))}
112+
/>
113+
<Tooltip title={t('Create a team')}>
114+
<Button
115+
borderless
116+
data-test-id="create-team"
117+
type="button"
118+
icon="icon-circle-add"
119+
onClick={() =>
120+
openCreateTeamModal({
121+
organization,
122+
onClose: ({slug}) => this.setState({team: slug}),
123+
})
124+
}
125+
/>
126+
</Tooltip>
127+
</TeamSelectInput>
128+
</div>
129+
<div>
130+
<Button
131+
data-test-id="create-project"
132+
priority="primary"
133+
disabled={!canSubmitForm}
134+
>
135+
{t('Create Project')}
136+
</Button>
137+
</div>
138+
</CreateProjectForm>
139+
);
140+
return hasIssueAlertOptionsEnabled ? (
141+
<React.Fragment>
142+
<PageHeading withMargins>{t('Give your project a name')}</PageHeading>
143+
{createProjectFormCaptured}
144+
</React.Fragment>
145+
) : (
146+
<StickyWrapper>{createProjectFormCaptured}</StickyWrapper>
147+
);
148+
};
149+
150+
canSubmitForm(inFlight, team, projectName, dataFragment, hasIssueAlertOptionsEnabled) {
151+
return (
152+
!inFlight &&
153+
team &&
154+
projectName !== '' &&
155+
(!hasIssueAlertOptionsEnabled ||
156+
!dataFragment?.shouldCreateCustomRule ||
157+
dataFragment?.conditions?.every?.(condition => condition.value))
158+
);
159+
}
160+
161+
createProject = async e => {
56162
e.preventDefault();
57-
const {organization, api} = this.props;
58-
const {projectName, platform, team} = this.state;
163+
const {organization, api, hasIssueAlertOptionsEnabled} = this.props;
164+
const {projectName, platform, team, dataFragment} = this.state;
59165
const {slug} = organization;
166+
const {
167+
shouldCreateCustomRule,
168+
name,
169+
conditions,
170+
actions,
171+
actionMatch,
172+
frequency,
173+
defaultRules,
174+
} = hasIssueAlertOptionsEnabled ? dataFragment : {};
60175

61176
this.setState({inFlight: true});
62177

@@ -68,39 +183,53 @@ class CreateProject extends React.Component {
68183
});
69184
}
70185

71-
api.request(`/teams/${slug}/${team}/projects/`, {
72-
method: 'POST',
73-
data: {
74-
name: projectName,
75-
platform,
76-
},
77-
success: data => {
78-
ProjectActions.createSuccess(data);
79-
80-
const platformKey = platform || 'other';
81-
const nextUrl = `/${organization.slug}/${data.slug}/getting-started/${platformKey}/`;
82-
83-
browserHistory.push(nextUrl);
84-
},
85-
error: err => {
86-
this.setState({
87-
inFlight: false,
88-
error: err.responseJSON.detail,
89-
});
186+
try {
187+
const projectData = await api.requestPromise(`/teams/${slug}/${team}/projects/`, {
188+
method: 'POST',
189+
data: {
190+
name: projectName,
191+
platform,
192+
default_rules: defaultRules ?? true,
193+
},
194+
});
195+
196+
if (shouldCreateCustomRule) {
197+
await api.requestPromise(
198+
`/projects/${organization.slug}/${projectData.slug}/rules/`,
199+
{
200+
method: 'POST',
201+
data: {
202+
name,
203+
conditions,
204+
actions,
205+
actionMatch,
206+
frequency,
207+
},
208+
}
209+
);
210+
}
211+
ProjectActions.createSuccess(projectData);
212+
const platformKey = platform || 'other';
213+
const nextUrl = `/${organization.slug}/${projectData.slug}/getting-started/${platformKey}/`;
214+
browserHistory.push(nextUrl);
215+
} catch (err) {
216+
this.setState({
217+
inFlight: false,
218+
error: err.responseJSON.detail,
219+
});
90220

91-
// Only log this if the error is something other than:
92-
// * The user not having access to create a project, or,
93-
// * A project with that slug already exists
94-
if (err.status !== 403 && err.status !== 409) {
95-
Sentry.withScope(scope => {
96-
scope.setExtra('err', err);
97-
scope.setExtra('props', this.props);
98-
scope.setExtra('state', this.state);
99-
Sentry.captureMessage('Project creation failed');
100-
});
101-
}
102-
},
103-
});
221+
// Only log this if the error is something other than:
222+
// * The user not having access to create a project, or,
223+
// * A project with that slug already exists
224+
if (err.status !== 403 && err.status !== 409) {
225+
Sentry.withScope(scope => {
226+
scope.setExtra('err', err);
227+
scope.setExtra('props', this.props);
228+
scope.setExtra('state', this.state);
229+
Sentry.captureMessage('Project creation failed');
230+
});
231+
}
232+
}
104233
};
105234

106235
setPlatform = platformId =>
@@ -113,10 +242,16 @@ class CreateProject extends React.Component {
113242
}));
114243

115244
render() {
116-
const {organization} = this.props;
117-
const {projectName, team, platform, error, inFlight} = this.state;
245+
const {organization, hasIssueAlertOptionsEnabled} = this.props;
246+
const {projectName, team, platform, error, inFlight, dataFragment} = this.state;
118247
const teams = this.props.teams.filter(filterTeam => filterTeam.hasAccess);
119-
248+
const canSubmitForm = this.canSubmitForm(
249+
inFlight,
250+
team,
251+
projectName,
252+
dataFragment,
253+
hasIssueAlertOptionsEnabled
254+
);
120255
return (
121256
<React.Fragment>
122257
{error && <Alert type="error">{error}</Alert>}
@@ -130,65 +265,26 @@ class CreateProject extends React.Component {
130265
for your API server and frontend client.`
131266
)}
132267
</HelpText>
133-
268+
{hasIssueAlertOptionsEnabled && (
269+
<PageHeading withMargins>{t('Choose a platform')}</PageHeading>
270+
)}
134271
<PlatformPicker platform={platform} setPlatform={this.setPlatform} showOther />
135-
<CreateProjectForm onSubmit={this.createProject}>
136-
<div>
137-
<FormLabel>{t('Give your project a name')}</FormLabel>
138-
<ProjectNameInput>
139-
<ProjectPlatformIcon monoTone platform={platform} />
140-
<input
141-
type="text"
142-
name="name"
143-
label={t('Project Name')}
144-
placeholder={t('Project name')}
145-
autoComplete="off"
146-
value={projectName}
147-
onChange={e => this.setState({projectName: e.target.value})}
148-
/>
149-
</ProjectNameInput>
150-
</div>
151-
<div>
152-
<FormLabel>{t('Assign a Team')}</FormLabel>
153-
<TeamSelectInput>
154-
<SelectControl
155-
deprecatedSelectControl
156-
name="select-team"
157-
clearable={false}
158-
value={team}
159-
placeholder={t('Select a Team')}
160-
onChange={choice => this.setState({team: choice.value})}
161-
options={teams.map(({slug}) => ({
162-
label: `#${slug}`,
163-
value: slug,
164-
}))}
165-
/>
166-
<Tooltip title={t('Create a team')}>
167-
<Button
168-
borderless
169-
data-test-id="create-team"
170-
type="button"
171-
icon="icon-circle-add"
172-
onClick={() =>
173-
openCreateTeamModal({
174-
organization,
175-
onClose: ({slug}) => this.setState({team: slug}),
176-
})
177-
}
178-
/>
179-
</Tooltip>
180-
</TeamSelectInput>
181-
</div>
182-
<div>
183-
<Button
184-
data-test-id="create-project"
185-
priority="primary"
186-
disabled={inFlight || !team || projectName === ''}
187-
>
188-
{t('Create Project')}
189-
</Button>
190-
</div>
191-
</CreateProjectForm>
272+
{hasIssueAlertOptionsEnabled && (
273+
<IssueAlertOptions
274+
onChange={updatedData => {
275+
this.setState({dataFragment: updatedData});
276+
}}
277+
/>
278+
)}
279+
{this.renderProjectForm(
280+
projectName,
281+
team,
282+
teams,
283+
platform,
284+
hasIssueAlertOptionsEnabled,
285+
organization,
286+
canSubmitForm
287+
)}
192288
</div>
193289
</React.Fragment>
194290
);
@@ -206,8 +302,6 @@ const CreateProjectForm = styled('form')`
206302
padding: ${space(3)} 0;
207303
box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.1);
208304
background: #fff;
209-
position: sticky;
210-
bottom: 0;
211305
`;
212306

213307
const FormLabel = styled('div')`
@@ -243,3 +337,9 @@ const HelpText = styled('p')`
243337
color: ${p => p.theme.gray3};
244338
max-width: 700px;
245339
`;
340+
341+
const StickyWrapper = styled('div')`
342+
position: sticky;
343+
background: #fff;
344+
bottom: 0;
345+
`;

0 commit comments

Comments
 (0)