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')};
${tw`sw-p-6`};
${tw`sw-rounded-1`};
`;
+
+const GreyCardStyled = styled(CardStyled)`
+ border: ${themeBorder('default', 'almCardBorder')};
+`;
${({ 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')}));
}
`};
`;
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>);
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');
+});
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
setupAzureProjectCreation,
setupBitbucketCloudProjectCreation,
setupBitbucketServerProjectCreation,
- setupGitlabProjectCreation,
setupGithubProjectCreation,
+ setupGitlabProjectCreation,
} from '../alm-integrations';
export default class AlmIntegrationsServiceMock {
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);
'/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
* 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';
};
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,
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>
);
}
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>
);
}
* 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';
<CreateProjectModeSelection
almCounts={almCounts}
loadingBindings={loading}
- onSelectMode={this.handleModeSelect}
onConfigMode={this.handleModeConfig}
/>
);
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>
/>
)}
</div>
- </>
+ </LargeCenteredLayout>
);
}
}
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');
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();
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();
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/]);
});
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/]);
});
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',
+ });
}
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');
}),
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();
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')
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/]);
});
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/]);
});
});
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',
+ });
}
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');
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();
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')
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/]);
});
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/]);
});
});
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/]);
});
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/]);
});
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',
+ });
}
--- /dev/null
+/*
+ * 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 }),
+ });
+}
});
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();
});
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();
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');
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();
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();
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/]);
});
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/]);
});
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' });
});
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',
+ });
}
};
async function fillFormAndNext(displayName: string, user: UserEvent) {
- await user.click(ui.manualCreateProjectOption.get());
-
expect(ui.manualProjectHeader.get()).toBeInTheDocument();
await user.click(ui.displayNameField.get());
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();
});
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();
});
function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
- renderApp('project/create', <CreateProjectPage {...props} />);
+ renderApp('project/create', <CreateProjectPage {...props} />, {
+ navigateTo: 'project/create?mode=manual',
+ });
}
<>
<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>
</>
)}
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'),
*/
import {
Breadcrumbs,
- Card,
+ GreyCard,
HoverLink,
LightLabel,
LightPrimary,
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>
{translate('onboarding.mode.help.otherci')}
</LightLabel>
)}
- </Card>
+ </GreyCard>
);
}
getComponentIssuesUrl,
getComponentOverviewUrl,
getComponentSecurityHotspotsUrl,
+ getCreateProjectModeLocation,
getDeprecatedActiveRulesUrl,
getGlobalSettingsUrl,
getIssuesUrl,
expect(convertToTo('/whatever')).toBe('/whatever');
});
});
+
+describe('#get import devops config URL', () => {
+ it('should work as expected', () => {
+ expect(getCreateProjectModeLocation(AlmKeys.GitHub)).toEqual({
+ search: '?mode=github',
+ });
+ });
+});
};
}
+/**
+ * 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',
select_tags=Add or remove tags
set=Set
set_up=Set Up
+setup=Setup
settings=Settings
severity=Severity
shared=Shared
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.
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
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.