Skip to content

Commit a8988e6

Browse files
fix(project-creation): Button should be busy until all mutations are complete
1 parent 43886d9 commit a8988e6

File tree

5 files changed

+142
-69
lines changed

5 files changed

+142
-69
lines changed

static/app/components/onboarding/frameworkSuggestionModal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {Radio} from 'sentry/components/core/radio';
1212
import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup';
1313
import List from 'sentry/components/list';
1414
import ListItem from 'sentry/components/list/listItem';
15-
import {useIsCreatingProject} from 'sentry/components/onboarding/useCreateProject';
15+
import {useIsCreatingProjectAndRules} from 'sentry/components/onboarding/useCreateProjectAndRules';
1616
import Panel from 'sentry/components/panels/panel';
1717
import PanelBody from 'sentry/components/panels/panelBody';
1818
import categoryList, {createablePlatforms} from 'sentry/data/platformPickerCategories';
@@ -133,7 +133,8 @@ export function FrameworkSuggestionModal({
133133
organization,
134134
newOrg,
135135
}: FrameworkSuggestionModalProps) {
136-
const isCreatingProject = useIsCreatingProject();
136+
const isCreatingProjectAndRules = useIsCreatingProjectAndRules();
137+
137138
const [selectedFramework, setSelectedFramework] = useState<
138139
OnboardingSelectedSDK | undefined
139140
>(selectedPlatform);
@@ -306,7 +307,11 @@ export function FrameworkSuggestionModal({
306307
</StyledPanel>
307308
</Body>
308309
<Footer>
309-
<Button priority="primary" onClick={debounceHandleClick} busy={isCreatingProject}>
310+
<Button
311+
priority="primary"
312+
onClick={debounceHandleClick}
313+
busy={isCreatingProjectAndRules}
314+
>
310315
{t('Configure SDK')}
311316
</Button>
312317
</Footer>

static/app/components/onboarding/useCreateProject.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import ProjectsStore from 'sentry/stores/projectsStore';
22
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
33
import type {Project} from 'sentry/types/project';
4-
import {useIsMutating, useMutation} from 'sentry/utils/queryClient';
4+
import {useMutation} from 'sentry/utils/queryClient';
55
import type RequestError from 'sentry/utils/requestError/requestError';
66
import useApi from 'sentry/utils/useApi';
77
import useOrganization from 'sentry/utils/useOrganization';
88

9-
const MUTATION_KEY = 'create-project';
10-
119
interface Variables {
1210
platform: OnboardingSelectedSDK;
1311
default_rules?: boolean;
@@ -20,7 +18,6 @@ export function useCreateProject() {
2018
const organization = useOrganization();
2119

2220
return useMutation<Project, RequestError, Variables>({
23-
mutationKey: [MUTATION_KEY],
2421
mutationFn: ({firstTeamSlug, name, platform, default_rules}) => {
2522
return api.requestPromise(
2623
firstTeamSlug
@@ -42,7 +39,3 @@ export function useCreateProject() {
4239
},
4340
});
4441
}
45-
46-
export function useIsCreatingProject() {
47-
return Boolean(useIsMutating({mutationKey: [MUTATION_KEY]}));
48-
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {useCreateProject} from 'sentry/components/onboarding/useCreateProject';
2+
import {useCreateProjectRules} from 'sentry/components/onboarding/useCreateProjectRules';
3+
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
4+
import {defined} from 'sentry/utils';
5+
import {useIsMutating, useMutation} from 'sentry/utils/queryClient';
6+
import type {useCreateNotificationAction} from 'sentry/views/projectInstall/issueAlertNotificationOptions';
7+
import type {RequestDataFragment} from 'sentry/views/projectInstall/issueAlertOptions';
8+
9+
const MUTATION_KEY = 'create-project-and-rules';
10+
11+
export function useCreateProjectAndRules() {
12+
const createProject = useCreateProject();
13+
const createProjectRules = useCreateProjectRules();
14+
15+
return useMutation({
16+
mutationKey: [MUTATION_KEY],
17+
mutationFn: async ({
18+
projectName,
19+
platform,
20+
alertRuleConfig,
21+
team,
22+
createNotificationAction,
23+
}: {
24+
alertRuleConfig: Partial<RequestDataFragment>;
25+
createNotificationAction: ReturnType<
26+
typeof useCreateNotificationAction
27+
>['createNotificationAction'];
28+
platform: OnboardingSelectedSDK;
29+
projectName: string;
30+
team?: string;
31+
}) => {
32+
const project = await createProject.mutateAsync({
33+
name: projectName,
34+
platform,
35+
default_rules: alertRuleConfig?.defaultRules ?? true,
36+
firstTeamSlug: team,
37+
});
38+
39+
const ruleIds = [];
40+
41+
if (alertRuleConfig?.shouldCreateCustomRule) {
42+
const ruleData = await createProjectRules.mutateAsync({
43+
projectSlug: project.slug,
44+
name: project.name,
45+
conditions: alertRuleConfig?.conditions,
46+
actions: alertRuleConfig?.actions,
47+
actionMatch: alertRuleConfig?.actionMatch,
48+
frequency: alertRuleConfig?.frequency,
49+
});
50+
51+
ruleIds.push(ruleData.id);
52+
}
53+
54+
const notificationRule = await createNotificationAction({
55+
shouldCreateRule: alertRuleConfig?.shouldCreateRule,
56+
name: project.name,
57+
projectSlug: project.slug,
58+
conditions: alertRuleConfig?.conditions,
59+
actionMatch: alertRuleConfig?.actionMatch,
60+
frequency: alertRuleConfig?.frequency,
61+
});
62+
63+
ruleIds.push(notificationRule?.id);
64+
65+
return {project, ruleIds: ruleIds.filter(defined)};
66+
},
67+
});
68+
}
69+
70+
export function useIsCreatingProjectAndRules() {
71+
return Boolean(useIsMutating({mutationKey: [MUTATION_KEY]}));
72+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type {Project} from 'sentry/types/project';
2+
import {useMutation} from 'sentry/utils/queryClient';
3+
import type RequestError from 'sentry/utils/requestError/requestError';
4+
import useApi from 'sentry/utils/useApi';
5+
import useOrganization from 'sentry/utils/useOrganization';
6+
import type {RequestDataFragment} from 'sentry/views/projectInstall/issueAlertOptions';
7+
8+
interface Variables
9+
extends Partial<
10+
Pick<
11+
RequestDataFragment,
12+
'conditions' | 'actions' | 'actionMatch' | 'frequency' | 'name'
13+
>
14+
> {
15+
projectSlug: string;
16+
}
17+
18+
export function useCreateProjectRules() {
19+
const api = useApi();
20+
const organization = useOrganization();
21+
return useMutation<Project, RequestError, Variables>({
22+
mutationFn: ({projectSlug, name, conditions, actions, actionMatch, frequency}) => {
23+
return api.requestPromise(`/projects/${organization.slug}/${projectSlug}/rules/`, {
24+
method: 'POST',
25+
data: {
26+
name,
27+
conditions,
28+
actions,
29+
actionMatch,
30+
frequency,
31+
},
32+
});
33+
},
34+
});
35+
}

static/app/views/projectInstall/createProject.tsx

Lines changed: 26 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useCallback, useEffect, useMemo, useState} from 'react';
22
import styled from '@emotion/styled';
33
import * as Sentry from '@sentry/react';
4+
import debounce from 'lodash/debounce';
45
import omit from 'lodash/omit';
56
import startCase from 'lodash/startCase';
67
import {PlatformIcon} from 'platformicons';
@@ -18,7 +19,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
1819
import List from 'sentry/components/list';
1920
import ListItem from 'sentry/components/list/listItem';
2021
import {SupportedLanguages} from 'sentry/components/onboarding/frameworkSuggestionModal';
21-
import {useCreateProject} from 'sentry/components/onboarding/useCreateProject';
22+
import {useCreateProjectAndRules} from 'sentry/components/onboarding/useCreateProjectAndRules';
2223
import type {Platform} from 'sentry/components/platformPicker';
2324
import PlatformPicker from 'sentry/components/platformPicker';
2425
import TeamSelector from 'sentry/components/teamSelector';
@@ -139,7 +140,7 @@ export function CreateProject() {
139140
const location = useLocation();
140141
const {createNotificationAction, notificationProps} = useCreateNotificationAction();
141142
const canUserCreateProject = useCanCreateProject();
142-
const createProject = useCreateProject();
143+
const createProjectAndRules = useCreateProjectAndRules();
143144
const {teams} = useTeams();
144145
const accessTeams = teams.filter((team: Team) => team.access.includes('team:admin'));
145146
const referrer = decodeScalar(location.query.referrer);
@@ -149,49 +150,6 @@ export function CreateProject() {
149150
null
150151
);
151152

152-
const createRules = useCallback(
153-
async ({
154-
project,
155-
alertRuleConfig,
156-
}: {project: Project} & Pick<FormData, 'alertRuleConfig'>) => {
157-
const ruleIds = [];
158-
159-
if (alertRuleConfig?.shouldCreateCustomRule) {
160-
const ruleData = await api.requestPromise(
161-
`/projects/${organization.slug}/${project.slug}/rules/`,
162-
{
163-
method: 'POST',
164-
data: {
165-
name: project.name,
166-
conditions: alertRuleConfig?.conditions,
167-
actions: alertRuleConfig?.actions,
168-
actionMatch: alertRuleConfig?.actionMatch,
169-
frequency: alertRuleConfig?.frequency,
170-
},
171-
}
172-
);
173-
174-
ruleIds.push(ruleData.id);
175-
}
176-
177-
const notificationRule = await createNotificationAction({
178-
shouldCreateRule: alertRuleConfig?.shouldCreateRule,
179-
name: project.name,
180-
projectSlug: project.slug,
181-
conditions: alertRuleConfig?.conditions,
182-
actionMatch: alertRuleConfig?.actionMatch,
183-
frequency: alertRuleConfig?.frequency,
184-
});
185-
186-
if (notificationRule) {
187-
ruleIds.push(notificationRule.id);
188-
}
189-
190-
return ruleIds;
191-
},
192-
[organization, api, createNotificationAction]
193-
);
194-
195153
const autoFill = useMemo(() => {
196154
return referrer === 'getting-started' && projectId === createdProject?.id;
197155
}, [referrer, projectId, createdProject?.id]);
@@ -238,9 +196,6 @@ export function CreateProject() {
238196
missingValues.isMissingMessagingIntegrationChannel,
239197
].filter(value => value).length;
240198

241-
const canSubmitForm =
242-
!createProject.isPending && canUserCreateProject && formErrorCount === 0;
243-
244199
const submitTooltipText = getSubmitTooltipText({
245200
...missingValues,
246201
formErrorCount,
@@ -287,17 +242,15 @@ export function CreateProject() {
287242
let projectToRollback: Project | undefined;
288243

289244
try {
290-
const project = await createProject.mutateAsync({
291-
name: projectName,
245+
const {project, ruleIds} = await createProjectAndRules.mutateAsync({
246+
projectName,
292247
platform: selectedPlatform,
293-
default_rules: alertRuleConfig?.defaultRules ?? true,
294-
firstTeamSlug: team,
248+
team,
249+
alertRuleConfig,
250+
createNotificationAction,
295251
});
296-
297252
projectToRollback = project;
298253

299-
const ruleIds = await createRules({project, alertRuleConfig});
300-
301254
trackAnalytics('project_creation_page.created', {
302255
organization,
303256
issue_alert: alertRuleConfig?.defaultRules
@@ -376,11 +329,20 @@ export function CreateProject() {
376329
}
377330
}
378331
},
379-
[createRules, organization, createProject, setCreatedProject, navigate, api]
332+
[
333+
organization,
334+
setCreatedProject,
335+
navigate,
336+
api,
337+
createProjectAndRules,
338+
createNotificationAction,
339+
]
380340
);
381341

382342
const handleProjectCreation = useCallback(
383343
async (data: FormData) => {
344+
setErrors(undefined);
345+
384346
const selectedPlatform = data.platform;
385347

386348
if (!isNotPartialPlatform(selectedPlatform)) {
@@ -431,6 +393,11 @@ export function CreateProject() {
431393
[configurePlatform, organization]
432394
);
433395

396+
const debounceHandleProjectCreation = useMemo(
397+
() => debounce(handleProjectCreation, 2000, {leading: true, trailing: false}),
398+
[handleProjectCreation]
399+
);
400+
434401
const handlePlatformChange = useCallback(
435402
(value: Platform | null) => {
436403
if (!value) {
@@ -552,8 +519,9 @@ export function CreateProject() {
552519
<Button
553520
data-test-id="create-project"
554521
priority="primary"
555-
disabled={!canSubmitForm}
556-
onClick={() => handleProjectCreation(formData)}
522+
disabled={!(canUserCreateProject && formErrorCount === 0)}
523+
busy={createProjectAndRules.isPending}
524+
onClick={() => debounceHandleProjectCreation(formData)}
557525
>
558526
{t('Create Project')}
559527
</Button>

0 commit comments

Comments
 (0)