Browse Source

SONAR-20086 - Migrating devops main page

tags/10.2.0.77647
Kevin Silva 10 months ago
parent
commit
73d607c715
25 changed files with 421 additions and 178 deletions
  1. 10
    0
      server/sonar-web/design-system/src/components/Card.tsx
  2. 3
    3
      server/sonar-web/design-system/src/components/Link.tsx
  3. 12
    1
      server/sonar-web/design-system/src/components/__tests__/Card-test.tsx
  4. 15
    0
      server/sonar-web/public/images/alm/azure_grey.svg
  5. 10
    0
      server/sonar-web/public/images/alm/bitbucket_grey.svg
  6. 10
    0
      server/sonar-web/public/images/alm/github_grey.svg
  7. 13
    0
      server/sonar-web/public/images/alm/gitlab_grey.svg
  8. 1
    1
      server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts
  9. 6
    0
      server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts
  10. 1
    1
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  11. 87
    95
      server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
  12. 5
    4
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  13. 18
    10
      server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
  14. 25
    10
      server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
  15. 32
    14
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
  16. 89
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-it.tsx
  17. 4
    12
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx
  18. 24
    11
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
  19. 13
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
  20. 3
    1
      server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
  21. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
  22. 3
    3
      server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
  23. 9
    0
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
  24. 9
    0
      server/sonar-web/src/main/js/helpers/urls.ts
  25. 18
    8
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 10
- 0
server/sonar-web/design-system/src/components/Card.tsx View 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')};
`;

+ 3
- 3
server/sonar-web/design-system/src/components/Link.tsx View 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')}));
}
`};
`;

+ 12
- 1
server/sonar-web/design-system/src/components/__tests__/Card-test.tsx View 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');
});

+ 15
- 0
server/sonar-web/public/images/alm/azure_grey.svg View File

@@ -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>

+ 10
- 0
server/sonar-web/public/images/alm/bitbucket_grey.svg View File

@@ -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>

+ 10
- 0
server/sonar-web/public/images/alm/github_grey.svg View File

@@ -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>

+ 13
- 0
server/sonar-web/public/images/alm/gitlab_grey.svg View File

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts View File

@@ -60,8 +60,8 @@ import {
setupAzureProjectCreation,
setupBitbucketCloudProjectCreation,
setupBitbucketServerProjectCreation,
setupGitlabProjectCreation,
setupGithubProjectCreation,
setupGitlabProjectCreation,
} from '../alm-integrations';

export default class AlmIntegrationsServiceMock {

+ 6
- 0
server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts View 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);


+ 1
- 1
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx View 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

+ 87
- 95
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx View File

@@ -18,13 +18,22 @@
* 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>
);
}


+ 5
- 4
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View 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>
);
}
}

+ 18
- 10
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx View 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',
});
}

+ 25
- 10
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx View 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',
});
}

+ 32
- 14
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx View 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',
});
}

+ 89
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-it.tsx View File

@@ -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 }),
});
}

+ 4
- 12
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx View 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();


+ 24
- 11
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx View 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',
});
}

+ 13
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx View 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',
});
}

+ 3
- 1
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx View 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>
</>
)}

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx View 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'),

+ 3
- 3
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx View 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>
);
}


+ 9
- 0
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts View 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',
});
});
});

+ 9
- 0
server/sonar-web/src/main/js/helpers/urls.ts View 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',

+ 18
- 8
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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.

Loading…
Cancel
Save