*/
import { get, getJSON, post, postJSON } from 'sonar-ui-common/helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { BitbucketProject, BitbucketRepository } from '../types/alm-integration';
+import {
+ BitbucketProject,
+ BitbucketRepository,
+ GithubOrganization,
+ GithubRepository
+} from '../types/alm-integration';
import { ProjectBase } from './components';
export function setAlmPersonalAccessToken(almSetting: string, pat: string): Promise<void> {
export function checkPersonalAccessTokenIsValid(almSetting: string): Promise<boolean> {
return get('/api/alm_integrations/check_pat', { almSetting })
.then(() => true)
- .catch(response => {
+ .catch((response: Response) => {
if (response.status === 400) {
return false;
} else {
repositoryName
});
}
+
+export function getGithubClientId(almSetting: string): Promise<{ clientId: string }> {
+ return getJSON('/api/alm_integrations/get_github_client_id', { almSetting });
+}
+
+export function getGithubOrganizations(
+ almSetting: string,
+ token: string
+): Promise<{ organizations: GithubOrganization[] }> {
+ return getJSON('/api/alm_integrations/list_github_enterprise_organizations', {
+ almSetting,
+ token
+ });
+}
+
+export function getGithubRepositories(
+ almSetting: string,
+ organization: string,
+ p = 1,
+ query?: string
+): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> {
+ return getJSON('/api/alm_integrations/list_github_enterprise_repositories', {
+ almSetting,
+ organization,
+ p,
+ query: query || undefined
+ });
+}
</div>
)
}
- showBreadcrumb={true}
title={
<span className="text-middle">
<img
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { AlmKeys } from '../../../types/alm-settings';
import { CreateProjectModes } from './types';
export interface CreateProjectModeSelectionProps {
- bbsBindingCount: number;
+ almCounts: { [key in AlmKeys]: number };
loadingBindings: boolean;
onSelectMode: (mode: CreateProjectModes) => void;
}
-export default function CreateProjectModeSelection(props: CreateProjectModeSelectionProps) {
- const { bbsBindingCount, loadingBindings } = props;
- const bbsBindingDisabled = bbsBindingCount !== 1 || loadingBindings;
+function renderAlmOption(
+ props: CreateProjectModeSelectionProps,
+ alm: AlmKeys,
+ mode: CreateProjectModes
+) {
+ const { almCounts, loadingBindings } = props;
+
+ const count = almCounts[alm];
+ const disabled = count !== 1 || loadingBindings;
+
+ return (
+ <button
+ className={classNames(
+ 'button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs',
+ { disabled }
+ )}
+ disabled={disabled}
+ onClick={() => props.onSelectMode(mode)}
+ type="button">
+ <img
+ alt="" // Should be ignored by screen readers
+ height={80}
+ src={`${getBaseUrl()}/images/alm/${alm}.svg`}
+ />
+ <div className="medium big-spacer-top">
+ {translate('onboarding.create_project.select_method', alm)}
+ </div>
+
+ {loadingBindings && (
+ <span>
+ {translate('onboarding.create_project.check_alm_supported')}
+ <i className="little-spacer-left spinner" />
+ </span>
+ )}
+ {!loadingBindings && disabled && (
+ <div className="text-muted small spacer-top" style={{ lineHeight: 1.5 }}>
+ {translate('onboarding.create_project.alm_not_configured')}
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay={
+ count === 0
+ ? translate('onboarding.create_project.zero_alm_instances', alm)
+ : `${translate('onboarding.create_project.too_many_alm_instances', alm)}
+ ${translateWithParameters(
+ 'onboarding.create_project.alm_instances_count_X',
+ count
+ )}`
+ }
+ />
+ </div>
+ )}
+ </button>
+ );
+}
+
+export default function CreateProjectModeSelection(props: CreateProjectModeSelectionProps) {
return (
<>
<header className="huge-spacer-top big-spacer-bottom padded">
<p className="text-center big">{translate('onboarding.create_project.select_method')}</p>
</header>
- <div className="create-project-modes huge-spacer-top display-flex-space-around">
+ <div className="create-project-modes huge-spacer-top display-flex-justify-center">
<button
className="button button-huge display-flex-column create-project-mode-type-manual"
onClick={() => props.onSelectMode(CreateProjectModes.Manual)}
</div>
</button>
- <button
- className={classNames(
- 'button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs',
- { disabled: bbsBindingDisabled }
- )}
- disabled={bbsBindingDisabled}
- onClick={() => props.onSelectMode(CreateProjectModes.BitbucketServer)}
- type="button">
- <img
- alt="" // Should be ignored by screen readers
- height={80}
- src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
- width={80}
- />
- <div className="medium big-spacer-top">
- {translate('onboarding.create_project.select_method.from_bbs')}
- </div>
-
- {loadingBindings && (
- <span>
- {translate('onboarding.create_project.check_bbs_supported')}
- <i className="little-spacer-left spinner" />
- </span>
- )}
-
- {!loadingBindings && bbsBindingDisabled && (
- <div className="text-muted small spacer-top" style={{ lineHeight: 1.5 }}>
- {translate('onboarding.create_project.bbs_not_configured')}
- <HelpTooltip
- className="little-spacer-left"
- overlay={
- bbsBindingCount === 0
- ? translate('onboarding.create_project.zero_bbs_instances')
- : translateWithParameters(
- 'onboarding.create_project.too_many_bbs_instances_X',
- bbsBindingCount
- )
- }
- />
- </div>
- )}
- </button>
+ {renderAlmOption(props, AlmKeys.Bitbucket, CreateProjectModes.BitbucketServer)}
+ {renderAlmOption(props, AlmKeys.GitHub, CreateProjectModes.GitHub)}
</div>
</>
);
import { Helmet } from 'react-helmet-async';
import { WithRouterProps } from 'react-router';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import { addWhitePageClass, removeWhitePageClass } from 'sonar-ui-common/helpers/pages';
import { getAlmSettings } from '../../../api/alm-settings';
import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketProjectCreate from './BitbucketProjectCreate';
import CreateProjectModeSelection from './CreateProjectModeSelection';
+import GitHubProjectCreate from './GitHubProjectCreate';
import ManualProjectCreate from './ManualProjectCreate';
import './style.css';
import { CreateProjectModes } from './types';
interface Props extends Pick<WithRouterProps, 'router' | 'location'> {
- appState: Pick<T.AppState, 'branchesEnabled'>;
+ appState: Pick<T.AppState, 'branchesEnabled' | 'canAdmin'>;
currentUser: T.LoggedInUser;
}
interface State {
bitbucketSettings: AlmSettingsInstance[];
+ githubSettings: AlmSettingsInstance[];
loading: boolean;
}
export class CreateProjectPage extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { bitbucketSettings: [], loading: false };
+ state: State = { bitbucketSettings: [], githubSettings: [], loading: false };
componentDidMount() {
const {
- appState: { branchesEnabled },
- location
+ appState: { branchesEnabled }
} = this.props;
this.mounted = true;
if (branchesEnabled) {
this.fetchAlmBindings();
}
-
- if (location.query?.mode || !branchesEnabled) {
- addWhitePageClass();
- }
- }
-
- componentDidUpdate(prevProps: Props) {
- if (this.props.location.query?.mode && !prevProps.location.query?.mode) {
- addWhitePageClass();
- } else if (!this.props.location.query?.mode && prevProps.location.query?.mode) {
- removeWhitePageClass();
- }
}
componentWillUnmount() {
this.mounted = false;
- removeWhitePageClass();
}
fetchAlmBindings = () => {
if (this.mounted) {
this.setState({
bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.Bitbucket),
+ githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
loading: false
});
}
});
};
+ renderForm(mode?: CreateProjectModes) {
+ const {
+ appState: { canAdmin },
+ location
+ } = this.props;
+ const { bitbucketSettings, githubSettings, loading } = this.state;
+
+ switch (mode) {
+ case CreateProjectModes.BitbucketServer: {
+ return (
+ <BitbucketProjectCreate
+ bitbucketSettings={bitbucketSettings}
+ loadingBindings={loading}
+ location={location}
+ onProjectCreate={this.handleProjectCreate}
+ />
+ );
+ }
+ case CreateProjectModes.GitHub: {
+ return (
+ <GitHubProjectCreate
+ canAdmin={!!canAdmin}
+ code={location.query?.code}
+ settings={githubSettings[0]}
+ />
+ );
+ }
+ case CreateProjectModes.Manual: {
+ return <ManualProjectCreate onProjectCreate={this.handleProjectCreate} />;
+ }
+ default: {
+ const almCounts = {
+ [AlmKeys.Azure]: 0,
+ [AlmKeys.Bitbucket]: bitbucketSettings.length,
+ [AlmKeys.GitHub]: githubSettings.length,
+ [AlmKeys.GitLab]: 0
+ };
+ return (
+ <CreateProjectModeSelection
+ almCounts={almCounts}
+ loadingBindings={loading}
+ onSelectMode={this.handleModeSelect}
+ />
+ );
+ }
+ }
+ }
+
render() {
const {
appState: { branchesEnabled },
- currentUser,
location
} = this.props;
- const { bitbucketSettings, loading } = this.state;
-
const mode: CreateProjectModes | undefined = location.query?.mode;
- const showManualForm = !branchesEnabled || mode === CreateProjectModes.Manual;
- const showBBSForm = branchesEnabled && mode === CreateProjectModes.BitbucketServer;
return (
<>
<Helmet title={translate('my_account.create_new.TRK')} titleTemplate="%s" />
<A11ySkipTarget anchor="create_project_main" />
<div className="page page-limited huge-spacer-bottom position-relative" id="create-project">
- {!showBBSForm && !showManualForm && (
- <CreateProjectModeSelection
- bbsBindingCount={bitbucketSettings.length}
- loadingBindings={loading}
- onSelectMode={this.handleModeSelect}
- />
- )}
-
- {showManualForm && (
- <ManualProjectCreate
- branchesEnabled={branchesEnabled}
- currentUser={currentUser}
- onProjectCreate={this.handleProjectCreate}
- />
- )}
-
- {showBBSForm && (
- <BitbucketProjectCreate
- bitbucketSettings={bitbucketSettings}
- loadingBindings={loading}
- location={location}
- onProjectCreate={this.handleProjectCreate}
- />
- )}
+ {this.renderForm(branchesEnabled ? mode : CreateProjectModes.Manual)}
</div>
</>
);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Link } from 'react-router';
-import { translate } from 'sonar-ui-common/helpers/l10n';
export interface CreateProjectPageHeaderProps {
additionalActions?: React.ReactNode;
- showBreadcrumb?: boolean;
title: React.ReactNode;
}
export default function CreateProjectPageHeader(props: CreateProjectPageHeaderProps) {
- const { additionalActions, showBreadcrumb, title } = props;
+ const { additionalActions, title } = props;
return (
<header className="huge-spacer-bottom bordered-bottom overflow-hidden">
- <h1 className="pull-left huge big-spacer-bottom">
- {showBreadcrumb && (
- <>
- <Link to="/projects/create">{translate('my_account.create_new.TRK')}</Link>
- <span className="big-spacer-left big-spacer-right slash-separator" />
- </>
- )}
- {title}
- </h1>
+ <h1 className="pull-left huge big-spacer-bottom">{title}</h1>
{additionalActions}
</header>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { debounce } from 'lodash';
+import * as React from 'react';
+import { getHostUrl } from 'sonar-ui-common/helpers/urls';
+import {
+ getGithubClientId,
+ getGithubOrganizations,
+ getGithubRepositories
+} from '../../../api/alm-integrations';
+import { GithubOrganization, GithubRepository } from '../../../types/alm-integration';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
+
+interface Props {
+ canAdmin: boolean;
+ code?: string;
+ settings?: AlmSettingsInstance;
+}
+
+interface State {
+ error: boolean;
+ loading: boolean;
+ loadingRepositories: boolean;
+ organizations: GithubOrganization[];
+ repositoryPaging: T.Paging;
+ repositories: GithubRepository[];
+ searchQuery: string;
+ selectedOrganization?: GithubOrganization;
+ selectedRepository?: GithubRepository;
+}
+
+const REPOSITORY_PAGE_SIZE = 30;
+
+export default class GitHubProjectCreate extends React.Component<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ error: false,
+ loading: true,
+ loadingRepositories: false,
+ organizations: [],
+ repositories: [],
+ repositoryPaging: { pageSize: REPOSITORY_PAGE_SIZE, total: 0, pageIndex: 1 },
+ searchQuery: ''
+ };
+
+ this.triggerSearch = debounce(this.triggerSearch, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+
+ this.initialize();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (!prevProps.settings && this.props.settings) {
+ this.initialize();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ async initialize() {
+ const { code, settings } = this.props;
+
+ if (!settings) {
+ this.setState({ error: true });
+ return;
+ } else {
+ this.setState({ error: false });
+ }
+
+ try {
+ if (!code) {
+ await this.redirectToGithub(settings);
+ } else {
+ await this.fetchOrganizations(settings, code);
+ }
+ } catch (e) {
+ if (this.mounted) {
+ this.setState({ error: true });
+ }
+ }
+ }
+
+ async redirectToGithub(settings: AlmSettingsInstance) {
+ const { clientId } = await getGithubClientId(settings.key);
+
+ const queryParams = [
+ { param: 'client_id', value: clientId },
+ { param: 'redirect_uri', value: `${getHostUrl()}/projects/create?mode=${AlmKeys.GitHub}` }
+ ]
+ .map(({ param, value }) => `${param}=${value}`)
+ .join('&');
+
+ window.location.replace(`https://github.com/login/oauth/authorize?${queryParams}`);
+ }
+
+ async fetchOrganizations(settings: AlmSettingsInstance, token: string) {
+ const { organizations } = await getGithubOrganizations(settings.key, token);
+
+ if (this.mounted) {
+ this.setState({ loading: false, organizations });
+ }
+ }
+
+ async fetchRepositories(params: { organizationKey: string; page?: number; query?: string }) {
+ const { organizationKey, page = 1, query } = params;
+ const { settings } = this.props;
+
+ if (!settings) {
+ this.setState({ error: true });
+ return;
+ }
+
+ this.setState({ loadingRepositories: true });
+
+ const data = await getGithubRepositories(settings.key, organizationKey, page, query);
+
+ if (this.mounted) {
+ this.setState(({ repositories }) => ({
+ loadingRepositories: false,
+ repositoryPaging: data.paging,
+ repositories: page === 1 ? data.repositories : [...repositories, ...data.repositories]
+ }));
+ }
+ }
+
+ triggerSearch = (query: string) => {
+ const { selectedOrganization } = this.state;
+ if (selectedOrganization) {
+ this.fetchRepositories({ organizationKey: selectedOrganization.key, query });
+ }
+ };
+
+ handleSelectOrganization = (key: string) => {
+ this.setState(({ organizations }) => ({
+ selectedOrganization: organizations.find(o => o.key === key)
+ }));
+ this.fetchRepositories({ organizationKey: key });
+ };
+
+ handleSelectRepository = (key: string) => {
+ this.setState(({ repositories }) => ({
+ selectedRepository: repositories?.find(r => r.key === key)
+ }));
+ };
+
+ handleSearch = (searchQuery: string) => {
+ this.setState({ searchQuery });
+ this.triggerSearch(searchQuery);
+ };
+
+ handleLoadMore = () => {
+ const { repositoryPaging, searchQuery, selectedOrganization } = this.state;
+
+ if (selectedOrganization) {
+ this.fetchRepositories({
+ organizationKey: selectedOrganization.key,
+ page: repositoryPaging.pageIndex + 1,
+ query: searchQuery
+ });
+ }
+ };
+
+ render() {
+ const { canAdmin } = this.props;
+ const {
+ error,
+ loading,
+ loadingRepositories,
+ organizations,
+ repositoryPaging,
+ repositories,
+ searchQuery,
+ selectedOrganization,
+ selectedRepository
+ } = this.state;
+ return (
+ <GitHubProjectCreateRenderer
+ canAdmin={canAdmin}
+ error={error}
+ loading={loading}
+ loadingRepositories={loadingRepositories}
+ onLoadMore={this.handleLoadMore}
+ onSearch={this.handleSearch}
+ onSelectOrganization={this.handleSelectOrganization}
+ onSelectRepository={this.handleSelectRepository}
+ organizations={organizations}
+ repositoryPaging={repositoryPaging}
+ searchQuery={searchQuery}
+ repositories={repositories}
+ selectedOrganization={selectedOrganization}
+ selectedRepository={selectedRepository}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
+import SearchSelect from 'sonar-ui-common/components/controls/SearchSelect';
+import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from '../../../helpers/system';
+import { GithubOrganization, GithubRepository } from '../../../types/alm-integration';
+import CreateProjectPageHeader from './CreateProjectPageHeader';
+
+export interface GitHubProjectCreateRendererProps {
+ canAdmin: boolean;
+ error: boolean;
+ loading: boolean;
+ loadingRepositories: boolean;
+ onLoadMore: () => void;
+ onSearch: (q: string) => void;
+ onSelectOrganization: (key: string) => void;
+ onSelectRepository: (key: string) => void;
+ organizations: GithubOrganization[];
+ repositories?: GithubRepository[];
+ repositoryPaging: T.Paging;
+ searchQuery: string;
+ selectedOrganization?: GithubOrganization;
+ selectedRepository?: GithubRepository;
+}
+
+function orgToOption({ key, name }: GithubOrganization) {
+ return { value: key, label: name };
+}
+
+export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRendererProps) {
+ const {
+ canAdmin,
+ error,
+ loading,
+ loadingRepositories,
+ organizations,
+ repositories,
+ repositoryPaging,
+ searchQuery,
+ selectedOrganization,
+ selectedRepository
+ } = props;
+
+ return (
+ <div>
+ <CreateProjectPageHeader
+ title={
+ <span className="text-middle display-flex-center">
+ <img
+ alt="" // Should be ignored by screen readers
+ className="spacer-right"
+ height={24}
+ src={`${getBaseUrl()}/images/alm/github.svg`}
+ />
+ {translate('onboarding.create_project.github.title')}
+ </span>
+ }
+ />
+
+ {error ? (
+ <div className="display-flex-justify-center">
+ <div className="boxed-group padded width-50 huge-spacer-top">
+ <h2 className="big-spacer-bottom">
+ {translate('onboarding.create_project.github.warning.title')}
+ </h2>
+ <Alert variant="warning">
+ {canAdmin ? (
+ <FormattedMessage
+ id="onboarding.create_project.github.warning.message_admin"
+ defaultMessage={translate(
+ 'onboarding.create_project.github.warning.message_admin'
+ )}
+ values={{
+ link: (
+ <Link to="/admin/settings?category=almintegration">
+ {translate('onboarding.create_project.github.warning.message_admin.link')}
+ </Link>
+ )
+ }}
+ />
+ ) : (
+ translate('onboarding.create_project.github.warning.message')
+ )}
+ </Alert>
+ </div>
+ </div>
+ ) : (
+ <DeferredSpinner loading={loading}>
+ <div className="form-field">
+ <label>{translate('onboarding.create_project.github.choose_organization')}</label>
+ {organizations.length > 0 ? (
+ <SearchSelect
+ defaultOptions={organizations.slice(0, 10).map(orgToOption)}
+ onSearch={(q: string) =>
+ Promise.resolve(
+ organizations.filter(o => !q || o.name.includes(q)).map(orgToOption)
+ )
+ }
+ minimumQueryLength={0}
+ onSelect={({ value }) => props.onSelectOrganization(value)}
+ value={selectedOrganization && orgToOption(selectedOrganization)}
+ />
+ ) : (
+ !loading && (
+ <Alert className="spacer-top" variant="error">
+ {canAdmin ? (
+ <FormattedMessage
+ id="onboarding.create_project.github.no_orgs_admin"
+ defaultMessage={translate('onboarding.create_project.github.no_orgs_admin')}
+ values={{
+ link: (
+ <Link to="/admin/settings?category=almintegration">
+ {translate(
+ 'onboarding.create_project.github.warning.message_admin.link'
+ )}
+ </Link>
+ )
+ }}
+ />
+ ) : (
+ translate('onboarding.create_project.github.no_orgs')
+ )}
+ </Alert>
+ )
+ )}
+ </div>
+ </DeferredSpinner>
+ )}
+
+ {selectedOrganization && repositories && (
+ <div className="boxed-group padded display-flex-wrap">
+ <div className="width-100">
+ <SearchBox
+ className="big-spacer-bottom"
+ onChange={props.onSearch}
+ placeholder={translate('onboarding.create_project.search_repositories')}
+ value={searchQuery}
+ />
+ </div>
+
+ {repositories.length === 0 ? (
+ <div className="padded">
+ <DeferredSpinner loading={loadingRepositories}>
+ {translate('no_results')}
+ </DeferredSpinner>
+ </div>
+ ) : (
+ repositories.map(r => (
+ <Radio
+ className="spacer-top spacer-bottom padded create-project-github-repository"
+ key={r.key}
+ checked={
+ !!r.sqProjectKey || (!!selectedRepository && selectedRepository.key === r.key)
+ }
+ disabled={!!r.sqProjectKey || loadingRepositories || importing}
+ value={r.key}
+ onCheck={props.onSelectRepository}>
+ <div className="big overflow-hidden" title={r.name}>
+ <div className="overflow-hidden text-ellipsis">{r.name}</div>
+ {r.sqProjectKey && (
+ <em className="notice text-muted-2 small display-flex-center">
+ {translate('onboarding.create_project.repository_imported')}
+ <CheckIcon className="little-spacer-left" size={12} />
+ </em>
+ )}
+ </div>
+ </Radio>
+ ))
+ )}
+
+ <div className="display-flex-justify-center width-100">
+ <ListFooter
+ count={repositories.length}
+ total={repositoryPaging.total}
+ loadMore={props.onLoadMore}
+ loading={loadingRepositories}
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
import './ManualProjectCreate.css';
interface Props {
- branchesEnabled?: boolean;
- currentUser: T.LoggedInUser;
onProjectCreate: (projectKeys: string[]) => void;
}
touched,
validating
} = this.state;
- const { branchesEnabled } = this.props;
const projectNameIsInvalid = touched && projectNameError !== undefined;
const projectNameIsValid = touched && projectNameError === undefined;
return (
<>
- <CreateProjectPageHeader
- showBreadcrumb={branchesEnabled}
- title={translate('onboarding.create_project.setup_manually')}
- />
+ <CreateProjectPageHeader title={translate('onboarding.create_project.setup_manually')} />
<div className="create-project-manual">
<div className="flex-1 huge-spacer-right">
import { shallow } from 'enzyme';
import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
+import { AlmKeys } from '../../../../types/alm-settings';
import CreateProjectModeSelection, {
CreateProjectModeSelectionProps
} from '../CreateProjectModeSelection';
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
- expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading bbs instances');
- expect(shallowRender({ bbsBindingCount: 0 })).toMatchSnapshot('no bbs instances');
- expect(shallowRender({ bbsBindingCount: 2 })).toMatchSnapshot('too many bbs instances');
+ expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading instances');
+ expect(shallowRender({}, { [AlmKeys.Bitbucket]: 0, [AlmKeys.GitHub]: 2 })).toMatchSnapshot(
+ 'invalid configs'
+ );
});
it('should correctly pass the selected mode up', () => {
click(wrapper.find('button.create-project-mode-type-manual'));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.Manual);
- click(wrapper.find('button.create-project-mode-type-bbs'));
+ click(wrapper.find('button.create-project-mode-type-bbs').at(0));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.BitbucketServer);
+
+ click(wrapper.find('button.create-project-mode-type-bbs').at(1));
+ expect(onSelectMode).toBeCalledWith(CreateProjectModes.GitHub);
});
-function shallowRender(props: Partial<CreateProjectModeSelectionProps> = {}) {
+function shallowRender(
+ props: Partial<CreateProjectModeSelectionProps> = {},
+ almCountOverrides = {}
+) {
+ const almCounts = {
+ [AlmKeys.Azure]: 0,
+ [AlmKeys.Bitbucket]: 1,
+ [AlmKeys.GitHub]: 0,
+ [AlmKeys.GitLab]: 0,
+ ...almCountOverrides
+ };
return shallow<CreateProjectModeSelectionProps>(
<CreateProjectModeSelection
- bbsBindingCount={1}
+ almCounts={almCounts}
loadingBindings={false}
onSelectMode={jest.fn()}
{...props}
import { shallow } from 'enzyme';
import * as React from 'react';
-import { addWhitePageClass } from 'sonar-ui-common/helpers/pages';
import { getAlmSettings } from '../../../../api/alm-settings';
import { mockLocation, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
getAlmSettings: jest.fn().mockResolvedValue([{ alm: AlmKeys.Bitbucket, key: 'foo' }])
}));
-jest.mock('sonar-ui-common/helpers/pages', () => ({
- addWhitePageClass: jest.fn(),
- removeWhitePageClass: jest.fn()
-}));
-
beforeEach(jest.clearAllMocks);
it('should render correctly', () => {
expect(push).toBeCalledWith(expect.objectContaining(location));
expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
- expect(addWhitePageClass).toBeCalled();
});
it('should render correctly if the BBS method is selected', () => {
expect(push).toBeCalledWith(expect.objectContaining(location));
expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
- expect(addWhitePageClass).toBeCalled();
+});
+
+it('should render correctly if the GitHub method is selected', () => {
+ const push = jest.fn();
+ const location = { query: { mode: CreateProjectModes.GitHub } };
+ const wrapper = shallowRender({ router: mockRouter({ push }) });
+
+ wrapper.instance().handleModeSelect(CreateProjectModes.GitHub);
+ expect(push).toBeCalledWith(expect.objectContaining(location));
+
+ expect(wrapper.setProps({ location: mockLocation(location) })).toMatchSnapshot();
});
function shallowRender(props: Partial<CreateProjectPage['props']> = {}) {
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
- expect(shallowRender({ showBreadcrumb: true })).toMatchSnapshot('with breadcrumb');
expect(shallowRender({ additionalActions: 'Bar' })).toMatchSnapshot('additional content');
});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import {
+ getGithubClientId,
+ getGithubOrganizations,
+ getGithubRepositories
+} from '../../../../api/alm-integrations';
+import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import GitHubProjectCreate from '../GitHubProjectCreate';
+
+jest.mock('../../../../api/alm-integrations', () => ({
+ getGithubClientId: jest.fn().mockResolvedValue({ clientId: 'client-id-124' }),
+ getGithubOrganizations: jest.fn().mockResolvedValue({ organizations: [] }),
+ getGithubRepositories: jest.fn().mockResolvedValue({ repositories: [], paging: {} })
+}));
+
+const originalLocation = window.location;
+
+beforeAll(() => {
+ const location = {
+ ...window.location,
+ replace: jest.fn()
+ };
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: location
+ });
+});
+
+afterAll(() => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: originalLocation
+ });
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should handle no settings', async () => {
+ const wrapper = shallowRender({ settings: undefined });
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().error).toBe(true);
+});
+
+it('should redirect when no code', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(getGithubClientId).toBeCalled();
+ expect(window.location.replace).toBeCalled();
+});
+
+it('should fetch organizations when code', async () => {
+ const organizations = [
+ { key: '1', name: 'org1' },
+ { key: '2', name: 'org2' }
+ ];
+ (getGithubOrganizations as jest.Mock).mockResolvedValueOnce({ organizations });
+ const wrapper = shallowRender({ code: '123456' });
+ await waitAndUpdate(wrapper);
+
+ expect(getGithubOrganizations).toBeCalled();
+ expect(wrapper.state().organizations).toBe(organizations);
+});
+
+it('should handle org selection', async () => {
+ const organizations = [
+ { key: '1', name: 'org1' },
+ { key: '2', name: 'org2' }
+ ];
+ (getGithubOrganizations as jest.Mock).mockResolvedValueOnce({ organizations });
+ const repositories = [mockGitHubRepository()];
+ (getGithubRepositories as jest.Mock).mockResolvedValueOnce({
+ repositories,
+ paging: { total: 1, pageIndex: 1 }
+ });
+ const wrapper = shallowRender({ code: '123456' });
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleSelectOrganization('1');
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().selectedOrganization).toBe(organizations[0]);
+ expect(getGithubRepositories).toBeCalled();
+
+ expect(wrapper.state().repositories).toBe(repositories);
+});
+
+it('should load more', async () => {
+ const wrapper = shallowRender();
+
+ const startRepos = [mockGitHubRepository({ key: 'first' })];
+ const repositories = [
+ mockGitHubRepository({ key: 'r1' }),
+ mockGitHubRepository({ key: 'r2' }),
+ mockGitHubRepository({ key: 'r3' })
+ ];
+ (getGithubRepositories as jest.Mock).mockResolvedValueOnce({ repositories });
+
+ wrapper.setState({
+ repositories: startRepos,
+ selectedOrganization: { key: 'o1', name: 'org' }
+ });
+
+ wrapper.instance().handleLoadMore();
+
+ await waitAndUpdate(wrapper);
+
+ expect(getGithubRepositories).toBeCalled();
+ expect(wrapper.state().repositories).toEqual([...startRepos, ...repositories]);
+});
+
+it('should handle search', async () => {
+ const wrapper = shallowRender();
+ const query = 'query';
+ const startRepos = [mockGitHubRepository({ key: 'first' })];
+ const repositories = [
+ mockGitHubRepository({ key: 'r1' }),
+ mockGitHubRepository({ key: 'r2' }),
+ mockGitHubRepository({ key: 'r3' })
+ ];
+ (getGithubRepositories as jest.Mock).mockResolvedValueOnce({ repositories });
+
+ wrapper.setState({
+ repositories: startRepos,
+ selectedOrganization: { key: 'o1', name: 'org' }
+ });
+
+ wrapper.instance().handleSearch(query);
+
+ await waitAndUpdate(wrapper);
+
+ expect(getGithubRepositories).toBeCalledWith('a', 'o1', 1, query);
+ expect(wrapper.state().repositories).toEqual(repositories);
+});
+
+it('should handle repository selection', async () => {
+ const repo = mockGitHubRepository();
+ const wrapper = shallowRender();
+ wrapper.setState({ repositories: [repo, mockGitHubRepository({ key: 'other' })] });
+
+ wrapper.instance().handleSelectRepository(repo.key);
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().selectedRepository).toBe(repo);
+});
+
+function shallowRender(props: Partial<GitHubProjectCreate['props']> = {}) {
+ return shallow<GitHubProjectCreate>(
+ <GitHubProjectCreate
+ canAdmin={false}
+ settings={mockAlmSettingsInstance({ key: 'a' })}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
+import SearchSelect from 'sonar-ui-common/components/controls/SearchSelect';
+import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations';
+import { GithubOrganization } from '../../../../types/alm-integration';
+import GitHubProjectCreateRenderer, {
+ GitHubProjectCreateRendererProps
+} from '../GitHubProjectCreateRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ error: true })).toMatchSnapshot('error');
+ expect(shallowRender({ canAdmin: true, error: true })).toMatchSnapshot('error for admin');
+
+ const organizations: GithubOrganization[] = [
+ { key: 'o1', name: 'org1' },
+ { key: 'o2', name: 'org2' }
+ ];
+
+ expect(shallowRender({ organizations })).toMatchSnapshot('organizations');
+ expect(
+ shallowRender({
+ organizations,
+ selectedOrganization: organizations[1]
+ })
+ ).toMatchSnapshot('no repositories');
+
+ const repositories = [
+ mockGitHubRepository({ id: '1', key: 'repo1' }),
+ mockGitHubRepository({ id: '2', key: 'repo2', sqProjectKey: 'repo2' }),
+ mockGitHubRepository({ id: '3', key: 'repo3' })
+ ];
+
+ expect(
+ shallowRender({
+ organizations,
+ selectedOrganization: organizations[1],
+ repositories,
+ selectedRepository: repositories[2]
+ })
+ ).toMatchSnapshot('repositories');
+});
+
+describe('callback', () => {
+ const onSelectOrganization = jest.fn();
+ const onSelectRepository = jest.fn();
+ const onSearch = jest.fn();
+ const org = { key: 'o1', name: 'org' };
+ const wrapper = shallowRender({
+ onSelectOrganization,
+ onSelectRepository,
+ onSearch,
+ organizations: [org],
+ selectedOrganization: org,
+ repositories: [mockGitHubRepository()]
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should be called when org is selected', () => {
+ const value = 'o1';
+ wrapper.find(SearchSelect).props().onSelect!({ value });
+ expect(onSelectOrganization).toBeCalledWith(value);
+ });
+
+ it('should be called when searchbox is changed', () => {
+ const value = 'search query';
+ wrapper
+ .find(SearchBox)
+ .props()
+ .onChange(value);
+ expect(onSearch).toBeCalledWith(value);
+ });
+
+ it('should be called when repo is selected', () => {
+ const value = 'repo1';
+ wrapper
+ .find(Radio)
+ .props()
+ .onCheck(value);
+ expect(onSelectRepository).toBeCalledWith(value);
+ });
+});
+
+function shallowRender(props: Partial<GitHubProjectCreateRendererProps> = {}) {
+ return shallow<GitHubProjectCreateRendererProps>(
+ <GitHubProjectCreateRenderer
+ canAdmin={false}
+ error={false}
+ loading={false}
+ loadingRepositories={false}
+ onLoadMore={jest.fn()}
+ onSearch={jest.fn()}
+ onSelectOrganization={jest.fn()}
+ onSelectRepository={jest.fn()}
+ organizations={[]}
+ repositoryPaging={{ total: 0, pageIndex: 1, pageSize: 30 }}
+ searchQuery=""
+ {...props}
+ />
+ );
+}
function shallowRender(props: Partial<ManualProjectCreate['props']> = {}) {
return shallow<ManualProjectCreate>(
- <ManualProjectCreate
- currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
- onProjectCreate={jest.fn()}
- {...props}
- />
+ <ManualProjectCreate onProjectCreate={jest.fn()} {...props} />
);
}
</Button>
</div>
}
- showBreadcrumb={true}
title={
<span
className="text-middle"
</Button>
</div>
}
- showBreadcrumb={true}
title={
<span
className="text-middle"
</Button>
</div>
}
- showBreadcrumb={true}
title={
<span
className="text-middle"
</Button>
</div>
}
- showBreadcrumb={true}
title={
<span
className="text-middle"
</Button>
</div>
}
- showBreadcrumb={true}
title={
<span
className="text-middle"
<Fragment>
<CreateProjectPageHeader
additionalActions={false}
- showBreadcrumb={true}
title={
<span
className="text-middle"
</Button>
</div>
}
- showBreadcrumb={true}
title={
<span
className="text-middle"
</p>
</header>
<div
- className="create-project-modes huge-spacer-top display-flex-space-around"
+ className="create-project-modes huge-spacer-top display-flex-justify-center"
>
<button
className="button button-huge display-flex-column create-project-mode-type-manual"
alt=""
height={80}
src="/images/alm/bitbucket.svg"
- width={80}
/>
<div
className="medium big-spacer-top"
>
- onboarding.create_project.select_method.from_bbs
+ onboarding.create_project.select_method.bitbucket
+ </div>
+ </button>
+ <button
+ className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+ disabled={true}
+ onClick={[Function]}
+ type="button"
+ >
+ <img
+ alt=""
+ height={80}
+ src="/images/alm/github.svg"
+ />
+ <div
+ className="medium big-spacer-top"
+ >
+ onboarding.create_project.select_method.github
+ </div>
+ <div
+ className="text-muted small spacer-top"
+ style={
+ Object {
+ "lineHeight": 1.5,
+ }
+ }
+ >
+ onboarding.create_project.alm_not_configured
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="onboarding.create_project.zero_alm_instances.github"
+ />
</div>
</button>
</div>
</Fragment>
`;
-exports[`should render correctly: loading bbs instances 1`] = `
+exports[`should render correctly: invalid configs 1`] = `
<Fragment>
<header
className="huge-spacer-top big-spacer-bottom padded"
</p>
</header>
<div
- className="create-project-modes huge-spacer-top display-flex-space-around"
+ className="create-project-modes huge-spacer-top display-flex-justify-center"
>
<button
className="button button-huge display-flex-column create-project-mode-type-manual"
alt=""
height={80}
src="/images/alm/bitbucket.svg"
- width={80}
/>
<div
className="medium big-spacer-top"
>
- onboarding.create_project.select_method.from_bbs
+ onboarding.create_project.select_method.bitbucket
</div>
- <span>
- onboarding.create_project.check_bbs_supported
- <i
- className="little-spacer-left spinner"
- />
- </span>
- </button>
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly: no bbs instances 1`] = `
-<Fragment>
- <header
- className="huge-spacer-top big-spacer-bottom padded"
- >
- <h1
- className="text-center huge big-spacer-bottom"
- >
- my_account.create_new.TRK
- </h1>
- <p
- className="text-center big"
- >
- onboarding.create_project.select_method
- </p>
- </header>
- <div
- className="create-project-modes huge-spacer-top display-flex-space-around"
- >
- <button
- className="button button-huge display-flex-column create-project-mode-type-manual"
- onClick={[Function]}
- type="button"
- >
- <img
- alt=""
- height={80}
- src="/images/sonarcloud/analysis/manual.svg"
- />
<div
- className="medium big-spacer-top"
+ className="text-muted small spacer-top"
+ style={
+ Object {
+ "lineHeight": 1.5,
+ }
+ }
>
- onboarding.create_project.select_method.manual
+ onboarding.create_project.alm_not_configured
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="onboarding.create_project.zero_alm_instances.bitbucket"
+ />
</div>
</button>
<button
<img
alt=""
height={80}
- src="/images/alm/bitbucket.svg"
- width={80}
+ src="/images/alm/github.svg"
/>
<div
className="medium big-spacer-top"
>
- onboarding.create_project.select_method.from_bbs
+ onboarding.create_project.select_method.github
</div>
<div
className="text-muted small spacer-top"
}
}
>
- onboarding.create_project.bbs_not_configured
+ onboarding.create_project.alm_not_configured
<HelpTooltip
className="little-spacer-left"
- overlay="onboarding.create_project.zero_bbs_instances"
+ overlay="onboarding.create_project.too_many_alm_instances.github
+ onboarding.create_project.alm_instances_count_X.2"
/>
</div>
</button>
</Fragment>
`;
-exports[`should render correctly: too many bbs instances 1`] = `
+exports[`should render correctly: loading instances 1`] = `
<Fragment>
<header
className="huge-spacer-top big-spacer-bottom padded"
</p>
</header>
<div
- className="create-project-modes huge-spacer-top display-flex-space-around"
+ className="create-project-modes huge-spacer-top display-flex-justify-center"
>
<button
className="button button-huge display-flex-column create-project-mode-type-manual"
alt=""
height={80}
src="/images/alm/bitbucket.svg"
- width={80}
/>
<div
className="medium big-spacer-top"
>
- onboarding.create_project.select_method.from_bbs
+ onboarding.create_project.select_method.bitbucket
</div>
+ <span>
+ onboarding.create_project.check_alm_supported
+ <i
+ className="little-spacer-left spinner"
+ />
+ </span>
+ </button>
+ <button
+ className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+ disabled={true}
+ onClick={[Function]}
+ type="button"
+ >
+ <img
+ alt=""
+ height={80}
+ src="/images/alm/github.svg"
+ />
<div
- className="text-muted small spacer-top"
- style={
- Object {
- "lineHeight": 1.5,
- }
- }
+ className="medium big-spacer-top"
>
- onboarding.create_project.bbs_not_configured
- <HelpTooltip
- className="little-spacer-left"
- overlay="onboarding.create_project.too_many_bbs_instances_X.2"
- />
+ onboarding.create_project.select_method.github
</div>
+ <span>
+ onboarding.create_project.check_alm_supported
+ <i
+ className="little-spacer-left spinner"
+ />
+ </span>
</button>
</div>
</Fragment>
id="create-project"
>
<CreateProjectModeSelection
- bbsBindingCount={0}
+ almCounts={
+ Object {
+ "azure": 0,
+ "bitbucket": 0,
+ "github": 0,
+ "gitlab": 0,
+ }
+ }
loadingBindings={true}
onSelectMode={[Function]}
/>
id="create-project"
>
<ManualProjectCreate
- branchesEnabled={false}
- currentUser={
- Object {
- "groups": Array [],
- "isLoggedIn": true,
- "login": "luke",
- "name": "Skywalker",
- "scmAccounts": Array [],
- }
- }
onProjectCreate={[Function]}
/>
</div>
</Fragment>
`;
+exports[`should render correctly if the GitHub method is selected 1`] = `
+<Fragment>
+ <Helmet
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="my_account.create_new.TRK"
+ titleTemplate="%s"
+ />
+ <A11ySkipTarget
+ anchor="create_project_main"
+ />
+ <div
+ className="page page-limited huge-spacer-bottom position-relative"
+ id="create-project"
+ >
+ <GitHubProjectCreate
+ canAdmin={false}
+ />
+ </div>
+</Fragment>
+`;
+
exports[`should render correctly if the manual method is selected 1`] = `
<Fragment>
<Helmet
id="create-project"
>
<ManualProjectCreate
- branchesEnabled={true}
- currentUser={
- Object {
- "groups": Array [],
- "isLoggedIn": true,
- "login": "luke",
- "name": "Skywalker",
- "scmAccounts": Array [],
- }
- }
onProjectCreate={[Function]}
/>
</div>
</h1>
</header>
`;
-
-exports[`should render correctly: with breadcrumb 1`] = `
-<header
- className="huge-spacer-bottom bordered-bottom overflow-hidden"
->
- <h1
- className="pull-left huge big-spacer-bottom"
- >
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/projects/create"
- >
- my_account.create_new.TRK
- </Link>
- <span
- className="big-spacer-left big-spacer-right slash-separator"
- />
- Foo
- </h1>
-</header>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<div>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle display-flex-center"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height={24}
+ src="/images/alm/github.svg"
+ />
+ onboarding.create_project.github.title
+ </span>
+ }
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <div
+ className="form-field"
+ >
+ <label>
+ onboarding.create_project.github.choose_organization
+ </label>
+ <Alert
+ className="spacer-top"
+ variant="error"
+ >
+ onboarding.create_project.github.no_orgs
+ </Alert>
+ </div>
+ </DeferredSpinner>
+</div>
+`;
+
+exports[`should render correctly: error 1`] = `
+<div>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle display-flex-center"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height={24}
+ src="/images/alm/github.svg"
+ />
+ onboarding.create_project.github.title
+ </span>
+ }
+ />
+ <div
+ className="display-flex-justify-center"
+ >
+ <div
+ className="boxed-group padded width-50 huge-spacer-top"
+ >
+ <h2
+ className="big-spacer-bottom"
+ >
+ onboarding.create_project.github.warning.title
+ </h2>
+ <Alert
+ variant="warning"
+ >
+ onboarding.create_project.github.warning.message
+ </Alert>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: error for admin 1`] = `
+<div>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle display-flex-center"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height={24}
+ src="/images/alm/github.svg"
+ />
+ onboarding.create_project.github.title
+ </span>
+ }
+ />
+ <div
+ className="display-flex-justify-center"
+ >
+ <div
+ className="boxed-group padded width-50 huge-spacer-top"
+ >
+ <h2
+ className="big-spacer-bottom"
+ >
+ onboarding.create_project.github.warning.title
+ </h2>
+ <Alert
+ variant="warning"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_project.github.warning.message_admin"
+ id="onboarding.create_project.github.warning.message_admin"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/admin/settings?category=almintegration"
+ >
+ onboarding.create_project.github.warning.message_admin.link
+ </Link>,
+ }
+ }
+ />
+ </Alert>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: no repositories 1`] = `
+<div>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle display-flex-center"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height={24}
+ src="/images/alm/github.svg"
+ />
+ onboarding.create_project.github.title
+ </span>
+ }
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <div
+ className="form-field"
+ >
+ <label>
+ onboarding.create_project.github.choose_organization
+ </label>
+ <SearchSelect
+ defaultOptions={
+ Array [
+ Object {
+ "label": "org1",
+ "value": "o1",
+ },
+ Object {
+ "label": "org2",
+ "value": "o2",
+ },
+ ]
+ }
+ minimumQueryLength={0}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ value={
+ Object {
+ "label": "org2",
+ "value": "o2",
+ }
+ }
+ />
+ </div>
+ </DeferredSpinner>
+</div>
+`;
+
+exports[`should render correctly: organizations 1`] = `
+<div>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle display-flex-center"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height={24}
+ src="/images/alm/github.svg"
+ />
+ onboarding.create_project.github.title
+ </span>
+ }
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <div
+ className="form-field"
+ >
+ <label>
+ onboarding.create_project.github.choose_organization
+ </label>
+ <SearchSelect
+ defaultOptions={
+ Array [
+ Object {
+ "label": "org1",
+ "value": "o1",
+ },
+ Object {
+ "label": "org2",
+ "value": "o2",
+ },
+ ]
+ }
+ minimumQueryLength={0}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ />
+ </div>
+ </DeferredSpinner>
+</div>
+`;
+
+exports[`should render correctly: repositories 1`] = `
+<div>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle display-flex-center"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height={24}
+ src="/images/alm/github.svg"
+ />
+ onboarding.create_project.github.title
+ </span>
+ }
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <div
+ className="form-field"
+ >
+ <label>
+ onboarding.create_project.github.choose_organization
+ </label>
+ <SearchSelect
+ defaultOptions={
+ Array [
+ Object {
+ "label": "org1",
+ "value": "o1",
+ },
+ Object {
+ "label": "org2",
+ "value": "o2",
+ },
+ ]
+ }
+ minimumQueryLength={0}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ value={
+ Object {
+ "label": "org2",
+ "value": "o2",
+ }
+ }
+ />
+ </div>
+ </DeferredSpinner>
+ <div
+ className="boxed-group padded display-flex-wrap"
+ >
+ <div
+ className="width-100"
+ >
+ <SearchBox
+ className="big-spacer-bottom"
+ onChange={[MockFunction]}
+ placeholder="onboarding.create_project.search_repositories"
+ value=""
+ />
+ </div>
+ <Radio
+ checked={false}
+ className="spacer-top spacer-bottom padded github-repository"
+ disabled={false}
+ key="repo1"
+ onCheck={[MockFunction]}
+ value="repo1"
+ >
+ <div
+ className="big overflow-hidden"
+ title="repository 1"
+ >
+ <div
+ className="overflow-hidden text-ellipsis"
+ >
+ repository 1
+ </div>
+ </div>
+ </Radio>
+ <Radio
+ checked={true}
+ className="spacer-top spacer-bottom padded github-repository"
+ disabled={true}
+ key="repo2"
+ onCheck={[MockFunction]}
+ value="repo2"
+ >
+ <div
+ className="big overflow-hidden"
+ title="repository 1"
+ >
+ <div
+ className="overflow-hidden text-ellipsis"
+ >
+ repository 1
+ </div>
+ <em
+ className="notice text-muted-2 small display-flex-center"
+ >
+ onboarding.create_project.already_imported
+ <CheckIcon
+ className="little-spacer-left"
+ size={12}
+ />
+ </em>
+ </div>
+ </Radio>
+ <Radio
+ checked={true}
+ className="spacer-top spacer-bottom padded github-repository"
+ disabled={false}
+ key="repo3"
+ onCheck={[MockFunction]}
+ value="repo3"
+ >
+ <div
+ className="big overflow-hidden"
+ title="repository 1"
+ >
+ <div
+ className="overflow-hidden text-ellipsis"
+ >
+ repository 1
+ </div>
+ </div>
+ </Radio>
+ <div
+ className="display-flex-justify-center width-100"
+ >
+ <ListFooter
+ count={3}
+ loadMore={[MockFunction]}
+ loading={false}
+ total={0}
+ />
+ </div>
+ </div>
+</div>
+`;
#create-project header {
padding-top: 20px;
-}
-
-.white-page #create-project header {
- background-color: white;
+ background-color: var(--barBackgroundColor);
position: sticky;
top: var(--globalNavHeight);
z-index: var(--pageMainZIndex);
}
-.create-project-modes {
- margin: 0 auto;
- max-width: 500px;
-}
-
.create-project-manual {
display: flex !important;
justify-content: space-between;
width: 250px;
min-height: 40px;
}
+
+.create-project-github-repository {
+ box-sizing: border-box;
+ width: 33.33%;
+}
+
+.create-project-github-repository .notice {
+ display: block;
+ position: absolute;
+}
+
+.create-project-github-repository .notice svg {
+ color: var(--green);
+}
*/
export enum CreateProjectModes {
Manual = 'manual',
- BitbucketServer = 'bitbucket'
+ BitbucketServer = 'bitbucket',
+ GitHub = 'github'
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { BitbucketProject, BitbucketRepository } from '../../types/alm-integration';
+import {
+ BitbucketProject,
+ BitbucketRepository,
+ GithubRepository
+} from '../../types/alm-integration';
export function mockBitbucketProject(overrides: Partial<BitbucketProject> = {}): BitbucketProject {
return {
...overrides
};
}
+
+export function mockGitHubRepository(overrides: Partial<GithubRepository> = {}): GithubRepository {
+ return {
+ id: 'id1234',
+ key: 'key3456',
+ name: 'repository 1',
+ sqProjectKey: '',
+ url: 'owner/repo1',
+ ...overrides
+ };
+}
allShown: boolean;
repositories: BitbucketRepository[];
}>;
+
+export interface GithubOrganization {
+ key: string;
+ name: string;
+}
+
+export interface GithubRepository {
+ id: string;
+ key: string;
+ name: string;
+ url: string;
+ sqProjectKey: string;
+}
zip.doLast {
def minLength = 220000000
def maxLength = 235000000
+
def length = archiveFile.get().asFile.length()
if (length < minLength)
throw new GradleException("$archiveName size ($length) too small. Min is $minLength")
onboarding.project_analysis.description=We initialized your project on {instance}, now it's up to you to launch analyses!
onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to integrating with Pipelines
-onboarding.create_project.setup_manually=Create manually
+onboarding.create_project.setup_manually=Create a project
+onboarding.create_project.select_method.manual=Manually
+onboarding.create_project.select_method.bitbucket=From Bitbucket Server
+onboarding.create_project.select_method.github=From GitHub Enterprise
+onboarding.create_project.alm_not_configured=Currently not active
+onboarding.create_project.check_alm_supported=Checking if available
onboarding.create_project.project_key=Project key
onboarding.create_project.project_key.description=Up to 400 characters. Allowed characters are alphanumeric, '-' (dash), '_' (underscore), '.' (period) and ':' (colon), with at least one non-digit.
onboarding.create_project.project_key.error.empty=You must provide at least one character.
onboarding.create_project.repository_imported=Already set up
onboarding.create_project.see_project=See the project
onboarding.create_project.search_repositories_by_name=Search for repository name starting with...
+onboarding.create_project.search_repositories=Search for a repository
onboarding.create_project.select_repositories=Select repositories
onboarding.create_project.select_all_repositories=Select all available repositories
-onboarding.create_project.from_bbs=From Bitbucket Server
+onboarding.create_project.from_bbs=Create a project from Bitbucket Server
onboarding.create_project.grant_access_to_bbs.title=Grant access to your repositories
onboarding.create_project.grant_access_to_bbs.help=SonarQube needs a personal access token to access and list your repositories from Bitbucket Server.
onboarding.create_project.select_method=How do you want to create your project?
-onboarding.create_project.select_method.manual=Manually
-onboarding.create_project.select_method.from_bbs=From a Bitbucket Server repository
-onboarding.create_project.check_bbs_supported=Checking if available
-onboarding.create_project.too_many_bbs_instances_X=You must have exactly 1 Bitbucket Server instance configured in order to use this method. You currently have {0}.
-onboarding.create_project.zero_bbs_instances=You must first configure a Bitbucket Server instance.
-onboarding.create_project.bbs_not_configured=Currently not active
+onboarding.create_project.too_many_alm_instances.bitbucket=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
+onboarding.create_project.too_many_alm_instances.github=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
+onboarding.create_project.alm_instances_count_X=You currently have {0}.
+onboarding.create_project.zero_alm_instances.bitbucket=You must first configure a Bitbucket Server instance.
+onboarding.create_project.zero_alm_instances.github=You must first configure a GitHub Enterprise instance.
onboarding.create_project.no_bbs_binding=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator.
onboarding.create_project.no_bbs_binding.admin=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method. You can configure instances under {url}.
onboarding.create_project.enter_pat=Enter personal access token
onboarding.create_project.only_showing_X_first_repos=We're only displaying the first {0} repositories. If you're looking for a repository that's not in this list, use the search above.
onboarding.create_project.import_selected_repo=Set up selected repository
onboarding.create_project.go_to_project=Go to project
+onboarding.create_project.github.title=Which GitHub repository do you want to setup?
+onboarding.create_project.github.choose_organization=Choose organization
+onboarding.create_project.github.warning.title=Could not connect to GitHub Enterprise
+onboarding.create_project.github.warning.message=Please contact an administrator to configure GitHub Enterprise integration.
+onboarding.create_project.github.warning.message_admin=Please make sure a GitHub Enterprise instance is configured in the {link} to create a new project from a repository.
+onboarding.create_project.github.warning.message_admin.link=ALM integration settings
+onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator.
+onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations with your key. Check the GitHub Enterprise instance configured in the {link}.
onboarding.create_organization.page.header=Create Organization
onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.