Skip to content

Commit ccd66cb

Browse files
fix(project-creation): Button should be busy until all mutations are complete
1 parent 4437b77 commit ccd66cb

File tree

4 files changed

+107
-66
lines changed

4 files changed

+107
-66
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+
}

static/app/views/projectInstall/createProject.tsx

Lines changed: 26 additions & 55 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,8 +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 {useCreateProjectRules} from 'sentry/components/onboarding/useCreateProjectRules';
22+
import {useCreateProjectAndRules} from 'sentry/components/onboarding/useCreateProjectAndRules';
2323
import type {Platform} from 'sentry/components/platformPicker';
2424
import PlatformPicker from 'sentry/components/platformPicker';
2525
import TeamSelector from 'sentry/components/teamSelector';
@@ -140,8 +140,7 @@ export function CreateProject() {
140140
const location = useLocation();
141141
const {createNotificationAction, notificationProps} = useCreateNotificationAction();
142142
const canUserCreateProject = useCanCreateProject();
143-
const createProject = useCreateProject();
144-
const createProjectRules = useCreateProjectRules();
143+
const createProjectAndRules = useCreateProjectAndRules();
145144
const {teams} = useTeams();
146145
const accessTeams = teams.filter((team: Team) => team.access.includes('team:admin'));
147146
const referrer = decodeScalar(location.query.referrer);
@@ -151,44 +150,6 @@ export function CreateProject() {
151150
null
152151
);
153152

154-
const createRules = useCallback(
155-
async ({
156-
project,
157-
alertRuleConfig,
158-
}: {project: Project} & Pick<FormData, 'alertRuleConfig'>) => {
159-
const ruleIds = [];
160-
161-
if (alertRuleConfig?.shouldCreateCustomRule) {
162-
const ruleData = await createProjectRules.mutateAsync({
163-
projectSlug: project.slug,
164-
name: project.name,
165-
conditions: alertRuleConfig?.conditions,
166-
actions: alertRuleConfig?.actions,
167-
actionMatch: alertRuleConfig?.actionMatch,
168-
frequency: alertRuleConfig?.frequency,
169-
});
170-
171-
ruleIds.push(ruleData.id);
172-
}
173-
174-
const notificationRule = await createNotificationAction({
175-
shouldCreateRule: alertRuleConfig?.shouldCreateRule,
176-
name: project.name,
177-
projectSlug: project.slug,
178-
conditions: alertRuleConfig?.conditions,
179-
actionMatch: alertRuleConfig?.actionMatch,
180-
frequency: alertRuleConfig?.frequency,
181-
});
182-
183-
if (notificationRule) {
184-
ruleIds.push(notificationRule.id);
185-
}
186-
187-
return ruleIds;
188-
},
189-
[createNotificationAction, createProjectRules]
190-
);
191-
192153
const autoFill = useMemo(() => {
193154
return referrer === 'getting-started' && projectId === createdProject?.id;
194155
}, [referrer, projectId, createdProject?.id]);
@@ -235,9 +196,6 @@ export function CreateProject() {
235196
missingValues.isMissingMessagingIntegrationChannel,
236197
].filter(value => value).length;
237198

238-
const canSubmitForm =
239-
!createProject.isPending && canUserCreateProject && formErrorCount === 0;
240-
241199
const submitTooltipText = getSubmitTooltipText({
242200
...missingValues,
243201
formErrorCount,
@@ -284,17 +242,15 @@ export function CreateProject() {
284242
let projectToRollback: Project | undefined;
285243

286244
try {
287-
const project = await createProject.mutateAsync({
288-
name: projectName,
245+
const {project, ruleIds} = await createProjectAndRules.mutateAsync({
246+
projectName,
289247
platform: selectedPlatform,
290-
default_rules: alertRuleConfig?.defaultRules ?? true,
291-
firstTeamSlug: team,
248+
team,
249+
alertRuleConfig,
250+
createNotificationAction,
292251
});
293-
294252
projectToRollback = project;
295253

296-
const ruleIds = await createRules({project, alertRuleConfig});
297-
298254
trackAnalytics('project_creation_page.created', {
299255
organization,
300256
issue_alert: alertRuleConfig?.defaultRules
@@ -373,11 +329,20 @@ export function CreateProject() {
373329
}
374330
}
375331
},
376-
[createRules, organization, createProject, setCreatedProject, navigate, api]
332+
[
333+
organization,
334+
setCreatedProject,
335+
navigate,
336+
api,
337+
createProjectAndRules,
338+
createNotificationAction,
339+
]
377340
);
378341

379342
const handleProjectCreation = useCallback(
380343
async (data: FormData) => {
344+
setErrors(undefined);
345+
381346
const selectedPlatform = data.platform;
382347

383348
if (!isNotPartialPlatform(selectedPlatform)) {
@@ -428,6 +393,11 @@ export function CreateProject() {
428393
[configurePlatform, organization]
429394
);
430395

396+
const debounceHandleProjectCreation = useMemo(
397+
() => debounce(handleProjectCreation, 2000, {leading: true, trailing: false}),
398+
[handleProjectCreation]
399+
);
400+
431401
const handlePlatformChange = useCallback(
432402
(value: Platform | null) => {
433403
if (!value) {
@@ -549,8 +519,9 @@ export function CreateProject() {
549519
<Button
550520
data-test-id="create-project"
551521
priority="primary"
552-
disabled={!canSubmitForm}
553-
onClick={() => handleProjectCreation(formData)}
522+
disabled={!(canUserCreateProject && formErrorCount === 0)}
523+
busy={createProjectAndRules.isPending}
524+
onClick={() => debounceHandleProjectCreation(formData)}
554525
>
555526
{t('Create Project')}
556527
</Button>

0 commit comments

Comments
 (0)