Browse Source

feat(project-creation): use experimental endpoint (#49895)

Frontend changes to let org members create project using new endpoint:
1. don't show them team selection dropdown (they have none)
2. replace url
Athena Moghaddam 1 year ago
parent
commit
8f1f29d7e7

+ 87 - 2
static/app/views/projectInstall/createProject.spec.tsx

@@ -10,6 +10,7 @@ import {
 import OrganizationStore from 'sentry/stores/organizationStore';
 import TeamStore from 'sentry/stores/teamStore';
 import {Organization} from 'sentry/types';
+import * as useExperiment from 'sentry/utils/useExperiment';
 import {CreateProject} from 'sentry/views/projectInstall/createProject';
 
 function renderFrameworkModalMockRequests({
@@ -84,11 +85,28 @@ describe('CreateProject', function () {
     expect(container).toSnapshot();
   });
 
-  it('can create a new team', async function () {
+  it('can create a new team as admin', async function () {
+    const {organization} = initializeOrg({
+      organization: {
+        access: ['project:admin'],
+      },
+    });
+    renderFrameworkModalMockRequests({organization, teamSlug: 'team-two'});
+    TeamStore.loadUserTeams([
+      TestStubs.Team({id: 2, slug: 'team-two', access: ['team:admin']}),
+    ]);
+
     render(<CreateProject />, {
       context: TestStubs.routerContext([
-        {organization: {id: '1', slug: 'testOrg', access: ['project:read']}},
+        {
+          organization: {
+            id: '1',
+            slug: 'testOrg',
+            access: ['project:read'],
+          },
+        },
       ]),
+      organization,
     });
 
     renderGlobalModal();
@@ -104,6 +122,73 @@ describe('CreateProject', function () {
     await userEvent.click(screen.getByRole('button', {name: 'Close Modal'}));
   });
 
+  it('can create a new project without team as org member', async function () {
+    const {organization} = initializeOrg({
+      organization: {
+        access: ['project:read'],
+        features: ['organizations:team-project-creation-all'],
+      },
+    });
+
+    jest.spyOn(useExperiment, 'useExperiment').mockReturnValue({
+      experimentAssignment: 1,
+      logExperiment: jest.fn(),
+    });
+    renderFrameworkModalMockRequests({organization, teamSlug: 'team-two'});
+    TeamStore.loadUserTeams([TestStubs.Team({id: 2, slug: 'team-two', access: []})]);
+
+    render(<CreateProject />, {
+      context: TestStubs.routerContext([
+        {
+          organization: {
+            id: '1',
+            slug: 'testOrg',
+            access: ['project:read'],
+          },
+        },
+      ]),
+      organization,
+    });
+
+    renderGlobalModal();
+    await userEvent.click(screen.getByTestId('platform-apple-ios'));
+    const createTeamButton = screen.queryByRole('button', {name: 'Create a team'});
+    expect(createTeamButton).not.toBeInTheDocument();
+    expect(screen.getByRole('button', {name: 'Create Project'})).toBeEnabled();
+  });
+
+  it('can create a new team before project creation if org owner', async function () {
+    const {organization} = initializeOrg({
+      organization: {
+        access: ['project:admin'],
+      },
+    });
+
+    render(<CreateProject />, {
+      context: TestStubs.routerContext([
+        {
+          organization: {
+            id: '1',
+            slug: 'testOrg',
+            access: ['project:read'],
+          },
+        },
+      ]),
+      organization,
+    });
+
+    renderGlobalModal();
+    await userEvent.click(screen.getByRole('button', {name: 'Create a team'}));
+
+    expect(
+      await screen.findByText(
+        'Members of a team have access to specific areas, such as a new release or a new application feature.'
+      )
+    ).toBeInTheDocument();
+
+    await userEvent.click(screen.getByRole('button', {name: 'Close Modal'}));
+  });
+
   it('should only allow teams which the user is a team-admin', async function () {
     const organization = TestStubs.Organization();
     renderFrameworkModalMockRequests({organization, teamSlug: 'team-two'});

+ 48 - 38
static/app/views/projectInstall/createProject.tsx

@@ -85,14 +85,19 @@ function CreateProject() {
       setInFlight(true);
 
       try {
-        const projectData = await api.requestPromise(`/teams/${slug}/${team}/projects/`, {
-          method: 'POST',
-          data: {
-            name: projectName,
-            platform: selectedPlatform,
-            default_rules: defaultRules ?? true,
-          },
-        });
+        const projectData = await api.requestPromise(
+          team === null
+            ? `/organizations/${slug}/experimental/projects/`
+            : `/teams/${slug}/${team}/projects/`,
+          {
+            method: 'POST',
+            data: {
+              name: projectName,
+              platform: selectedPlatform,
+              default_rules: defaultRules ?? true,
+            },
+          }
+        );
 
         let ruleId: string | undefined;
         if (shouldCreateCustomRule) {
@@ -214,9 +219,12 @@ function CreateProject() {
   const {shouldCreateCustomRule, conditions} = alertRuleConfig || {};
   const {canCreateProject} = useProjectCreationAccess({organization, teams: accessTeams});
 
+  const isOrgMemberWithNoAccess =
+    accessTeams.length === 0 && !organization.access.includes('project:admin');
+
   const canSubmitForm =
     !inFlight &&
-    team &&
+    (team || isOrgMemberWithNoAccess) &&
     canCreateProject &&
     projectName !== '' &&
     (!shouldCreateCustomRule || conditions?.every?.(condition => condition.value));
@@ -247,34 +255,36 @@ function CreateProject() {
             />
           </ProjectNameInputWrap>
         </div>
-        <div>
-          <FormLabel>{t('Team')}</FormLabel>
-          <TeamSelectInput>
-            <TeamSelector
-              name="select-team"
-              aria-label={t('Select a Team')}
-              menuPlacement="auto"
-              clearable={false}
-              value={team}
-              placeholder={t('Select a Team')}
-              onChange={choice => setTeam(choice.value)}
-              teamFilter={(tm: Team) => tm.access.includes('team:admin')}
-            />
-            <Button
-              borderless
-              data-test-id="create-team"
-              icon={<IconAdd isCircled />}
-              onClick={() =>
-                openCreateTeamModal({
-                  organization,
-                  onClose: ({slug}) => setTeam(slug),
-                })
-              }
-              title={t('Create a team')}
-              aria-label={t('Create a team')}
-            />
-          </TeamSelectInput>
-        </div>
+        {!isOrgMemberWithNoAccess && (
+          <div>
+            <FormLabel>{t('Team')}</FormLabel>
+            <TeamSelectInput>
+              <TeamSelector
+                name="select-team"
+                aria-label={t('Select a Team')}
+                menuPlacement="auto"
+                clearable={false}
+                value={team}
+                placeholder={t('Select a Team')}
+                onChange={choice => setTeam(choice.value)}
+                teamFilter={(tm: Team) => tm.access.includes('team:admin')}
+              />
+              <Button
+                borderless
+                data-test-id="create-team"
+                icon={<IconAdd isCircled />}
+                onClick={() =>
+                  openCreateTeamModal({
+                    organization,
+                    onClose: ({slug}) => setTeam(slug),
+                  })
+                }
+                title={t('Create a team')}
+                aria-label={t('Create a team')}
+              />
+            </TeamSelectInput>
+          </div>
+        )}
         <div>
           <Button
             type="submit"
@@ -290,7 +300,7 @@ function CreateProject() {
   );
 
   return (
-    <Access access={canCreateProject ? ['project:read'] : ['project:write']}>
+    <Access access={canCreateProject ? ['project:read'] : ['project:admin']}>
       {error && <Alert type="error">{error}</Alert>}
       <div data-test-id="onboarding-info">
         <Layout.Title withMargins>{t('Create a new project in 3 steps')}</Layout.Title>