]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13475 List GitHub repositories
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 16 Jun 2020 15:48:08 +0000 (17:48 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 1 Jul 2020 20:05:53 +0000 (20:05 +0000)
25 files changed:
server/sonar-web/src/main/js/api/alm-integrations.ts
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPageHeader.tsx
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/style.css
server/sonar-web/src/main/js/apps/create/project/types.ts
server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
server/sonar-web/src/main/js/types/alm-integration.ts
sonar-application/build.gradle
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index dd8e6388c909cbc7d4e41929e9995ab91f1ebc01..3b5712b5d225684cd61380f114e14edc87d5fda5 100644 (file)
  */
 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> {
@@ -29,7 +34,7 @@ export function setAlmPersonalAccessToken(almSetting: string, pat: string): Prom
 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 {
@@ -81,3 +86,31 @@ export function searchForBitbucketServerRepositories(
     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
+  });
+}
index 94afa8a8d68dd036a91f989fc7d55d4df21a3c58..878a11b609c98eddbf469caed86c97c85053c95b 100644 (file)
@@ -88,7 +88,6 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
             </div>
           )
         }
-        showBreadcrumb={true}
         title={
           <span className="text-middle">
             <img
index c8155b434f42446ded32421adb4804e5dffa9024..43229c38eeffe45d5c6e6551414e1496391b884e 100644 (file)
@@ -22,18 +22,72 @@ import * as React from 'react';
 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">
@@ -43,7 +97,7 @@ export default function CreateProjectModeSelection(props: CreateProjectModeSelec
         <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)}
@@ -58,48 +112,8 @@ export default function CreateProjectModeSelection(props: CreateProjectModeSelec
           </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>
     </>
   );
index 278221707486ae3af0bc81bb95296a525f300eb5..5737a0609330e5279ed5ce98447419344be6d6d1 100644 (file)
@@ -21,7 +21,6 @@ import * as React from 'react';
 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';
@@ -30,50 +29,38 @@ import { getProjectUrl } from '../../../helpers/urls';
 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 = () => {
@@ -83,6 +70,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
         if (this.mounted) {
           this.setState({
             bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.Bitbucket),
+            githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
             loading: false
           });
         }
@@ -108,47 +96,67 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
     });
   };
 
+  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>
       </>
     );
index 1cbd7c569183f861935ab791e6be0d7c475606ab..5b2616bc4220e9939c3cf9827ced0c9f218e80da 100644 (file)
  * 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>
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
new file mode 100644 (file)
index 0000000..3417541
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * 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}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
new file mode 100644 (file)
index 0000000..289bdf4
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * 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>
+  );
+}
index 65212ebb525d8006cff8ea6ad8ac13b167685d8f..6ae7d229bb3e6c8894a874cfe1c18c8a187619d7 100644 (file)
@@ -33,8 +33,6 @@ import CreateProjectPageHeader from './CreateProjectPageHeader';
 import './ManualProjectCreate.css';
 
 interface Props {
-  branchesEnabled?: boolean;
-  currentUser: T.LoggedInUser;
   onProjectCreate: (projectKeys: string[]) => void;
 }
 
@@ -182,16 +180,12 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
       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">
index d97f54cf8d0b4d80a80ded29c381fd4acb718e7b..2e11297a12d6d7cb6b0116523e2a0200e01ac2b4 100644 (file)
@@ -21,6 +21,7 @@
 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';
@@ -28,9 +29,10 @@ import { CreateProjectModes } from '../types';
 
 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', () => {
@@ -40,14 +42,27 @@ 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}
index 6ed0a1e9459780e034b5ee66f105114ceb871111..07e45b72fe6d909d73d5a2329bd25797992d45c6 100644 (file)
@@ -20,7 +20,6 @@
 
 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';
@@ -31,11 +30,6 @@ jest.mock('../../../../api/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', () => {
@@ -57,7 +51,6 @@ it('should render correctly if the manual 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 BBS method is selected', () => {
@@ -69,7 +62,17 @@ 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']> = {}) {
index 8c4c7fb8d41873ffbd48c5b157bac359a12a2417..e0b99089532c387bf99c48e57f5dd2d6a2bc4041 100644 (file)
@@ -24,7 +24,6 @@ import CreateProjectPageHeader, { CreateProjectPageHeaderProps } from '../Create
 
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ showBreadcrumb: true })).toMatchSnapshot('with breadcrumb');
   expect(shallowRender({ additionalActions: 'Bar' })).toMatchSnapshot('additional content');
 });
 
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.tsx
new file mode 100644 (file)
index 0000000..36203dc
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * 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}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
new file mode 100644 (file)
index 0000000..b6a0ec8
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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}
+    />
+  );
+}
index 33123a28e998c1dacc5313f06474fcc033954d0c..9eeb5f26cacfbb2a5b74a4a9c0e0046d4b068a35 100644 (file)
@@ -144,10 +144,6 @@ it('should have an error when the name is incorrect', () => {
 
 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} />
   );
 }
index 57c131750140589674dcd0a9e484f2e9ba0a443c..5b0b50bc580539c63cc8f18f708c2cc0adb7fd31 100644 (file)
@@ -21,7 +21,6 @@ exports[`should render correctly: default 1`] = `
         </Button>
       </div>
     }
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
@@ -90,7 +89,6 @@ exports[`should render correctly: importing 1`] = `
         </Button>
       </div>
     }
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
@@ -159,7 +157,6 @@ exports[`should render correctly: invalid config, admin user 1`] = `
         </Button>
       </div>
     }
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
@@ -224,7 +221,6 @@ exports[`should render correctly: invalid config, regular user 1`] = `
         </Button>
       </div>
     }
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
@@ -268,7 +264,6 @@ exports[`should render correctly: loading 1`] = `
         </Button>
       </div>
     }
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
@@ -293,7 +288,6 @@ exports[`should render correctly: pat form 1`] = `
 <Fragment>
   <CreateProjectPageHeader
     additionalActions={false}
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
@@ -342,7 +336,6 @@ exports[`should render correctly: selected repo 1`] = `
         </Button>
       </div>
     }
-    showBreadcrumb={true}
     title={
       <span
         className="text-middle"
index e9d79c11a0e12ad3a3cc9d06399b60c05f307b5d..b0e0b0f9f3d52eef83d4ae41901cd5e21d71723b 100644 (file)
@@ -17,7 +17,7 @@ exports[`should render correctly: default 1`] = `
     </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"
@@ -45,19 +45,49 @@ exports[`should render correctly: default 1`] = `
         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"
@@ -74,7 +104,7 @@ exports[`should render correctly: loading bbs instances 1`] = `
     </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"
@@ -102,57 +132,25 @@ exports[`should render correctly: loading bbs instances 1`] = `
         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
@@ -164,13 +162,12 @@ exports[`should render correctly: no bbs instances 1`] = `
       <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"
@@ -180,10 +177,11 @@ exports[`should render correctly: no bbs instances 1`] = `
           }
         }
       >
-        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>
@@ -191,7 +189,7 @@ exports[`should render correctly: no bbs instances 1`] = `
 </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"
@@ -208,7 +206,7 @@ exports[`should render correctly: too many bbs instances 1`] = `
     </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"
@@ -236,27 +234,41 @@ exports[`should render correctly: too many bbs instances 1`] = `
         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>
index 99b42043c24cf870428027d1189483639bc2e056..a7b160cc0bd754ae1d0d55fef93874619a74c686 100644 (file)
@@ -16,7 +16,14 @@ exports[`should render correctly 1`] = `
     id="create-project"
   >
     <CreateProjectModeSelection
-      bbsBindingCount={0}
+      almCounts={
+        Object {
+          "azure": 0,
+          "bitbucket": 0,
+          "github": 0,
+          "gitlab": 0,
+        }
+      }
       loadingBindings={true}
       onSelectMode={[Function]}
     />
@@ -40,16 +47,6 @@ exports[`should render correctly if no branch support 1`] = `
     id="create-project"
   >
     <ManualProjectCreate
-      branchesEnabled={false}
-      currentUser={
-        Object {
-          "groups": Array [],
-          "isLoggedIn": true,
-          "login": "luke",
-          "name": "Skywalker",
-          "scmAccounts": Array [],
-        }
-      }
       onProjectCreate={[Function]}
     />
   </div>
@@ -93,6 +90,28 @@ exports[`should render correctly if the BBS method is selected 1`] = `
 </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
@@ -109,16 +128,6 @@ exports[`should render correctly if the manual method is selected 1`] = `
     id="create-project"
   >
     <ManualProjectCreate
-      branchesEnabled={true}
-      currentUser={
-        Object {
-          "groups": Array [],
-          "isLoggedIn": true,
-          "login": "luke",
-          "name": "Skywalker",
-          "scmAccounts": Array [],
-        }
-      }
       onProjectCreate={[Function]}
     />
   </div>
index 0dbf8413a9c78b189481d949c2d28ac70e8c6cb3..c8d161620b0d82e5f273a55a6029391654d2770b 100644 (file)
@@ -24,25 +24,3 @@ exports[`should render correctly: default 1`] = `
   </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>
-`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..c97790e
--- /dev/null
@@ -0,0 +1,378 @@
+// 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>
+`;
index 10291b37851c29f52aa1e4d6d116e8f07bb25512..7930a3b216e6a8ca468f03a7daa6e3e4f8c0c940 100644 (file)
 
 #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);
+}
index d991cd60d3a5f2fbc51826d921a54d0ad96c37c9..0fe8944a40557863fc3d08cc8d85272343db582b 100644 (file)
@@ -19,5 +19,6 @@
  */
 export enum CreateProjectModes {
   Manual = 'manual',
-  BitbucketServer = 'bitbucket'
+  BitbucketServer = 'bitbucket',
+  GitHub = 'github'
 }
index 6e04e4807cd30806a89606b5f893dcadc8895bbf..b4a1275fb586d589cb5f497a66ccb742a69d1a2f 100644 (file)
  * 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 {
@@ -39,3 +43,14 @@ export function mockBitbucketRepository(
     ...overrides
   };
 }
+
+export function mockGitHubRepository(overrides: Partial<GithubRepository> = {}): GithubRepository {
+  return {
+    id: 'id1234',
+    key: 'key3456',
+    name: 'repository 1',
+    sqProjectKey: '',
+    url: 'owner/repo1',
+    ...overrides
+  };
+}
index e746ef153b180ae70f6118cd08298868a9c3a004..4a2a5864e8c5a56aa8aa4c23907b7113150f6f9b 100644 (file)
@@ -35,3 +35,16 @@ export type BitbucketProjectRepositories = T.Dict<{
   allShown: boolean;
   repositories: BitbucketRepository[];
 }>;
+
+export interface GithubOrganization {
+  key: string;
+  name: string;
+}
+
+export interface GithubRepository {
+  id: string;
+  key: string;
+  name: string;
+  url: string;
+  sqProjectKey: string;
+}
index 572cd7fff791628d22fa076e781c8e7f01993632..cc4cb31561108756fc6fa21cabb583691739f61f 100644 (file)
@@ -179,6 +179,7 @@ zip.doFirst {
 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")
index 99d6119ead352b677a4c07dae60a36a35f7dec46..21a20f561943f65cc1166c6ef00fb0074cc5dfdd 100644 (file)
@@ -3097,7 +3097,12 @@ onboarding.project_analysis.header=Analyze your project
 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.
@@ -3114,18 +3119,18 @@ onboarding.create_project.display_name.help=Some scanners might override the val
 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
@@ -3144,6 +3149,14 @@ onboarding.create_project.no_bbs_repos.filter=No repositories match your filter.
 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.