]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20086 - Migrating devops main page
authorKevin Silva <kevin.silva@sonarsource.com>
Fri, 4 Aug 2023 17:11:04 +0000 (19:11 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 14 Aug 2023 20:02:57 +0000 (20:02 +0000)
25 files changed:
server/sonar-web/design-system/src/components/Card.tsx
server/sonar-web/design-system/src/components/Link.tsx
server/sonar-web/design-system/src/components/__tests__/Card-test.tsx
server/sonar-web/public/images/alm/azure_grey.svg [new file with mode: 0644]
server/sonar-web/public/images/alm/bitbucket_grey.svg [new file with mode: 0644]
server/sonar-web/public/images/alm/github_grey.svg [new file with mode: 0644]
server/sonar-web/public/images/alm/gitlab_grey.svg [new file with mode: 0644]
server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts
server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
server/sonar-web/src/main/js/helpers/urls.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 3a00a11b6b2c63a6c398523ecfae6f1cad954799..4ec5254342f1c59301ad32d1cd0138c077fa525f 100644 (file)
@@ -32,6 +32,12 @@ export function Card(props: CardProps) {
   return <CardStyled {...rest}>{children}</CardStyled>;
 }
 
+export function GreyCard(props: CardProps) {
+  const { children, ...rest } = props;
+
+  return <GreyCardStyled {...rest}>{children}</GreyCardStyled>;
+}
+
 const CardStyled = styled.div`
   background-color: ${themeColor('backgroundSecondary')};
   border: ${themeBorder('default', 'projectCardBorder')};
@@ -39,3 +45,7 @@ const CardStyled = styled.div`
   ${tw`sw-p-6`};
   ${tw`sw-rounded-1`};
 `;
+
+const GreyCardStyled = styled(CardStyled)`
+  border: ${themeBorder('default', 'almCardBorder')};
+`;
index 02889e25c5faaf6d0b2232b975b92ad4494dfb76..0ad37abd8522e82a30638c8a8113e923f916e961 100644 (file)
@@ -132,13 +132,13 @@ const StyledBaseLink = styled(BaseLink)`
   ${({ icon }) =>
     icon &&
     css`
-      margin-left: calc(${twTheme('width.icon')} + ${twTheme('spacing.1')});
+      margin-left: calc(${twTheme('width.icon')} + ${twTheme('spacing.3')});
 
       & > svg,
       & > img {
-        ${tw`sw-mr-1`}
+        ${tw`sw-mr-3`}
 
-        margin-left: calc(-1 * (${twTheme('width.icon')} + ${twTheme('spacing.1')}));
+        margin-left: calc(-1 * (${twTheme('width.icon')} + ${twTheme('spacing.3')}));
       }
     `};
 `;
index 5d3c5a28b7aee2c18d547e78f55384c78ccf3d95..c71f6e9a8332d35e09ed85cbf6944fd813407141 100644 (file)
@@ -20,7 +20,7 @@
 
 import { screen } from '@testing-library/react';
 import { render } from '../../helpers/testUtils';
-import { Card } from '../Card';
+import { Card, GreyCard } from '../Card';
 
 it('renders card correctly', () => {
   render(<Card>Hello</Card>);
@@ -41,3 +41,14 @@ it('renders card correctly with classNames', () => {
   expect(cardContent).toHaveClass('sw-bg-black sw-border-8');
   expect(cardContent).toHaveAttribute('role', 'tabpanel');
 });
+
+it('renders grey card correctly with classNames', () => {
+  render(
+    <GreyCard className="sw-bg-black sw-border-8" role="tabpanel">
+      Hello
+    </GreyCard>
+  );
+  const cardContent = screen.getByText('Hello');
+  expect(cardContent).toHaveClass('sw-bg-black sw-border-8');
+  expect(cardContent).toHaveAttribute('role', 'tabpanel');
+});
diff --git a/server/sonar-web/public/images/alm/azure_grey.svg b/server/sonar-web/public/images/alm/azure_grey.svg
new file mode 100644 (file)
index 0000000..44453e2
--- /dev/null
@@ -0,0 +1,15 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1483_60265)">
+<g clip-path="url(#clip1_1483_60265)">
+<path d="M0 5.93225L1.4975 3.95575L7.1015 1.67725V0.03125L12.0155 3.62525L1.9765 5.57325V11.0577L0 10.4872L0 5.93225ZM16 2.96575V12.7337L12.164 15.9992L5.9635 13.9627V15.9992L1.9765 11.0567L12.0155 12.2547V3.62475L16 2.96575Z" fill="#9F9F9F"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_1483_60265">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+<clipPath id="clip1_1483_60265">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/server/sonar-web/public/images/alm/bitbucket_grey.svg b/server/sonar-web/public/images/alm/bitbucket_grey.svg
new file mode 100644 (file)
index 0000000..4ad14c1
--- /dev/null
@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0.124366 1.17313C0.223258 1.0615 0.367814 0.998141 0.519258 1.00004L15.4807 1.00254C15.6322 1.00064 15.7767 1.064 15.8756 1.17562C15.9745 1.28725 16.0176 1.43571 15.9934 1.58119L13.8172 14.5809C13.7766 14.8248 13.5585 15.0031 13.3046 14.9999H2.8646C2.52625 14.9972 2.23875 14.7584 2.18278 14.4337L0.00661524 1.57869C-0.0176304 1.43321 0.0254741 1.28475 0.124366 1.17313ZM6.35057 10.2909H9.68275L10.4902 5.70407H5.44832L6.35057 10.2909Z" fill="#C1C1C1"/>
+<path d="M15.2998 5.90039H10.4458L9.63122 10.3412H6.26937L2.2998 14.741C2.42562 14.8426 2.58603 14.8991 2.75236 14.9003H13.2879C13.5441 14.9034 13.7641 14.7309 13.8051 14.4947L15.2998 5.90039Z" fill="url(#paint0_linear_1483_60281)"/>
+<defs>
+<linearGradient id="paint0_linear_1483_60281" x1="12.1999" y1="4.36721" x2="7.0939" y2="12.1311" gradientUnits="userSpaceOnUse">
+<stop offset="0.18" stop-color="#C8C8C8"/>
+<stop offset="1" stop-color="white"/>
+</linearGradient>
+</defs>
+</svg>
diff --git a/server/sonar-web/public/images/alm/github_grey.svg b/server/sonar-web/public/images/alm/github_grey.svg
new file mode 100644 (file)
index 0000000..b7571df
--- /dev/null
@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1483_60262)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.67055 0 8.20235C0 11.8319 2.29 14.8975 5.47 15.9843C5.87 16.0561 6.02 15.81 6.02 15.5947C6.02 15.3999 6.01 14.754 6.01 14.067C4 14.4464 3.48 13.5646 3.32 13.1033C3.23 12.8674 2.84 12.1395 2.5 11.9447C2.22 11.7909 1.82 11.4115 2.49 11.4013C3.12 11.391 3.57 11.9959 3.72 12.242C4.44 13.4826 5.59 13.134 6.05 12.9187C6.12 12.3855 6.33 12.0267 6.56 11.8216C4.78 11.6166 2.92 10.9091 2.92 7.77173C2.92 6.87972 3.23 6.14151 3.74 5.56735C3.66 5.36229 3.38 4.52155 3.82 3.39372C3.82 3.39372 4.49 3.17841 6.02 4.23446C6.66 4.04991 7.34 3.95763 8.02 3.95763C8.7 3.95763 9.38 4.04991 10.02 4.23446C11.55 3.16816 12.22 3.39372 12.22 3.39372C12.66 4.52155 12.38 5.36229 12.3 5.56735C12.81 6.14151 13.12 6.86947 13.12 7.77173C13.12 10.9194 11.25 11.6166 9.47 11.8216C9.76 12.078 10.01 12.5701 10.01 13.3391C10.01 14.4361 10 15.3179 10 15.5947C10 15.81 10.15 16.0664 10.55 15.9843C13.71 14.8975 16 11.8216 16 8.20235C16 3.67055 12.42 0 8 0Z" fill="#6B7279"/>
+</g>
+<defs>
+<clipPath id="clip0_1483_60262">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/server/sonar-web/public/images/alm/gitlab_grey.svg b/server/sonar-web/public/images/alm/gitlab_grey.svg
new file mode 100644 (file)
index 0000000..2c08696
--- /dev/null
@@ -0,0 +1,13 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1483_60264)">
+<path d="M15.7337 6.09936L15.7112 6.04188L13.5335 0.358462C13.4892 0.24707 13.4107 0.152576 13.3094 0.0885373C13.2079 0.0255869 13.0897 -0.00473207 12.9705 0.00167412C12.8513 0.00808031 12.7369 0.0509032 12.6429 0.124361C12.5498 0.199927 12.4824 0.302323 12.4496 0.417612L10.9792 4.91636H5.025L3.55457 0.417612C3.52268 0.301695 3.45505 0.198786 3.36129 0.123527C3.26722 0.0500699 3.15287 0.00724703 3.03368 0.000840838C2.9145 -0.00556535 2.79622 0.0247536 2.69481 0.087704C2.5937 0.152001 2.51531 0.246413 2.47071 0.357629L0.288816 6.03855L0.267156 6.09603C-0.046338 6.91514 -0.0850337 7.81397 0.156903 8.65699C0.398839 9.50001 0.908292 10.2415 1.60845 10.7697L1.61595 10.7756L1.63594 10.7897L4.95335 13.274L6.59456 14.5162L7.59428 15.271C7.71122 15.3598 7.85401 15.4078 8.00083 15.4078C8.14766 15.4078 8.29045 15.3598 8.40739 15.271L9.40711 14.5162L11.0483 13.274L14.3857 10.7747L14.3941 10.7681C15.0926 10.2398 15.6009 9.49901 15.8425 8.65713C16.084 7.81524 16.0459 6.9177 15.7337 6.09936V6.09936Z" fill="#747474"/>
+<path d="M15.7337 6.09948L15.7112 6.04199C14.6501 6.2598 13.6501 6.70927 12.7828 7.35829L8 10.9748C9.62871 12.2069 11.0467 13.2775 11.0467 13.2775L14.3841 10.7782L14.3924 10.7715C15.092 10.2432 15.601 9.502 15.8429 8.65939C16.0848 7.81679 16.0465 6.91841 15.7337 6.09948Z" fill="#A6A6A6"/>
+<path d="M4.95312 13.2773L6.59434 14.5195L7.59406 15.2742C7.711 15.363 7.85378 15.4111 8.00061 15.4111C8.14743 15.4111 8.29022 15.363 8.40716 15.2742L9.40688 14.5195L11.0481 13.2773C11.0481 13.2773 9.62849 12.2034 7.99978 10.9746C6.37106 12.2034 4.95312 13.2773 4.95312 13.2773Z" fill="#CCCCCC"/>
+<path d="M3.21633 7.35772C2.34974 6.70736 1.35002 6.25672 0.288816 6.03809L0.267156 6.09557C-0.046338 6.91468 -0.0850337 7.81351 0.156903 8.65653C0.398839 9.49955 0.908292 10.2411 1.60845 10.7693L1.61595 10.7751L1.63594 10.7893L4.95335 13.2736C4.95335 13.2736 6.36962 12.203 8 10.9709L3.21633 7.35772Z" fill="#A6A6A6"/>
+</g>
+<defs>
+<clipPath id="clip0_1483_60264">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>
index 6adbdcd7ec8daee434ece1799d141e6228171601..e74bc08e11e38f0c3467728fef2007c2c0a921ee 100644 (file)
@@ -60,8 +60,8 @@ import {
   setupAzureProjectCreation,
   setupBitbucketCloudProjectCreation,
   setupBitbucketServerProjectCreation,
-  setupGitlabProjectCreation,
   setupGithubProjectCreation,
+  setupGitlabProjectCreation,
 } from '../alm-integrations';
 
 export default class AlmIntegrationsServiceMock {
index 864aac3a0f5f70efa4380f5437a3597fa1f90874..0291697e4eb08b5c94d62609fa5bb47d0c1667df 100644 (file)
@@ -193,6 +193,12 @@ export default class AlmSettingsServiceMock {
     return this.reply(undefined);
   };
 
+  removeFromAlmSettings = (almKey: string) => {
+    this.#almSettings = cloneDeep(defaultAlmSettings).filter(
+      (almSetting) => almSetting.alm !== almKey
+    );
+  };
+
   handleCreateGithubConfiguration = (data: GithubBindingDefinition) => {
     this.#almDefinitions[AlmKeys.GitHub].push(data);
 
index a974c4bfba12d6341e6174b4403a3d2f88f6fed5..2346d06ccac4854b23d9147feddb3156afaee0f9 100644 (file)
@@ -50,7 +50,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/web_api_v2',
 ];
 
-const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = ['/tutorials'];
+const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = ['/tutorials', '/projects/create'];
 
 export default function GlobalContainer() {
   // it is important to pass `location` down to `GlobalNav` to trigger render on url change
index 77f69a7edcf50d17c20d535598035d5c95138739..cffe67b5283c459b6b9db1b1b39c25d60d9ca3c8 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 /* eslint-disable react/no-unused-prop-types */
-
-import classNames from 'classnames';
+import {
+  ButtonSecondary,
+  DeferredSpinner,
+  GreyCard,
+  HelperHintIcon,
+  LightPrimary,
+  StandoutLink,
+  TextMuted,
+  Title,
+} from 'design-system';
 import * as React from 'react';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
-import ChevronsIcon from '../../../components/icons/ChevronsIcon';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
+import { getCreateProjectModeLocation } from '../../../helpers/urls';
 import { AlmKeys } from '../../../types/alm-settings';
 import { AppState } from '../../../types/appstate';
 import { CreateProjectModes } from './types';
@@ -35,26 +44,13 @@ export interface CreateProjectModeSelectionProps {
   };
   appState: AppState;
   loadingBindings: boolean;
-  onSelectMode: (mode: CreateProjectModes) => void;
   onConfigMode: (mode: AlmKeys) => void;
 }
 
-const DEFAULT_ICON_SIZE = 50;
-
-function getErrorMessage(hasConfig: boolean, canAdmin: boolean | undefined) {
-  if (!hasConfig) {
-    return canAdmin
-      ? translate('onboarding.create_project.alm_not_configured.admin')
-      : translate('onboarding.create_project.alm_not_configured');
-  }
-  return undefined;
-}
-
 function renderAlmOption(
   props: CreateProjectModeSelectionProps,
   alm: AlmKeys,
-  mode: CreateProjectModes,
-  last = false
+  mode: CreateProjectModes
 ) {
   const {
     almCounts,
@@ -64,58 +60,52 @@ function renderAlmOption(
   const count = almCounts[alm];
   const hasConfig = count > 0;
   const disabled = loadingBindings || (!hasConfig && !canAdmin);
-
-  const onClick = () => {
-    if (!hasConfig && !canAdmin) {
-      return null;
-    }
-
-    if (!hasConfig && canAdmin) {
-      const configMode = alm === AlmKeys.BitbucketCloud ? AlmKeys.BitbucketServer : alm;
-      return props.onConfigMode(configMode);
-    }
-
-    return props.onSelectMode(mode);
-  };
-
-  const errorMessage = getErrorMessage(hasConfig, canAdmin);
+  const configMode = alm === AlmKeys.BitbucketCloud ? AlmKeys.BitbucketServer : alm;
 
   const svgFileName = alm === AlmKeys.BitbucketCloud ? AlmKeys.BitbucketServer : alm;
+  const svgFileNameGrey = `${svgFileName}_grey`;
+
+  const icon = (
+    <img
+      alt="" // Should be ignored by screen readers
+      className="sw-h-4 sw-w-4"
+      src={`${getBaseUrl()}/images/alm/${
+        !disabled && hasConfig ? svgFileName : svgFileNameGrey
+      }.svg`}
+    />
+  );
 
   return (
-    <div className="display-flex-column">
-      <button
-        className={classNames(
-          'button button-huge display-flex-column create-project-mode-type-alm',
-          { disabled, 'big-spacer-right': !last }
-        )}
-        disabled={disabled}
-        onClick={onClick}
-        type="button"
-      >
-        <img
-          alt="" // Should be ignored by screen readers
-          height={DEFAULT_ICON_SIZE}
-          src={`${getBaseUrl()}/images/alm/${svgFileName}.svg`}
-        />
-        <div className="medium big-spacer-top abs-height-50 display-flex-center">
-          {translate('onboarding.create_project.select_method', alm)}
-        </div>
-
-        {loadingBindings && (
-          <span>
-            {translate('onboarding.create_project.check_alm_supported')}
-            <i className="little-spacer-left spinner" />
-          </span>
+    <GreyCard className="sw-col-span-4 sw-p-4 sw-flex sw-justify-between sw-items-center">
+      <div className="sw-items-center sw-flex sw-py-2">
+        {!disabled && hasConfig ? (
+          <StandoutLink icon={icon} to={getCreateProjectModeLocation(mode)}>
+            {translate('onboarding.create_project.import_select_method', alm)}
+          </StandoutLink>
+        ) : (
+          <>
+            {icon}
+            <TextMuted
+              className="sw-ml-3 sw-text-sm sw-font-semibold"
+              text={translate('onboarding.create_project.import_select_method', alm)}
+            />
+          </>
         )}
+      </div>
 
-        {!loadingBindings && errorMessage && (
-          <p className="text-muted small spacer-top" style={{ lineHeight: 1.5 }}>
-            {errorMessage}
-          </p>
-        )}
-      </button>
-    </div>
+      <DeferredSpinner loading={loadingBindings}>
+        {!hasConfig &&
+          (canAdmin ? (
+            <ButtonSecondary onClick={() => props.onConfigMode(configMode)}>
+              {translate('setup')}
+            </ButtonSecondary>
+          ) : (
+            <HelpTooltip overlay={translate('onboarding.create_project.alm_not_configured')}>
+              <HelperHintIcon aria-label="help-tooltip" />
+            </HelpTooltip>
+          ))}
+      </DeferredSpinner>
+    </GreyCard>
   );
 }
 
@@ -127,39 +117,41 @@ export function CreateProjectModeSelection(props: CreateProjectModeSelectionProp
   const almTotalCount = Object.values(almCounts).reduce((prev, cur) => prev + cur);
 
   return (
-    <>
-      <h1 className="huge-spacer-top huge-spacer-bottom">
-        {translate('onboarding.create_project.select_method')}
-      </h1>
-
-      <p>{translate('onboarding.create_project.select_method.devops_platform')}</p>
-      {almTotalCount === 0 && canAdmin && (
-        <p className="spacer-top">
-          {translate('onboarding.create_project.select_method.no_alm_yet.admin')}
-        </p>
-      )}
-      <div className="big-spacer-top huge-spacer-bottom display-flex-center">
-        {renderAlmOption(props, AlmKeys.Azure, CreateProjectModes.AzureDevOps)}
-        {renderAlmOption(props, AlmKeys.BitbucketServer, CreateProjectModes.BitbucketServer)}
-        {renderAlmOption(props, AlmKeys.BitbucketCloud, CreateProjectModes.BitbucketCloud)}
-        {renderAlmOption(props, AlmKeys.GitHub, CreateProjectModes.GitHub)}
-        {renderAlmOption(props, AlmKeys.GitLab, CreateProjectModes.GitLab, true)}
-      </div>
-
-      <p className="big-spacer-bottom">
-        {translate('onboarding.create_project.select_method.manually')}
-      </p>
-      <button
-        className="button button-huge display-flex-column create-project-mode-type-manual"
-        onClick={() => props.onSelectMode(CreateProjectModes.Manual)}
-        type="button"
-      >
-        <ChevronsIcon size={DEFAULT_ICON_SIZE} />
-        <div className="medium big-spacer-top">
-          {translate('onboarding.create_project.select_method.manual')}
+    <div className="sw-body-sm">
+      <div className="sw-flex sw-flex-col">
+        <Title className="sw-mb-10">{translate('onboarding.create_project.select_method')}</Title>
+        <LightPrimary>
+          {translate('onboarding.create_project.select_method.devops_platform')}
+        </LightPrimary>
+        <LightPrimary>
+          {translate('onboarding.create_project.select_method.devops_platform_second')}
+        </LightPrimary>
+        {almTotalCount === 0 && canAdmin && (
+          <LightPrimary className="sw-mt-3">
+            {translate('onboarding.create_project.select_method.no_alm_yet.admin')}
+          </LightPrimary>
+        )}
+        <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-mt-6">
+          {renderAlmOption(props, AlmKeys.Azure, CreateProjectModes.AzureDevOps)}
+          {renderAlmOption(props, AlmKeys.BitbucketServer, CreateProjectModes.BitbucketServer)}
+          {renderAlmOption(props, AlmKeys.BitbucketCloud, CreateProjectModes.BitbucketCloud)}
+          {renderAlmOption(props, AlmKeys.GitHub, CreateProjectModes.GitHub)}
+          {renderAlmOption(props, AlmKeys.GitLab, CreateProjectModes.GitLab)}
+        </div>
+        <LightPrimary className="sw-mb-6 sw-mt-10">
+          {translate('onboarding.create_project.select_method.manually')}
+        </LightPrimary>
+        <div className="sw-grid sw-gap-6 sw-grid-cols-12">
+          <GreyCard className="sw-col-span-4 sw-p-4 sw-py-6 sw-flex sw-justify-between sw-items-center">
+            <div>
+              <StandoutLink to={getCreateProjectModeLocation(CreateProjectModes.Manual)}>
+                {translate('onboarding.create_project.import_select_method.manual')}
+              </StandoutLink>
+            </div>
+          </GreyCard>
         </div>
-      </button>
-    </>
+      </div>
+    </div>
   );
 }
 
index 1238d1e05ddbc35e196bae0475330b9c21c4e9df..51c697011121f3f36799fc59abebef1a036c4c51 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
+import { LargeCenteredLayout } from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { FormattedMessage } from 'react-intl';
@@ -284,7 +285,6 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
           <CreateProjectModeSelection
             almCounts={almCounts}
             loadingBindings={loading}
-            onSelectMode={this.handleModeSelect}
             onConfigMode={this.handleModeConfig}
           />
         );
@@ -349,10 +349,11 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
     const mode: CreateProjectModes | undefined = location.query?.mode;
 
     return (
-      <>
+      <LargeCenteredLayout className="sw-pt-8">
         <Helmet title={translate('onboarding.create_project.select_method')} titleTemplate="%s" />
         <A11ySkipTarget anchor="create_project_main" />
-        <div className="page page-limited huge-spacer-bottom position-relative" id="create-project">
+
+        <div id="create-project">
           <div className={classNames({ 'sw-hidden': isProjectSetupDone })}>
             {this.renderProjectCreation(mode)}
           </div>
@@ -369,7 +370,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
             />
           )}
         </div>
-      </>
+      </LargeCenteredLayout>
     );
   }
 }
index a22e6d6b32f6735089031cf674a9823054dbe6e8..33c29f898515cc8dafbb9a5fb4a7e0d3c45dceeb 100644 (file)
@@ -28,7 +28,7 @@ import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock
 import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
-import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import CreateProjectPage from '../CreateProjectPage';
 
 jest.mock('../../../../api/alm-integrations');
 jest.mock('../../../../api/alm-settings');
@@ -45,7 +45,13 @@ const ui = {
   instanceSelector: byLabelText(/alm.configuration.selector.label/),
 };
 
+const original = window.location;
+
 beforeAll(() => {
+  Object.defineProperty(window, 'location', {
+    configurable: true,
+    value: { replace: jest.fn() },
+  });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
   newCodePeriodHandler = new NewCodePeriodsServiceMock();
@@ -57,15 +63,14 @@ beforeEach(() => {
   almSettingsHandler.reset();
   newCodePeriodHandler.reset();
 });
+afterAll(() => {
+  Object.defineProperty(window, 'location', { configurable: true, value: original });
+});
 
 it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => {
   const user = userEvent.setup();
   renderCreateProject();
-  expect(ui.azureCreateProjectButton.get()).toBeInTheDocument();
-
-  await user.click(ui.azureCreateProjectButton.get());
-
-  expect(screen.getByText('onboarding.create_project.azure.title')).toBeInTheDocument();
+  expect(await screen.findByText('onboarding.create_project.azure.title')).toBeInTheDocument();
   expect(screen.getByText('alm.configuration.selector.label.alm.azure.long')).toBeInTheDocument();
 
   expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
@@ -88,10 +93,11 @@ it('should ask for PAT when it is not set yet and show the import project featur
 
 it('should show import project feature when PAT is already set', async () => {
   const user = userEvent.setup();
+
   renderCreateProject();
+  expect(await screen.findByText('onboarding.create_project.azure.title')).toBeInTheDocument();
 
   await act(async () => {
-    await user.click(ui.azureCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-azure-2/]);
   });
 
@@ -122,9 +128,9 @@ it('should show import project feature when PAT is already set', async () => {
 it('should show search filter when PAT is already set', async () => {
   const user = userEvent.setup();
   renderCreateProject();
+  expect(await screen.findByText('onboarding.create_project.azure.title')).toBeInTheDocument();
 
   await act(async () => {
-    await user.click(ui.azureCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-azure-2/]);
   });
 
@@ -143,6 +149,8 @@ it('should show search filter when PAT is already set', async () => {
   expect(screen.getByText('onboarding.create_project.azure.no_results')).toBeInTheDocument();
 });
 
-function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
-  renderApp('project/create', <CreateProjectPage {...props} />);
+function renderCreateProject() {
+  renderApp('project/create', <CreateProjectPage />, {
+    navigateTo: 'project/create?mode=azure',
+  });
 }
index 17d513bfb5d9d81c31ad3f779441230a30892300..a7916f10af82b04b15501e20b25a029e8365620a 100644 (file)
@@ -28,7 +28,7 @@ import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock
 import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
-import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import CreateProjectPage from '../CreateProjectPage';
 
 jest.mock('../../../../api/alm-integrations');
 jest.mock('../../../../api/alm-settings');
@@ -44,8 +44,13 @@ const ui = {
   }),
   instanceSelector: byLabelText(/alm.configuration.selector.label/),
 };
+const original = window.location;
 
 beforeAll(() => {
+  Object.defineProperty(window, 'location', {
+    configurable: true,
+    value: { replace: jest.fn() },
+  });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
   newCodePeriodHandler = new NewCodePeriodsServiceMock();
@@ -58,14 +63,16 @@ beforeEach(() => {
   newCodePeriodHandler.reset();
 });
 
+afterAll(() => {
+  Object.defineProperty(window, 'location', { configurable: true, value: original });
+});
+
 it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => {
   const user = userEvent.setup();
   renderCreateProject();
-  expect(ui.bitbucketServerCreateProjectButton.get()).toBeInTheDocument();
 
-  await user.click(ui.bitbucketServerCreateProjectButton.get());
   expect(screen.getByText('onboarding.create_project.from_bbs')).toBeInTheDocument();
-  expect(ui.instanceSelector.get()).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
 
   expect(
     screen.getByText('onboarding.create_project.pat_form.title.bitbucket')
@@ -91,8 +98,11 @@ it('should ask for PAT when it is not set yet and show the import project featur
 it('should show import project feature when PAT is already set', async () => {
   const user = userEvent.setup();
   renderCreateProject();
+
+  expect(screen.getByText('onboarding.create_project.from_bbs')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketServerCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketserver-2/]);
   });
 
@@ -144,8 +154,10 @@ it('should show search filter when PAT is already set', async () => {
   const user = userEvent.setup();
   renderCreateProject();
 
+  expect(screen.getByText('onboarding.create_project.from_bbs')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketServerCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketserver-2/]);
   });
 
@@ -162,17 +174,20 @@ it('should show search filter when PAT is already set', async () => {
 });
 
 it('should show no result message when there are no projects', async () => {
-  const user = userEvent.setup();
   almIntegrationHandler.setBitbucketServerProjects([]);
   renderCreateProject();
+  expect(screen.getByText('onboarding.create_project.from_bbs')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketServerCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketserver-2/]);
   });
 
   expect(screen.getByText('onboarding.create_project.no_bbs_projects')).toBeInTheDocument();
 });
 
-function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
-  renderApp('project/create', <CreateProjectPage {...props} />);
+function renderCreateProject() {
+  renderApp('project/create', <CreateProjectPage />, {
+    navigateTo: 'project/create?mode=bitbucket',
+  });
 }
index 58796cf522d2cf62ecfbe860d13341a4706474dc..37558ec5f1845202220367c7063022d9ffbfed58 100644 (file)
@@ -28,7 +28,7 @@ import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock
 import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
-import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import CreateProjectPage from '../CreateProjectPage';
 
 jest.mock('../../../../api/alm-integrations');
 jest.mock('../../../../api/alm-settings');
@@ -47,7 +47,13 @@ const ui = {
   instanceSelector: byLabelText(/alm.configuration.selector.label/),
 };
 
+const original = window.location;
+
 beforeAll(() => {
+  Object.defineProperty(window, 'location', {
+    configurable: true,
+    value: { replace: jest.fn() },
+  });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
   newCodePeriodHandler = new NewCodePeriodsServiceMock();
@@ -60,16 +66,16 @@ beforeEach(() => {
   newCodePeriodHandler.reset();
 });
 
+afterAll(() => {
+  Object.defineProperty(window, 'location', { configurable: true, value: original });
+});
+
 it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => {
   const user = userEvent.setup();
   renderCreateProject();
-  expect(ui.bitbucketCloudCreateProjectButton.get()).toBeInTheDocument();
 
-  await user.click(ui.bitbucketCloudCreateProjectButton.get());
-  expect(
-    screen.getByRole('heading', { name: 'onboarding.create_project.bitbucketcloud.title' })
-  ).toBeInTheDocument();
-  expect(ui.instanceSelector.get()).toBeInTheDocument();
+  expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
 
   expect(
     screen.getByText('onboarding.create_project.enter_pat.bitbucketcloud')
@@ -116,8 +122,11 @@ it('should show import project feature when PAT is already set', async () => {
   const user = userEvent.setup();
   let projectItem;
   renderCreateProject();
+
+  expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketCloudCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]);
   });
 
@@ -162,8 +171,10 @@ it('should show search filter when PAT is already set', async () => {
   const user = userEvent.setup();
   renderCreateProject();
 
+  expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketCloudCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]);
   });
 
@@ -189,11 +200,13 @@ it('should show search filter when PAT is already set', async () => {
 });
 
 it('should show no result message when there are no projects', async () => {
-  const user = userEvent.setup();
   almIntegrationHandler.setBitbucketCloudRepositories([]);
   renderCreateProject();
+
+  expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketCloudCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]);
   });
 
@@ -206,8 +219,11 @@ it('should have load more', async () => {
   const user = userEvent.setup();
   almIntegrationHandler.createRandomBitbucketCloudProjectsWithLoadMore(2, 4);
   renderCreateProject();
+
+  expect(screen.getByText('onboarding.create_project.bitbucketcloud.title')).toBeInTheDocument();
+  expect(await ui.instanceSelector.find()).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.bitbucketCloudCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]);
   });
 
@@ -230,6 +246,8 @@ it('should have load more', async () => {
   expect(loadMore).not.toBeInTheDocument();
 });
 
-function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
-  renderApp('project/create', <CreateProjectPage {...props} />);
+function renderCreateProject() {
+  renderApp('project/create', <CreateProjectPage />, {
+    navigateTo: 'project/create?mode=bitbucketcloud',
+  });
 }
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-it.tsx
new file mode 100644 (file)
index 0000000..bd2d47f
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+
+import * as React from 'react';
+import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
+import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import { mockAppState } from '../../../../helpers/testMocks';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { AlmKeys } from '../../../../types/alm-settings';
+import CreateProjectPage from '../CreateProjectPage';
+
+jest.mock('../../../../api/alm-integrations');
+jest.mock('../../../../api/alm-settings');
+
+let almIntegrationHandler: AlmIntegrationsServiceMock;
+let almSettingsHandler: AlmSettingsServiceMock;
+let newCodePeriodHandler: NewCodePeriodsServiceMock;
+
+const original = window.location;
+
+beforeAll(() => {
+  Object.defineProperty(window, 'location', {
+    configurable: true,
+    value: { replace: jest.fn() },
+  });
+  almIntegrationHandler = new AlmIntegrationsServiceMock();
+  almSettingsHandler = new AlmSettingsServiceMock();
+  newCodePeriodHandler = new NewCodePeriodsServiceMock();
+});
+
+beforeEach(() => {
+  jest.clearAllMocks();
+  almIntegrationHandler.reset();
+  almSettingsHandler.reset();
+  newCodePeriodHandler.reset();
+});
+afterAll(() => {
+  Object.defineProperty(window, 'location', { configurable: true, value: original });
+});
+
+it('should be able to setup if no config and admin', async () => {
+  almSettingsHandler.removeFromAlmSettings(AlmKeys.Azure);
+  renderCreateProject(true);
+  expect(await screen.findByText('onboarding.create_project.select_method')).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'setup' })).toBeInTheDocument();
+});
+
+it('should not be able to setup if no config and no admin rights', async () => {
+  almSettingsHandler.removeFromAlmSettings(AlmKeys.Azure);
+  renderCreateProject();
+  expect(await screen.findByText('onboarding.create_project.select_method')).toBeInTheDocument();
+  expect(screen.queryByRole('button', { name: 'setup' })).not.toBeInTheDocument();
+  await expect(screen.getByLabelText('help-tooltip')).toHaveATooltipWithContent(
+    'onboarding.create_project.alm_not_configured'
+  );
+});
+
+it('should be able to setup if config is present', async () => {
+  renderCreateProject();
+  expect(await screen.findByText('onboarding.create_project.select_method')).toBeInTheDocument();
+  expect(
+    screen.getByRole('link', { name: 'onboarding.create_project.import_select_method.bitbucket' })
+  ).toBeInTheDocument();
+});
+
+function renderCreateProject(canAdmin: boolean = false) {
+  renderApp('project/create', <CreateProjectPage />, {
+    appState: mockAppState({ canAdmin }),
+  });
+}
index 2662258a07b095d6cd4f3a3ae66df95dae5f43d0..45b42c0e5d3957056f373b4394b579a94a261a50 100644 (file)
@@ -67,13 +67,9 @@ afterAll(() => {
 });
 
 it('should redirect to github authorization page when not already authorized', async () => {
-  const user = userEvent.setup();
-  renderCreateProject();
-
-  expect(ui.githubCreateProjectButton.get()).toBeInTheDocument();
+  renderCreateProject('project/create?mode=github');
 
-  await user.click(ui.githubCreateProjectButton.get());
-  expect(screen.getByText('onboarding.create_project.github.title')).toBeInTheDocument();
+  expect(await screen.findByText('onboarding.create_project.github.title')).toBeInTheDocument();
   expect(screen.getByText('alm.configuration.selector.placeholder')).toBeInTheDocument();
   expect(ui.instanceSelector.get()).toBeInTheDocument();
 
@@ -86,13 +82,9 @@ it('should redirect to github authorization page when not already authorized', a
 });
 
 it('should not redirect to github when url is malformated', async () => {
-  const user = userEvent.setup();
-  renderCreateProject();
-
-  expect(ui.githubCreateProjectButton.get()).toBeInTheDocument();
+  renderCreateProject('project/create?mode=github');
 
-  await user.click(ui.githubCreateProjectButton.get());
-  expect(screen.getByText('onboarding.create_project.github.title')).toBeInTheDocument();
+  expect(await screen.findByText('onboarding.create_project.github.title')).toBeInTheDocument();
   expect(screen.getByText('alm.configuration.selector.placeholder')).toBeInTheDocument();
   expect(ui.instanceSelector.get()).toBeInTheDocument();
 
index f53f2aef51190bc30a27750cfc5ab763e3feacf6..de7780bc55e2e9384be797da35dba85e70bdfd99 100644 (file)
@@ -27,7 +27,7 @@ import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock
 import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
-import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import CreateProjectPage from '../CreateProjectPage';
 
 jest.mock('../../../../api/alm-integrations');
 jest.mock('../../../../api/alm-settings');
@@ -45,7 +45,13 @@ const ui = {
   instanceSelector: byLabelText(/alm.configuration.selector.label/),
 };
 
+const original = window.location;
+
 beforeAll(() => {
+  Object.defineProperty(window, 'location', {
+    configurable: true,
+    value: { replace: jest.fn() },
+  });
   almIntegrationHandler = new AlmIntegrationsServiceMock();
   almSettingsHandler = new AlmSettingsServiceMock();
   newCodePeriodHandler = new NewCodePeriodsServiceMock();
@@ -58,13 +64,15 @@ beforeEach(() => {
   newCodePeriodHandler.reset();
 });
 
+afterAll(() => {
+  Object.defineProperty(window, 'location', { configurable: true, value: original });
+});
+
 it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => {
   const user = userEvent.setup();
   renderCreateProject();
-  expect(ui.gitlabCreateProjectButton.get()).toBeInTheDocument();
 
-  await user.click(ui.gitlabCreateProjectButton.get());
-  expect(screen.getByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
+  expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
   expect(ui.instanceSelector.get()).toBeInTheDocument();
 
   expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
@@ -86,8 +94,9 @@ it('should show import project feature when PAT is already set', async () => {
   const user = userEvent.setup();
   let projectItem;
   renderCreateProject();
+
+  expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
   await act(async () => {
-    await user.click(ui.gitlabCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]);
   });
 
@@ -129,8 +138,9 @@ it('should show search filter when PAT is already set', async () => {
   const user = userEvent.setup();
   renderCreateProject();
 
+  expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
+
   await act(async () => {
-    await user.click(ui.gitlabCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]);
   });
 
@@ -152,8 +162,9 @@ it('should have load more', async () => {
   const user = userEvent.setup();
   almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(10, 20);
   renderCreateProject();
+
+  expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
   await act(async () => {
-    await user.click(ui.gitlabCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]);
   });
   const loadMore = screen.getByRole('button', { name: 'show_more' });
@@ -175,17 +186,19 @@ it('should have load more', async () => {
 });
 
 it('should show no result message when there are no projects', async () => {
-  const user = userEvent.setup();
   almIntegrationHandler.setGitlabProjects([]);
   renderCreateProject();
+
+  expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
   await act(async () => {
-    await user.click(ui.gitlabCreateProjectButton.get());
     await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]);
   });
 
   expect(screen.getByText('onboarding.create_project.gitlab.no_projects')).toBeInTheDocument();
 });
 
-function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
-  renderApp('project/create', <CreateProjectPage {...props} />);
+function renderCreateProject() {
+  renderApp('project/create', <CreateProjectPage />, {
+    navigateTo: 'project/create?mode=gitlab',
+  });
 }
index 1b22f0b085fe21303695d7005398ba4cb983adab..d1cc3348d03517ae0e425ca976ee39069f78d833 100644 (file)
@@ -80,8 +80,6 @@ const ui = {
 };
 
 async function fillFormAndNext(displayName: string, user: UserEvent) {
-  await user.click(ui.manualCreateProjectOption.get());
-
   expect(ui.manualProjectHeader.get()).toBeInTheDocument();
 
   await user.click(ui.displayNameField.get());
@@ -94,7 +92,13 @@ async function fillFormAndNext(displayName: string, user: UserEvent) {
 let almSettingsHandler: AlmSettingsServiceMock;
 let newCodePeriodHandler: NewCodePeriodsServiceMock;
 
+const original = window.location;
+
 beforeAll(() => {
+  Object.defineProperty(window, 'location', {
+    configurable: true,
+    value: { replace: jest.fn() },
+  });
   almSettingsHandler = new AlmSettingsServiceMock();
   newCodePeriodHandler = new NewCodePeriodsServiceMock();
 });
@@ -105,6 +109,10 @@ beforeEach(() => {
   newCodePeriodHandler.reset();
 });
 
+afterAll(() => {
+  Object.defineProperty(window, 'location', { configurable: true, value: original });
+});
+
 it('should fill form and move to NCD selection and back', async () => {
   const user = userEvent.setup();
   renderCreateProject();
@@ -246,5 +254,7 @@ it('the project onboarding page should be displayed when the project is created'
 });
 
 function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
-  renderApp('project/create', <CreateProjectPage {...props} />);
+  renderApp('project/create', <CreateProjectPage {...props} />, {
+    navigateTo: 'project/create?mode=manual',
+  });
 }
index b9aa166e627b43f0496fbdf4b2eb91ed4bec290a..3249b1bcec4a05bfa11a85fa559d84ab48d37d2c 100644 (file)
@@ -123,7 +123,9 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
               <>
                 <ItemDivider />
                 <ItemLink to={{ pathname: '/projects/create' }}>
-                  {translate('my_account.add_project.more')}
+                  {boundAlms.length === 0
+                    ? translate('my_account.add_project.more')
+                    : translate('my_account.add_project.more_others')}
                 </ItemLink>
               </>
             )}
index d4468e2c36aabb916fc78ebf5ca966004f62c9a0..3ed3aa8d4fc96f8ca3a741c80bbab0f2914f2a38 100644 (file)
@@ -54,7 +54,7 @@ const ui = {
   selectOptionBitbucket: byText('my_account.add_project.bitbucket'),
   selectOptionBitbucketCloud: byText('my_account.add_project.bitbucketcloud'),
   selectOptionManual: byText('my_account.add_project.manual'),
-  selectOptionMore: byText('my_account.add_project.more'),
+  selectOptionMore: byText('my_account.add_project.more_others'),
   selectOptionNewCode: byText('projects.view.new_code'),
   selectOptionAnalysisDate: byText('projects.sorting.analysis_date'),
   mandatoryFieldWarning: byText('fields_marked_with_x_required'),
index 3e88fe01f85b6d121bd2ea3f3674e5ff226144fe..5ce4cf487cac6d0a29e2e9637a02d2ee75fed312 100644 (file)
@@ -19,7 +19,7 @@
  */
 import {
   Breadcrumbs,
-  Card,
+  GreyCard,
   HoverLink,
   LightLabel,
   LightPrimary,
@@ -62,7 +62,7 @@ export interface TutorialSelectionRendererProps {
 
 function renderAlm(mode: TutorialModes, project: string, icon?: React.ReactNode) {
   return (
-    <Card className="sw-col-span-4 sw-p-4">
+    <GreyCard className="sw-col-span-4 sw-p-4">
       <StandoutLink icon={icon} to={getProjectTutorialLocation(project, mode)}>
         {translate('onboarding.tutorial.choose_method', mode)}
       </StandoutLink>
@@ -77,7 +77,7 @@ function renderAlm(mode: TutorialModes, project: string, icon?: React.ReactNode)
           {translate('onboarding.mode.help.otherci')}
         </LightLabel>
       )}
-    </Card>
+    </GreyCard>
   );
 }
 
index c5435a6928d18f08371af64ffcf9d07cd71c1ff2..f02f99ccd6625727c68a202a89b4466b29e13861 100644 (file)
@@ -34,6 +34,7 @@ import {
   getComponentIssuesUrl,
   getComponentOverviewUrl,
   getComponentSecurityHotspotsUrl,
+  getCreateProjectModeLocation,
   getDeprecatedActiveRulesUrl,
   getGlobalSettingsUrl,
   getIssuesUrl,
@@ -530,3 +531,11 @@ describe('convertToTo', () => {
     expect(convertToTo('/whatever')).toBe('/whatever');
   });
 });
+
+describe('#get import devops config URL', () => {
+  it('should work as expected', () => {
+    expect(getCreateProjectModeLocation(AlmKeys.GitHub)).toEqual({
+      search: '?mode=github',
+    });
+  });
+});
index 93060a015ba6fc0775ad94960a42b907e48f237e..abc925bde54ccaf2d48b072f5ef64e9e02ee8924 100644 (file)
@@ -331,6 +331,15 @@ export function getProjectTutorialLocation(
   };
 }
 
+/**
+ * Generate URL for the project creation page
+ */
+export function getCreateProjectModeLocation(mode?: string): Partial<Path> {
+  return {
+    search: queryToSearch({ mode }),
+  };
+}
+
 export function getQualityGatesUrl(): To {
   return {
     pathname: '/quality_gates',
index 06e783f31807051793bd792878624951f0880572..df0c2cdfa2122d190b189b77f62cc199e65815ba 100644 (file)
@@ -198,6 +198,7 @@ selected=Selected
 select_tags=Add or remove tags
 set=Set
 set_up=Set Up
+setup=Setup
 settings=Settings
 severity=Severity
 shared=Shared
@@ -2400,11 +2401,13 @@ my_account.set_notifications_for.title=Add a project
 my_account.create_new.TRK=Add a project
 my_account.add_project=Add Project
 my_account.add_project.manual=Manually
-my_account.add_project.azure=Azure DevOps
-my_account.add_project.bitbucket=Bitbucket Server
-my_account.add_project.bitbucketcloud=Bitbucket Cloud
-my_account.add_project.github=GitHub
-my_account.add_project.gitlab=GitLab
+my_account.add_project.azure=From Azure DevOps
+my_account.add_project.bitbucket=from Bitbucket Server
+my_account.add_project.bitbucketcloud=From Bitbucket Cloud
+my_account.add_project.github=From GitHub
+my_account.add_project.gitlab=From GitLab
+my_account.add_project.more_others=Import from other DevOps Platforms
+my_account.add_project.more=Import from DevOps Platforms
 my_account.reset_password.page=Update password
 my_account.reset_password=Update your password
 my_account.reset_password.explain=This account should not use the default password.
@@ -3843,7 +3846,9 @@ onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to int
 onboarding.create_project.setup_manually=Create a project
 onboarding.create_project.select_method=How do you want to create your project?
 onboarding.create_project.select_method.manually=Are you just testing or have an advanced use-case? Create a project manually.
-onboarding.create_project.select_method.devops_platform=Do you want to benefit from all of SonarQube's features (like repository import and Pull Request decoration)? Create your project from your favorite DevOps platform.
+onboarding.create_project.select_method.devops_platform=Do you want to benefit from all of SonarQube's features (like repository import and Pull Request decoration)?
+onboarding.create_project.select_method.devops_platform_second=Create your project from your favorite DevOps platform.
+
 onboarding.create_project.select_method.no_alm_yet.admin=First, you need to set up a DevOps platform configuration.
 onboarding.create_project.select_method.manual=Manually
 onboarding.create_project.select_method.azure=From Azure DevOps
@@ -3851,8 +3856,13 @@ onboarding.create_project.select_method.bitbucket=From Bitbucket Server
 onboarding.create_project.select_method.bitbucketcloud=From Bitbucket Cloud
 onboarding.create_project.select_method.github=From GitHub
 onboarding.create_project.select_method.gitlab=From GitLab
-onboarding.create_project.alm_not_configured=Contact admin to set up global configuration
-onboarding.create_project.alm_not_configured.admin=Set up global configuration
+onboarding.create_project.import_select_method.manual=Create project manually
+onboarding.create_project.import_select_method.azure=Import from Azure DevOps
+onboarding.create_project.import_select_method.bitbucket=Import from Bitbucket Server
+onboarding.create_project.import_select_method.bitbucketcloud=Import from Bitbucket Cloud
+onboarding.create_project.import_select_method.github=Import from GitHub
+onboarding.create_project.import_select_method.gitlab=Import from GitLab
+onboarding.create_project.alm_not_configured=Contact your admin to set up the global configuration allowing you to import project from this DevOps Platform
 onboarding.create_project.check_alm_supported=Checking if available
 onboarding.create_project.project_key=Project key
 onboarding.create_project.project_key.description=The project key is a unique identifier for your project. It may contain up to 400 characters. Allowed characters are alphanumeric, '-' (dash), '_' (underscore), '.' (period) and ':' (colon), with at least one non-digit.