]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21823 Adding support for gitlab monorepo imports
authorShane Findley <shane.findley@sonarsource.com>
Tue, 9 Apr 2024 14:31:01 +0000 (16:31 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 11 Apr 2024 20:02:48 +0000 (20:02 +0000)
20 files changed:
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/Gitlab/GItlabPersonalAccessTokenForm.tsx
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectSelectionForm.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/MonorepoProjectCreate-it.tsx
server/sonar-web/src/main/js/apps/create/project/components/DopSettingDropdown.tsx
server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoConnectionSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoNoOrganisations.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoOrganisationSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoProjectsList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoRepositorySelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/usePersonalAccessToken.ts
server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx
server/sonar-web/src/main/js/types/alm-settings.ts
server/sonar-web/src/main/js/types/dop-translation.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7b5ad35ffa87485e696cf0a89bc35a9a6c0f88b8..0c23ee8da2cdc4d751f8f846d39ab02539b27c75 100644 (file)
@@ -55,7 +55,7 @@ interface State {
   bitbucketSettings: AlmSettingsInstance[];
   bitbucketCloudSettings: AlmSettingsInstance[];
   githubSettings: DopSetting[];
-  gitlabSettings: AlmSettingsInstance[];
+  gitlabSettings: DopSetting[];
   loading: boolean;
   creatingAlmDefinition?: AlmKeys;
   importProjects?: ImportProjectParam;
@@ -202,9 +202,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
             .filter(({ type }) => type === AlmKeys.BitbucketCloud)
             .map(({ key, type, url }) => ({ alm: type, key, url })),
           githubSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitHub),
-          gitlabSettings: dopSettings
-            .filter(({ type }) => type === AlmKeys.GitLab)
-            .map(({ key, type, url }) => ({ alm: type, key, url })),
+          gitlabSettings: dopSettings.filter(({ type }) => type === AlmKeys.GitLab),
           loading: false,
         });
       })
@@ -324,10 +322,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
         return (
           <GitlabProjectCreate
             canAdmin={!!canAdmin}
-            loadingBindings={loading}
-            location={location}
-            router={router}
-            almInstances={gitlabSettings}
+            dopSettings={gitlabSettings}
+            isLoadingBindings={loading}
             onProjectSetupDone={this.handleProjectSetupDone}
           />
         );
index 529879f1a5b7751ab5e9212229c978f8cf03b52a..a31c97af912ef9d3f111d596932cb709d289e691 100644 (file)
@@ -20,7 +20,6 @@
 import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations';
 import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
-import { LabelValueSelectOption } from '../../../../helpers/search';
 import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration';
 import { AlmSettingsInstance } from '../../../../types/alm-settings';
 import { DopSetting } from '../../../../types/dop-translation';
@@ -30,6 +29,7 @@ import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
 import { CreateProjectModes } from '../types';
 import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
 import { redirectToGithub } from './utils';
+import { LabelValueSelectOption } from 'design-system';
 
 interface Props {
   canAdmin: boolean;
@@ -253,6 +253,7 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
       selectedDopSetting={selectedDopSetting}
       selectedOrganization={selectedOrganization && transformToOption(selectedOrganization)}
       selectedRepository={selectedRepository && transformToOption(selectedRepository)}
+      showOrganizations
     />
   ) : (
     <GitHubProjectCreateRenderer
@@ -290,6 +291,6 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
 function transformToOption({
   key,
   name,
-}: GithubOrganization | GithubRepository): LabelValueSelectOption {
+}: GithubOrganization | GithubRepository): LabelValueSelectOption<string> {
   return { value: key, label: name };
 }
index 2baadacf2d01a1a9b9ee9a5044c9c2ad1349f6b1..483de9465f597ecaae35aefd83f1092eb084834b 100644 (file)
@@ -30,11 +30,11 @@ import {
 import React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { translate } from '../../../../helpers/l10n';
-import { AlmSettingsInstance } from '../../../../types/alm-settings';
 import { usePersonalAccessToken } from '../usePersonalAccessToken';
+import { AlmInstanceBase } from '../../../../types/alm-settings';
 
 interface Props {
-  almSetting: AlmSettingsInstance;
+  almSetting: AlmInstanceBase;
   resetPat: boolean;
   onPersonalAccessTokenCreated: () => void;
 }
index 94dc946da033c01cc8524c5b0abd9449e6caa420..709200e925653aaf8710582198254b4faca8ce11 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 * as React from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { getGitlabProjects } from '../../../../api/alm-integrations';
-import { Location, Router } from '../../../../components/hoc/withRouter';
 import { GitlabProject } from '../../../../types/alm-integration';
-import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { AlmInstanceBase } from '../../../../types/alm-settings';
 import { Paging } from '../../../../types/types';
 import { ImportProjectParam } from '../CreateProjectPage';
 import { CreateProjectModes } from '../types';
 import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer';
+import { DopSetting } from '../../../../types/dop-translation';
+import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
+import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
+import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm';
+import { orderBy } from 'lodash';
+import { LabelValueSelectOption } from 'design-system';
 
 interface Props {
   canAdmin: boolean;
-  loadingBindings: boolean;
-  almInstances: AlmSettingsInstance[];
-  location: Location;
-  router: Router;
+  isLoadingBindings: boolean;
   onProjectSetupDone: (importProjects: ImportProjectParam) => void;
+  dopSettings: DopSetting[];
 }
 
-interface State {
-  loading: boolean;
-  loadingMore: boolean;
-  projects?: GitlabProject[];
-  projectsPaging: Paging;
-  resetPat: boolean;
-  searching: boolean;
-  searchQuery: string;
-  selectedAlmInstance: AlmSettingsInstance;
-  showPersonalAccessTokenForm: boolean;
-}
+const REPOSITORY_PAGE_SIZE = 50;
+const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250;
 
-const GITLAB_PROJECTS_PAGESIZE = 20;
+export default function GitlabProjectCreate(props: Readonly<Props>) {
+  const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props;
 
-export default class GitlabProjectCreate extends React.PureComponent<Props, State> {
-  mounted = false;
+  const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>();
 
-  constructor(props: Props) {
-    super(props);
+  const [isLoadingRepositories, setIsLoadingRepositories] = useState(false);
+  const [isLoadingMoreRepositories, setIsLoadingMoreRepositories] = useState(false);
+  const [repositories, setRepositories] = useState<GitlabProject[]>([]);
+  const [repositoryPaging, setRepositoryPaging] = useState<Paging>({
+    pageSize: REPOSITORY_PAGE_SIZE,
+    total: 0,
+    pageIndex: 1,
+  });
+  const [searchQuery, setSearchQuery] = useState('');
+  const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>();
+  const [selectedRepository, setSelectedRepository] = useState<GitlabProject>();
+  const [resetPersonalAccessToken, setResetPersonalAccessToken] = useState<boolean>(false);
+  const [showPersonalAccessTokenForm, setShowPersonalAccessTokenForm] = useState<boolean>(true);
 
-    this.state = {
-      loading: false,
-      loadingMore: false,
-      projectsPaging: { pageIndex: 1, total: 0, pageSize: GITLAB_PROJECTS_PAGESIZE },
-      resetPat: false,
-      showPersonalAccessTokenForm: true,
-      searching: false,
-      searchQuery: '',
-      selectedAlmInstance: props.almInstances[0],
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    const { almInstances } = this.props;
-    if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) {
-      this.setState({ selectedAlmInstance: almInstances[0] }, () => {
-        this.fetchInitialData().catch(() => {
-          /* noop */
-        });
-      });
+  const location = useLocation();
+  const router = useRouter();
+
+  const isMonorepoSetup = location.query?.mono === 'true';
+  const hasDopSettings = useMemo(() => {
+    if (dopSettings === undefined) {
+      return false;
     }
-  }
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+    return dopSettings.length > 0;
+  }, [dopSettings]);
+  const repositoryOptions = useMemo(() => {
+    return repositories.map(transformToOption);
+  }, [repositories]);
 
-  fetchInitialData = async () => {
-    const { showPersonalAccessTokenForm } = this.state;
+  const fetchProjects = useCallback(
+    (pageIndex = 1, query?: string) => {
+      if (!selectedDopSetting) {
+        return Promise.resolve(undefined);
+      }
 
+      // eslint-disable-next-line local-rules/no-api-imports
+      return getGitlabProjects({
+        almSetting: selectedDopSetting.key,
+        page: pageIndex,
+        pageSize: REPOSITORY_PAGE_SIZE,
+        query,
+      });
+    },
+    [selectedDopSetting],
+  );
+
+  const fetchInitialData = useCallback(() => {
     if (!showPersonalAccessTokenForm) {
-      this.setState({ loading: true });
-      const result = await this.fetchProjects();
-      if (this.mounted && result) {
-        const { projects, projectsPaging } = result;
-
-        this.setState({
-          loading: false,
-          projects,
-          projectsPaging,
+      setIsLoadingRepositories(true);
+
+      fetchProjects()
+        .then((result) => {
+          if (result?.projects) {
+            setIsLoadingRepositories(false);
+            setRepositories(
+              isMonorepoSetup
+                ? orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc'])
+                : result.projects,
+            );
+            setRepositoryPaging(result.projectsPaging);
+          } else {
+            setIsLoadingRepositories(false);
+          }
+        })
+        .catch(() => {
+          setResetPersonalAccessToken(true);
+          setShowPersonalAccessTokenForm(true);
         });
-      } else {
-        this.setState({
-          loading: false,
+    }
+  }, [fetchProjects, isMonorepoSetup, showPersonalAccessTokenForm]);
+
+  const cleanUrl = useCallback(() => {
+    delete location.query.resetPat;
+    router.replace(location);
+  }, [location, router]);
+
+  const handlePersonalAccessTokenCreated = useCallback(() => {
+    cleanUrl();
+    setShowPersonalAccessTokenForm(false);
+    setResetPersonalAccessToken(false);
+    fetchInitialData();
+  }, [cleanUrl, fetchInitialData]);
+
+  const handleImportRepository = useCallback(
+    (gitlabProjectId: string) => {
+      if (selectedDopSetting) {
+        onProjectSetupDone({
+          almSetting: selectedDopSetting.key,
+          creationMode: CreateProjectModes.GitLab,
+          monorepo: false,
+          projects: [{ gitlabProjectId }],
         });
       }
-    }
-  };
+    },
+    [onProjectSetupDone, selectedDopSetting],
+  );
 
-  handleError = () => {
-    if (this.mounted) {
-      this.setState({ resetPat: true, showPersonalAccessTokenForm: true });
+  const handleLoadMore = useCallback(async () => {
+    setIsLoadingMoreRepositories(true);
+    const result = await fetchProjects(repositoryPaging.pageIndex + 1, searchQuery);
+    if (result?.projects) {
+      setRepositoryPaging(result ? result.projectsPaging : repositoryPaging);
+      setRepositories(result ? [...repositories, ...result.projects] : repositories);
     }
+    setIsLoadingMoreRepositories(false);
+  }, [fetchProjects, repositories, repositoryPaging, searchQuery]);
 
-    return undefined;
-  };
+  const handleSelectRepository = useCallback(
+    (repositoryKey: string) => {
+      setSelectedRepository(repositories.find(({ id }) => id === repositoryKey));
+    },
+    [repositories],
+  );
 
-  fetchProjects = async (pageIndex = 1, query?: string) => {
-    const { selectedAlmInstance } = this.state;
-    if (!selectedAlmInstance) {
-      return Promise.resolve(undefined);
-    }
+  const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => {
+    setSelectedDopSetting(setting);
+    setShowPersonalAccessTokenForm(true);
+    setRepositories([]);
+    setSearchQuery('');
+  }, []);
 
-    try {
-      // eslint-disable-next-line local-rules/no-api-imports
-      return await getGitlabProjects({
-        almSetting: selectedAlmInstance.key,
-        page: pageIndex,
-        pageSize: GITLAB_PROJECTS_PAGESIZE,
-        query,
-      });
-    } catch (_) {
-      return this.handleError();
+  const onSelectedAlmInstanceChange = useCallback(
+    (instance: AlmInstanceBase) => {
+      onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key));
+    },
+    [dopSettings, onSelectDopSetting],
+  );
+
+  useEffect(() => {
+    if (dopSettings.length > 0) {
+      setSelectedDopSetting(dopSettings[0]);
+      return;
     }
-  };
 
-  handleImport = (gitlabProjectId: string) => {
-    const { selectedAlmInstance } = this.state;
+    setSelectedDopSetting(undefined);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [hasDopSettings]);
 
-    if (selectedAlmInstance) {
-      this.props.onProjectSetupDone({
-        creationMode: CreateProjectModes.GitLab,
-        almSetting: selectedAlmInstance.key,
-        monorepo: false,
-        projects: [{ gitlabProjectId }],
-      });
-    }
-  };
-
-  handleLoadMore = async () => {
-    this.setState({ loadingMore: true });
-
-    const {
-      projectsPaging: { pageIndex },
-      searchQuery,
-    } = this.state;
-
-    const result = await this.fetchProjects(pageIndex + 1, searchQuery);
-    if (this.mounted) {
-      this.setState(({ projects = [], projectsPaging }) => ({
-        loadingMore: false,
-        projects: result ? [...projects, ...result.projects] : projects,
-        projectsPaging: result ? result.projectsPaging : projectsPaging,
-      }));
+  useEffect(() => {
+    if (selectedDopSetting) {
+      fetchInitialData();
     }
-  };
-
-  handleSearch = async (searchQuery: string) => {
-    this.setState({ searching: true, searchQuery });
-
-    const result = await this.fetchProjects(1, searchQuery);
-    if (this.mounted) {
-      this.setState(({ projects, projectsPaging }) => ({
-        searching: false,
-        projects: result ? result.projects : projects,
-        projectsPaging: result ? result.projectsPaging : projectsPaging,
-      }));
-    }
-  };
+  }, [fetchInitialData, selectedDopSetting]);
 
-  cleanUrl = () => {
-    const { location, router } = this.props;
-    delete location.query.resetPat;
-    router.replace(location);
-  };
-
-  handlePersonalAccessTokenCreated = () => {
-    this.cleanUrl();
-    this.setState({ showPersonalAccessTokenForm: false, resetPat: false }, () => {
-      this.fetchInitialData();
-    });
-  };
-
-  onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => {
-    this.setState({
-      selectedAlmInstance: instance,
-      showPersonalAccessTokenForm: true,
-      projects: undefined,
-      resetPat: false,
-      searchQuery: '',
-    });
-  };
-
-  render() {
-    const { loadingBindings, location, almInstances, canAdmin } = this.props;
-    const {
-      loading,
-      loadingMore,
-      projects,
-      projectsPaging,
-      resetPat,
-      searching,
-      searchQuery,
-      selectedAlmInstance,
-      showPersonalAccessTokenForm,
-    } = this.state;
-
-    return (
-      <GitlabProjectCreateRenderer
-        canAdmin={canAdmin}
-        almInstances={almInstances}
-        selectedAlmInstance={selectedAlmInstance}
-        loading={loading || loadingBindings}
-        loadingMore={loadingMore}
-        onImport={this.handleImport}
-        onLoadMore={this.handleLoadMore}
-        onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
-        onSearch={this.handleSearch}
-        projects={projects}
-        projectsPaging={projectsPaging}
-        resetPat={resetPat || Boolean(location.query.resetPat)}
-        searching={searching}
-        searchQuery={searchQuery}
-        showPersonalAccessTokenForm={
-          showPersonalAccessTokenForm || Boolean(location.query.resetPat)
+  useEffect(() => {
+    repositorySearchDebounceId.current = setTimeout(async () => {
+      const result = await fetchProjects(1, searchQuery);
+      if (result?.projects) {
+        setRepositories(orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc']));
+        setRepositoryPaging(result.projectsPaging);
+      }
+    }, REPOSITORY_SEARCH_DEBOUNCE_TIME);
+
+    return () => {
+      clearTimeout(repositorySearchDebounceId.current);
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [searchQuery]);
+
+  return isMonorepoSetup ? (
+    <MonorepoProjectCreate
+      canAdmin={canAdmin}
+      dopSettings={dopSettings}
+      error={false}
+      loadingBindings={isLoadingBindings}
+      loadingOrganizations={false}
+      loadingRepositories={isLoadingRepositories}
+      onProjectSetupDone={onProjectSetupDone}
+      onSearchRepositories={setSearchQuery}
+      onSelectDopSetting={onSelectDopSetting}
+      onSelectRepository={handleSelectRepository}
+      personalAccessTokenComponent={
+        !isLoadingRepositories &&
+        selectedDopSetting && (
+          <GitlabPersonalAccessTokenForm
+            almSetting={selectedDopSetting}
+            resetPat={resetPersonalAccessToken}
+            onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated}
+          />
+        )
+      }
+      repositoryOptions={repositoryOptions}
+      repositorySearchQuery={searchQuery}
+      selectedDopSetting={selectedDopSetting}
+      selectedRepository={selectedRepository ? transformToOption(selectedRepository) : undefined}
+      showPersonalAccessToken={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
+    />
+  ) : (
+    <GitlabProjectCreateRenderer
+      almInstances={dopSettings.map((dopSetting) => ({
+        alm: dopSetting.type,
+        key: dopSetting.key,
+        url: dopSetting.url,
+      }))}
+      canAdmin={canAdmin}
+      loading={isLoadingRepositories || isLoadingBindings}
+      loadingMore={isLoadingMoreRepositories}
+      onImport={handleImportRepository}
+      onLoadMore={handleLoadMore}
+      onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated}
+      onSearch={setSearchQuery}
+      onSelectedAlmInstanceChange={onSelectedAlmInstanceChange}
+      projects={repositories}
+      projectsPaging={repositoryPaging}
+      resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)}
+      searching={isLoadingRepositories}
+      searchQuery={searchQuery}
+      selectedAlmInstance={
+        selectedDopSetting && {
+          alm: selectedDopSetting.type,
+          key: selectedDopSetting.key,
+          url: selectedDopSetting.url,
         }
-        onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}
-      />
-    );
-  }
+      }
+      showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
+    />
+  );
+}
+
+function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption<string> {
+  return { value: id, label: name };
 }
index 5d11f053b3c34403c1835f8c85688cd9389d2435..9d64c069572aec60378802d59f4546bab2ce87f0 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 { LightPrimary, Spinner, Title } from 'design-system';
+import { LightPrimary, Title } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
 import { GitlabProject } from '../../../../types/alm-integration';
-import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
+import { AlmInstanceBase, AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
 import { Paging } from '../../../../types/types';
 import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
 import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
 import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm';
 import GitlabProjectSelectionForm from './GitlabProjectSelectionForm';
+import { FormattedMessage } from 'react-intl';
+import { Link, Spinner } from '@sonarsource/echoes-react';
+import { queryToSearch } from '../../../../helpers/urls';
+import { CreateProjectModes } from '../types';
+import { Feature } from '../../../../types/features';
+import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
 
 export interface GitlabProjectCreateRendererProps {
   canAdmin?: boolean;
@@ -44,10 +50,16 @@ export interface GitlabProjectCreateRendererProps {
   almInstances?: AlmSettingsInstance[];
   selectedAlmInstance?: AlmSettingsInstance;
   showPersonalAccessTokenForm?: boolean;
-  onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void;
+  onSelectedAlmInstanceChange: (instance: AlmInstanceBase) => void;
 }
 
-export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
+export default function GitlabProjectCreateRenderer(
+  props: Readonly<GitlabProjectCreateRendererProps>,
+) {
+  const isMonorepoSupported = React.useContext(AvailableFeaturesContext).includes(
+    Feature.MonoRepositoryPullRequestDecoration,
+  );
+
   const {
     canAdmin,
     loading,
@@ -67,7 +79,28 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
       <header className="sw-mb-10">
         <Title className="sw-mb-4">{translate('onboarding.create_project.gitlab.title')}</Title>
         <LightPrimary className="sw-body-sm">
-          {translate('onboarding.create_project.gitlab.subtitle')}
+          {isMonorepoSupported ? (
+            <FormattedMessage
+              id="onboarding.create_project.gitlab.subtitle.with_monorepo"
+              values={{
+                monorepoSetupLink: (
+                  <Link
+                    to={{
+                      pathname: '/projects/create',
+                      search: queryToSearch({
+                        mode: CreateProjectModes.GitLab,
+                        mono: true,
+                      }),
+                    }}
+                  >
+                    <FormattedMessage id="onboarding.create_project.gitlab.subtitle.link" />
+                  </Link>
+                ),
+              }}
+            />
+          ) : (
+            <FormattedMessage id="onboarding.create_project.gitlab.subtitle" />
+          )}
         </LightPrimary>
       </header>
 
@@ -78,7 +111,7 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
         onChangeConfig={props.onSelectedAlmInstanceChange}
       />
 
-      <Spinner loading={loading} />
+      <Spinner isLoading={loading} />
 
       {!loading && !selectedAlmInstance && (
         <WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} />
index 7d83e80aaa3ea8460d602e4cf5f667cf5cf4228a..76dc41a54ef31c48b48c74e31d4b3bfa75a51e23 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { FlagMessage, InputSearch, LightPrimary, Link } from 'design-system';
+import { FlagMessage, InputSearch, LightPrimary } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import ListFooter from '../../../../components/controls/ListFooter';
@@ -29,6 +29,7 @@ import { GitlabProject } from '../../../../types/alm-integration';
 import { Paging } from '../../../../types/types';
 import AlmRepoItem from '../components/AlmRepoItem';
 import { CreateProjectModes } from '../types';
+import { Link } from '@sonarsource/echoes-react';
 
 export interface GitlabProjectSelectionFormProps {
   loadingMore: boolean;
@@ -41,7 +42,9 @@ export interface GitlabProjectSelectionFormProps {
   searchQuery: string;
 }
 
-export default function GitlabProjectSelectionForm(props: GitlabProjectSelectionFormProps) {
+export default function GitlabProjectSelectionForm(
+  props: Readonly<GitlabProjectSelectionFormProps>,
+) {
   const { loadingMore, projects = [], projectsPaging, searching, searchQuery } = props;
 
   if (projects.length === 0 && searchQuery.length === 0 && !searching) {
index 5853b155855072477848d9d419eb502d47e34591..356e373eb25f06ae40a6b05b00cf99e8e4a874ba 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { screen, within } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import selectEvent from 'react-select-event';
@@ -28,6 +28,8 @@ import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitio
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import CreateProjectPage from '../CreateProjectPage';
+import { Feature } from '../../../../types/features';
+import { CreateProjectModes } from '../types';
 
 jest.mock('../../../../api/alm-integrations');
 jest.mock('../../../../api/alm-settings');
@@ -37,12 +39,16 @@ let dopTranslationHandler: DopTranslationServiceMock;
 let newCodePeriodHandler: NewCodeDefinitionServiceMock;
 
 const ui = {
+  cancelButton: byRole('button', { name: 'cancel' }),
   gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'),
+  gitLabOnboardingTitle: byRole('heading', { name: 'onboarding.create_project.gitlab.title' }),
+  instanceSelector: byLabelText(/alm.configuration.selector.label/),
+  monorepoSetupLink: byRole('link', { name: 'onboarding.create_project.gitlab.subtitle.link' }),
+  monorepoTitle: byRole('heading', { name: 'onboarding.create_project.monorepo.titlealm.gitlab' }),
 
   personalAccessTokenInput: byRole('textbox', {
     name: /onboarding.create_project.enter_pat/,
   }),
-  instanceSelector: byLabelText(/alm.configuration.selector.label/),
 };
 
 const original = window.location;
@@ -145,17 +151,18 @@ it('should show search filter when PAT is already set', async () => {
   await user.click(inputSearch);
   await user.keyboard('sea');
 
+  await waitFor(() => expect(getGitlabProjects).toHaveBeenCalledTimes(2));
   expect(getGitlabProjects).toHaveBeenLastCalledWith({
     almSetting: 'conf-final-2',
     page: 1,
-    pageSize: 20,
+    pageSize: 50,
     query: 'sea',
   });
 });
 
 it('should have load more', async () => {
   const user = userEvent.setup();
-  almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(10, 20);
+  almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(50, 75);
   renderCreateProject();
 
   expect(await screen.findByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
@@ -167,12 +174,12 @@ it('should have load more', async () => {
    * Next api call response will simulate reaching the last page so we can test the
    * loadmore button disapperance.
    */
-  almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(20, 20);
+  almIntegrationHandler.createRandomGitlabProjectsWithLoadMore(50, 50);
   await user.click(loadMore);
   expect(getGitlabProjects).toHaveBeenLastCalledWith({
     almSetting: 'conf-final-2',
     page: 2,
-    pageSize: 20,
+    pageSize: 50,
     query: '',
   });
   expect(loadMore).not.toBeInTheDocument();
@@ -190,8 +197,38 @@ it('should show no result message when there are no projects', async () => {
   ).toBeInTheDocument();
 });
 
-function renderCreateProject() {
-  renderApp('project/create', <CreateProjectPage />, {
-    navigateTo: 'project/create?mode=gitlab',
+describe('GitLab monorepo project navigation', () => {
+  it('should be able to access monorepo setup page from GitLab project import page', async () => {
+    const user = userEvent.setup();
+    renderCreateProject();
+
+    await user.click(await ui.monorepoSetupLink.find());
+
+    expect(ui.monorepoTitle.get()).toBeInTheDocument();
+  });
+
+  it('should be able to go back to GitLab onboarding page from monorepo setup page', async () => {
+    const user = userEvent.setup();
+    renderCreateProject({ isMonorepo: true });
+
+    await user.click(await ui.cancelButton.find());
+
+    expect(ui.gitLabOnboardingTitle.get()).toBeInTheDocument();
+  });
+});
+
+function renderCreateProject({
+  isMonorepo = false,
+}: {
+  isMonorepo?: boolean;
+} = {}) {
+  let queryString = `mode=${CreateProjectModes.GitLab}`;
+  if (isMonorepo) {
+    queryString += '&mono=true';
+  }
+
+  renderApp('projects/create', <CreateProjectPage />, {
+    navigateTo: `projects/create?${queryString}`,
+    featureList: [Feature.MonoRepositoryPullRequestDecoration],
   });
 }
index bdd41870f991c94e1fb9daa07e1323eb75289793..7dd30b62b0bd28b48664d619011bcd48594eedcb 100644 (file)
@@ -51,7 +51,7 @@ const ui = {
   addButton: byRole('button', { name: 'onboarding.create_project.monorepo.add_project' }),
   cancelButton: byRole('button', { name: 'cancel' }),
   dopSettingSelector: byRole('combobox', {
-    name: `onboarding.create_project.monorepo.choose_dop_setting.${AlmKeys.GitHub}`,
+    name: `onboarding.create_project.monorepo.choose_dop_settingalm.${AlmKeys.GitHub}`,
   }),
   gitHubOnboardingTitle: byRole('heading', { name: 'onboarding.create_project.github.title' }),
   monorepoProjectTitle: byRole('heading', {
@@ -60,11 +60,11 @@ const ui = {
   monorepoSetupLink: byRole('link', { name: 'onboarding.create_project.github.subtitle.link' }),
   monorepoTitle: byRole('heading', { name: 'onboarding.create_project.monorepo.titlealm.github' }),
   organizationSelector: byRole('combobox', {
-    name: `onboarding.create_project.monorepo.choose_organization.${AlmKeys.GitHub}`,
+    name: `onboarding.create_project.monorepo.choose_organization`,
   }),
   removeButton: byRole('button', { name: 'onboarding.create_project.monorepo.remove_project' }),
   repositorySelector: byRole('combobox', {
-    name: `onboarding.create_project.monorepo.choose_repository.${AlmKeys.GitHub}`,
+    name: `onboarding.create_project.monorepo.choose_repository`,
   }),
   notBoundRepositoryMessage: byText(
     'onboarding.create_project.monorepo.choose_repository.no_already_bound_projects',
index f0400c500753e91c9c25c06ad884024e63f20e60..599318c0894e99106f51d97ee0a95193485de2a9 100644 (file)
@@ -20,7 +20,7 @@
 import classNames from 'classnames';
 import { DarkLabel, InputSelect, LabelValueSelectOption, Note } from 'design-system';
 import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
 import { OptionProps, SingleValueProps, components } from 'react-select';
 import { translate } from '../../../../helpers/l10n';
 import { AlmKeys } from '../../../../types/alm-settings';
@@ -62,6 +62,8 @@ function orgToOption(alm: DopSetting) {
 }
 
 export default function DopSettingDropdown(props: Readonly<DopSettingDropdownProps>) {
+  const { formatMessage } = useIntl();
+
   const { almKey, className, dopSettings, onChangeSetting, selectedDopSetting } = props;
   if (!dopSettings || dopSettings.length < MIN_SIZE_INSTANCES) {
     return null;
@@ -70,7 +72,10 @@ export default function DopSettingDropdown(props: Readonly<DopSettingDropdownPro
   return (
     <div className={classNames('sw-flex sw-flex-col', className)}>
       <DarkLabel htmlFor="dop-setting-dropdown" className="sw-mb-2">
-        <FormattedMessage id={`onboarding.create_project.monorepo.choose_dop_setting.${almKey}`} />
+        <FormattedMessage
+          id="onboarding.create_project.monorepo.choose_dop_setting"
+          values={{ almKey: formatMessage({ id: `alm.${almKey}` }) }}
+        />
       </DarkLabel>
 
       <InputSelect
diff --git a/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoConnectionSelector.tsx b/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoConnectionSelector.tsx
new file mode 100644 (file)
index 0000000..fbfbcfd
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { Title } from 'design-system/lib';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import DopSettingDropdown from '../components/DopSettingDropdown';
+import { MonorepoOrganisationSelector } from './MonorepoOrganisationSelector';
+import { MonorepoRepositorySelector } from './MonorepoRepositorySelector';
+import { LabelValueSelectOption } from '../../../../helpers/search';
+import { DopSetting } from '../../../../types/dop-translation';
+import { AlmKeys } from '../../../../types/alm-settings';
+import MonorepoNoOrganisations from './MonorepoNoOrganisations';
+
+interface Props {
+  almKey: AlmKeys;
+  alreadyBoundProjects: {
+    projectId: string;
+    projectName: string;
+  }[];
+  canAdmin: boolean;
+  dopSettings: DopSetting[];
+  error: boolean;
+  isFetchingAlreadyBoundProjects: boolean;
+  isLoadingAlreadyBoundProjects: boolean;
+  loadingBindings: boolean;
+  loadingOrganizations?: boolean;
+  loadingRepositories: boolean;
+  onSearchRepositories: (query: string) => void;
+  onSelectDopSetting: (instance: DopSetting) => void;
+  onSelectOrganization?: (organizationKey: string) => void;
+  onSelectRepository: (repositoryKey: string) => void;
+  organizationOptions?: LabelValueSelectOption[];
+  personalAccessTokenComponent?: React.ReactNode;
+  repositoryOptions?: LabelValueSelectOption[];
+  repositorySearchQuery: string;
+  selectedDopSetting?: DopSetting;
+  selectedOrganization?: LabelValueSelectOption;
+  selectedRepository?: LabelValueSelectOption;
+  showPersonalAccessToken?: boolean;
+  showOrganizations?: boolean;
+}
+
+export function MonorepoConnectionSelector({
+  almKey,
+  alreadyBoundProjects,
+  canAdmin,
+  dopSettings,
+  error,
+  isFetchingAlreadyBoundProjects,
+  isLoadingAlreadyBoundProjects,
+  loadingOrganizations,
+  loadingRepositories,
+  onSearchRepositories,
+  onSelectDopSetting,
+  onSelectOrganization,
+  onSelectRepository,
+  organizationOptions,
+  personalAccessTokenComponent,
+  repositoryOptions,
+  repositorySearchQuery,
+  selectedDopSetting,
+  selectedOrganization,
+  selectedRepository,
+  showPersonalAccessToken,
+  showOrganizations,
+}: Readonly<Props>) {
+  return (
+    <div className="sw-flex sw-flex-col sw-gap-6">
+      <Title>
+        <FormattedMessage
+          id={
+            showOrganizations
+              ? 'onboarding.create_project.monorepo.choose_organization_and_repository'
+              : 'onboarding.create_project.monorepo.choose_repository'
+          }
+        />
+      </Title>
+
+      <DopSettingDropdown
+        almKey={almKey}
+        dopSettings={dopSettings}
+        selectedDopSetting={selectedDopSetting}
+        onChangeSetting={onSelectDopSetting}
+      />
+
+      {showPersonalAccessToken ? (
+        personalAccessTokenComponent
+      ) : (
+        <>
+          {showOrganizations && error && selectedDopSetting && !loadingOrganizations && (
+            <MonorepoNoOrganisations almKey={almKey} canAdmin={canAdmin} />
+          )}
+
+          {showOrganizations && organizationOptions && (
+            <div className="sw-flex sw-flex-col">
+              <MonorepoOrganisationSelector
+                almKey={almKey}
+                canAdmin={canAdmin}
+                error={error}
+                organizationOptions={organizationOptions}
+                loadingOrganizations={loadingOrganizations}
+                onSelectOrganization={onSelectOrganization}
+                selectedOrganization={selectedOrganization}
+              />
+            </div>
+          )}
+
+          <div className="sw-flex sw-flex-col">
+            <MonorepoRepositorySelector
+              almKey={almKey}
+              alreadyBoundProjects={alreadyBoundProjects}
+              error={error}
+              isFetchingAlreadyBoundProjects={isFetchingAlreadyBoundProjects}
+              isLoadingAlreadyBoundProjects={isLoadingAlreadyBoundProjects}
+              loadingRepositories={loadingRepositories}
+              onSelectRepository={onSelectRepository}
+              onSearchRepositories={onSearchRepositories}
+              repositoryOptions={repositoryOptions}
+              repositorySearchQuery={repositorySearchQuery}
+              selectedOrganization={selectedOrganization}
+              selectedRepository={selectedRepository}
+              showOrganizations={showOrganizations}
+            />
+          </div>
+        </>
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoNoOrganisations.tsx b/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoNoOrganisations.tsx
new file mode 100644 (file)
index 0000000..37fb214
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { Link } from '@sonarsource/echoes-react';
+import { FlagMessage } from 'design-system/lib';
+import React from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { AlmKeys } from '../../../../types/alm-settings';
+
+export default function MonorepoNoOrganisations({
+  almKey,
+  canAdmin,
+}: Readonly<{ almKey: AlmKeys; canAdmin: boolean }>) {
+  const { formatMessage } = useIntl();
+
+  return (
+    <FlagMessage variant="warning">
+      <span>
+        {canAdmin ? (
+          <FormattedMessage
+            id="onboarding.create_project.monorepo.warning.message_admin"
+            defaultMessage={formatMessage({
+              id: 'onboarding.create_project.monorepo.warning.message_admin',
+            })}
+            values={{
+              almKey: formatMessage({ id: `alm.${almKey}` }),
+              link: (
+                <Link to="/admin/settings?category=almintegration">
+                  <FormattedMessage id="onboarding.create_project.monorepo.warning.message_admin.link" />
+                </Link>
+              ),
+            }}
+          />
+        ) : (
+          <FormattedMessage
+            id="onboarding.create_project.monorepo.warning.message"
+            values={{ almKey: formatMessage({ id: `alm.${almKey}` }) }}
+          />
+        )}
+      </span>
+    </FlagMessage>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoOrganisationSelector.tsx b/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoOrganisationSelector.tsx
new file mode 100644 (file)
index 0000000..3511810
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { Link, Spinner } from '@sonarsource/echoes-react';
+import { DarkLabel, FlagMessage, InputSelect } from 'design-system';
+import React from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { LabelValueSelectOption } from '../../../../helpers/search';
+import { AlmKeys } from '../../../../types/alm-settings';
+
+interface Props {
+  almKey: AlmKeys;
+  canAdmin: boolean;
+  error: boolean;
+  loadingOrganizations?: boolean;
+  onSelectOrganization?: (organizationKey: string) => void;
+  organizationOptions: LabelValueSelectOption[];
+  selectedOrganization?: LabelValueSelectOption;
+}
+
+export function MonorepoOrganisationSelector({
+  almKey,
+  canAdmin,
+  error,
+  loadingOrganizations,
+  onSelectOrganization,
+  organizationOptions,
+  selectedOrganization,
+}: Readonly<Props>) {
+  const { formatMessage } = useIntl();
+
+  return (
+    !error && (
+      <>
+        <DarkLabel htmlFor={`${almKey}-monorepo-choose-organization`} className="sw-mb-2">
+          <FormattedMessage id="onboarding.create_project.monorepo.choose_organization" />
+        </DarkLabel>
+
+        <Spinner isLoading={loadingOrganizations && !error}>
+          {organizationOptions.length > 0 ? (
+            <InputSelect
+              size="large"
+              isSearchable
+              inputId={`${almKey}-monorepo-choose-organization`}
+              options={organizationOptions}
+              onChange={({ value }: LabelValueSelectOption) => {
+                if (onSelectOrganization) {
+                  onSelectOrganization(value);
+                }
+              }}
+              placeholder={formatMessage({
+                id: 'onboarding.create_project.monorepo.choose_organization.placeholder',
+              })}
+              value={selectedOrganization}
+            />
+          ) : (
+            !loadingOrganizations && (
+              <FlagMessage variant="error" className="sw-mb-2">
+                <span>
+                  {canAdmin ? (
+                    <FormattedMessage
+                      id="onboarding.create_project.monorepo.no_orgs_admin"
+                      defaultMessage={formatMessage({
+                        id: 'onboarding.create_project.monorepo.no_orgs_admin',
+                      })}
+                      values={{
+                        almKey,
+                        link: (
+                          <Link to="/admin/settings?category=almintegration">
+                            <FormattedMessage id="onboarding.create_project.monorepo.warning.message_admin.link" />
+                          </Link>
+                        ),
+                      }}
+                    />
+                  ) : (
+                    <FormattedMessage id="onboarding.create_project.monorepo.no_orgs" />
+                  )}
+                </span>
+              </FlagMessage>
+            )
+          )}
+        </Spinner>
+      </>
+    )
+  );
+}
index 4ec9b5e04707019f849207db3d0c8b0ab2f53de4..a3b1232dc4b2b505702a234d210d3b78dd2ab947 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 { Link, LinkHighlight, LinkStandalone, Spinner } from '@sonarsource/echoes-react';
-import {
-  AddNewIcon,
-  BlueGreySeparator,
-  ButtonPrimary,
-  ButtonSecondary,
-  DarkLabel,
-  FlagMessage,
-  InputSelect,
-  SubTitle,
-  Title,
-} from 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
+import { BlueGreySeparator, ButtonPrimary, ButtonSecondary } from 'design-system';
 import React, { useEffect, useRef } from 'react';
-import { FormattedMessage, useIntl } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
 import { getComponents } from '../../../../api/project-management';
 import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
 import { throwGlobalError } from '../../../../helpers/error';
-import { translate } from '../../../../helpers/l10n';
 import { LabelValueSelectOption } from '../../../../helpers/search';
-import { getProjectUrl } from '../../../../helpers/urls';
 import { useProjectBindingsQuery } from '../../../../queries/dop-translation';
 import { AlmKeys } from '../../../../types/alm-settings';
 import { DopSetting } from '../../../../types/dop-translation';
 import { ImportProjectParam } from '../CreateProjectPage';
-import DopSettingDropdown from '../components/DopSettingDropdown';
-import { ProjectData, ProjectValidationCard } from '../components/ProjectValidation';
+import { ProjectData } from '../components/ProjectValidation';
 import { CreateProjectModes } from '../types';
 import { getSanitizedProjectKey } from '../utils';
 import { MonorepoProjectHeader } from './MonorepoProjectHeader';
+import { MonorepoConnectionSelector } from './MonorepoConnectionSelector';
+import { MonorepoProjectsList } from './MonorepoProjectsList';
 
 interface MonorepoProjectCreateProps {
   canAdmin: boolean;
@@ -57,37 +46,29 @@ interface MonorepoProjectCreateProps {
   onProjectSetupDone: (importProjects: ImportProjectParam) => void;
   onSearchRepositories: (query: string) => void;
   onSelectDopSetting: (instance: DopSetting) => void;
-  onSelectOrganization: (organizationKey: string) => void;
-  onSelectRepository: (repositoryIdentifier: string) => void;
+  onSelectOrganization?: (organizationKey: string) => void;
+  onSelectRepository: (repositoryKey: string) => void;
   organizationOptions?: LabelValueSelectOption[];
+  personalAccessTokenComponent?: React.ReactNode;
   repositoryOptions?: LabelValueSelectOption[];
   repositorySearchQuery: string;
   selectedDopSetting?: DopSetting;
   selectedOrganization?: LabelValueSelectOption;
   selectedRepository?: LabelValueSelectOption;
+  showOrganizations?: boolean;
+  showPersonalAccessToken?: boolean;
 }
 
 type ProjectItem = Required<ProjectData<number>>;
 
 export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCreateProps>) {
   const {
-    dopSettings,
-    canAdmin,
-    error,
     loadingBindings,
-    loadingOrganizations,
-    loadingRepositories,
     onProjectSetupDone,
-    onSearchRepositories,
-    onSelectDopSetting,
-    onSelectOrganization,
-    onSelectRepository,
-    organizationOptions,
-    repositoryOptions,
-    repositorySearchQuery,
     selectedDopSetting,
     selectedOrganization,
     selectedRepository,
+    showOrganizations = false,
   } = props;
 
   const projectCounter = useRef(0);
@@ -99,7 +80,6 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre
 
   const location = useLocation();
   const { push } = useRouter();
-  const { formatMessage } = useIntl();
 
   const projectKeys = React.useMemo(() => projects.map(({ key }) => key), [projects]);
   const {
@@ -116,24 +96,26 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre
 
   const almKey = location.query.mode as AlmKeys;
 
+  const isOptionSelectionInvalid =
+    (showOrganizations && selectedOrganization === undefined) || selectedRepository === undefined;
   const isSetupInvalid =
     selectedDopSetting === undefined ||
-    selectedOrganization === undefined ||
-    selectedRepository === undefined ||
+    isOptionSelectionInvalid ||
     projects.length === 0 ||
     projects.some(({ hasError, key, name }) => hasError || key === '' || name === '');
 
-  const addProject = () => {
-    if (selectedOrganization === undefined || selectedRepository === undefined) {
+  const onAddProject = React.useCallback(() => {
+    if (isOptionSelectionInvalid) {
       return;
     }
 
     const id = projectCounter.current;
     projectCounter.current += 1;
-
     const projectKeySuffix = id === 0 ? '' : `-${id}`;
     const projectKey = getSanitizedProjectKey(
-      `${selectedOrganization.label}_${selectedRepository.label}_add-your-reference${projectKeySuffix}`,
+      showOrganizations && selectedOrganization
+        ? `${selectedOrganization.label}_${selectedRepository.label}_add-your-reference${projectKeySuffix}`
+        : `${selectedRepository.label}_add-your-reference${projectKeySuffix}`,
     );
 
     const newProjects = [
@@ -148,28 +130,40 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre
     ];
 
     setProjects(newProjects);
-  };
-
-  const onProjectChange = (project: ProjectItem) => {
-    const newProjects = projects.filter(({ id }) => id !== project.id);
-    newProjects.push({
-      ...project,
-    });
-    newProjects.sort((a, b) => a.id - b.id);
-
-    setProjects(newProjects);
-  };
+  }, [
+    isOptionSelectionInvalid,
+    projects,
+    selectedOrganization,
+    selectedRepository,
+    showOrganizations,
+  ]);
+
+  const onChangeProject = React.useCallback(
+    (project: ProjectItem) => {
+      const newProjects = projects.filter(({ id }) => id !== project.id);
+      newProjects.push({
+        ...project,
+      });
+      newProjects.sort((a, b) => a.id - b.id);
+
+      setProjects(newProjects);
+    },
+    [projects],
+  );
 
-  const onProjectRemove = (id: number) => {
-    const newProjects = projects.filter(({ id: projectId }) => projectId !== id);
+  const onRemoveProject = React.useCallback(
+    (id: number) => {
+      const newProjects = projects.filter(({ id: projectId }) => projectId !== id);
 
-    setProjects(newProjects);
-  };
+      setProjects(newProjects);
+    },
+    [projects],
+  );
 
   const cancelMonorepoSetup = () => {
     push({
       pathname: location.pathname,
-      query: { mode: AlmKeys.GitHub },
+      query: { mode: almKey },
     });
   };
 
@@ -194,7 +188,7 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre
 
   useEffect(() => {
     if (selectedRepository !== undefined && projects.length === 0) {
-      addProject();
+      onAddProject();
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [selectedRepository]);
@@ -233,188 +227,25 @@ export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCre
 
       <BlueGreySeparator className="sw-my-5" />
 
-      <div className="sw-flex sw-flex-col sw-gap-6">
-        <Title>
-          <FormattedMessage
-            id={`onboarding.create_project.monorepo.choose_organization_and_repository.${almKey}`}
-          />
-        </Title>
-
-        <DopSettingDropdown
-          almKey={almKey}
-          dopSettings={dopSettings}
-          selectedDopSetting={selectedDopSetting}
-          onChangeSetting={onSelectDopSetting}
-        />
-
-        {error && selectedDopSetting && !loadingOrganizations && (
-          <FlagMessage variant="warning">
-            <span>
-              {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')
-              )}
-            </span>
-          </FlagMessage>
-        )}
-
-        <div className="sw-flex sw-flex-col">
-          <Spinner isLoading={loadingOrganizations && !error}>
-            {!error && (
-              <>
-                <DarkLabel htmlFor="monorepo-choose-organization" className="sw-mb-2">
-                  <FormattedMessage
-                    id={`onboarding.create_project.monorepo.choose_organization.${almKey}`}
-                  />
-                </DarkLabel>
-                {(organizationOptions?.length ?? 0) > 0 ? (
-                  <InputSelect
-                    size="full"
-                    isSearchable
-                    inputId="monorepo-choose-organization"
-                    options={organizationOptions}
-                    onChange={({ value }: LabelValueSelectOption) => {
-                      onSelectOrganization(value);
-                    }}
-                    placeholder={formatMessage({
-                      id: `onboarding.create_project.monorepo.choose_organization.${almKey}.placeholder`,
-                    })}
-                    value={selectedOrganization}
-                  />
-                ) : (
-                  !loadingOrganizations && (
-                    <FlagMessage variant="error" className="sw-mb-2">
-                      <span>
-                        {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')
-                        )}
-                      </span>
-                    </FlagMessage>
-                  )
-                )}
-              </>
-            )}
-          </Spinner>
-        </div>
-
-        <div className="sw-flex sw-flex-col">
-          {selectedOrganization && (
-            <DarkLabel className="sw-mb-2" htmlFor="monorepo-choose-repository">
-              <FormattedMessage
-                id={`onboarding.create_project.monorepo.choose_repository.${almKey}`}
-              />
-            </DarkLabel>
-          )}
-          {selectedOrganization && (
-            <>
-              <InputSelect
-                inputId="monorepo-choose-repository"
-                inputValue={repositorySearchQuery}
-                isLoading={loadingRepositories}
-                isSearchable
-                noOptionsMessage={() => formatMessage({ id: 'no_results' })}
-                onChange={({ value }: LabelValueSelectOption) => {
-                  onSelectRepository(value);
-                }}
-                onInputChange={onSearchRepositories}
-                options={repositoryOptions}
-                placeholder={formatMessage({
-                  id: `onboarding.create_project.monorepo.choose_repository.${almKey}.placeholder`,
-                })}
-                size="full"
-                value={selectedRepository}
-              />
-              {selectedRepository &&
-                !isLoadingAlreadyBoundProjects &&
-                !isFetchingAlreadyBoundProjects && (
-                  <FlagMessage className="sw-mt-2" variant="info">
-                    {alreadyBoundProjects.length === 0 ? (
-                      <FormattedMessage id="onboarding.create_project.monorepo.choose_repository.no_already_bound_projects" />
-                    ) : (
-                      <div>
-                        <FormattedMessage id="onboarding.create_project.monorepo.choose_repository.existing_already_bound_projects" />
-                        <ul className="sw-mt-4">
-                          {alreadyBoundProjects.map(({ projectId, projectName }) => (
-                            <li key={projectId}>
-                              <LinkStandalone
-                                to={getProjectUrl(projectId)}
-                                highlight={LinkHighlight.Subdued}
-                              >
-                                {projectName}
-                              </LinkStandalone>
-                            </li>
-                          ))}
-                        </ul>
-                      </div>
-                    )}
-                  </FlagMessage>
-                )}
-            </>
-          )}
-        </div>
-      </div>
+      <MonorepoConnectionSelector
+        almKey={almKey}
+        alreadyBoundProjects={alreadyBoundProjects}
+        isFetchingAlreadyBoundProjects={isFetchingAlreadyBoundProjects}
+        isLoadingAlreadyBoundProjects={isLoadingAlreadyBoundProjects}
+        {...props}
+      />
 
       {selectedRepository !== undefined && (
         <>
           <BlueGreySeparator className="sw-my-5" />
 
-          <div>
-            <SubTitle>
-              <FormattedMessage id="onboarding.create_project.monorepo.project_title" />
-            </SubTitle>
-            <div>
-              {projects.map(({ id, key, name }) => (
-                <ProjectValidationCard
-                  className="sw-mt-4"
-                  initialKey={key}
-                  initialName={name}
-                  key={id}
-                  monorepoSetupProjectKeys={projectKeys}
-                  onChange={onProjectChange}
-                  onRemove={() => {
-                    onProjectRemove(id);
-                  }}
-                  projectId={id}
-                />
-              ))}
-            </div>
-
-            <div className="sw-flex sw-justify-end sw-mt-4">
-              <ButtonSecondary onClick={addProject}>
-                <AddNewIcon className="sw-mr-2" />
-                <FormattedMessage id="onboarding.create_project.monorepo.add_project" />
-              </ButtonSecondary>
-            </div>
-          </div>
+          <MonorepoProjectsList
+            projectKeys={projectKeys}
+            onAddProject={onAddProject}
+            onChangeProject={onChangeProject}
+            onRemoveProject={onRemoveProject}
+            projects={projects}
+          />
         </>
       )}
 
diff --git a/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoProjectsList.tsx b/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoProjectsList.tsx
new file mode 100644 (file)
index 0000000..e0b2999
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { ButtonSecondary, SubTitle } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { ProjectData, ProjectValidationCard } from '../components/ProjectValidation';
+
+interface Props {
+  projectKeys: string[];
+  onAddProject: () => void;
+  onChangeProject: (project: ProjectData<number>) => void;
+  onRemoveProject: (id?: number) => void;
+  projects: ProjectData<number>[];
+}
+
+export function MonorepoProjectsList({
+  projectKeys,
+  onAddProject,
+  onChangeProject,
+  onRemoveProject,
+  projects,
+}: Readonly<Props>) {
+  return (
+    <div>
+      <SubTitle>
+        <FormattedMessage id="onboarding.create_project.monorepo.project_title" />
+      </SubTitle>
+      <div>
+        {projects.map(({ id, key, name }) => (
+          <ProjectValidationCard
+            className="sw-mt-4"
+            initialKey={key}
+            initialName={name}
+            key={id}
+            monorepoSetupProjectKeys={projectKeys}
+            onChange={onChangeProject}
+            onRemove={() => {
+              onRemoveProject(id);
+            }}
+            projectId={id}
+          />
+        ))}
+      </div>
+
+      <div className="sw-flex sw-justify-end sw-mt-4">
+        <ButtonSecondary onClick={onAddProject}>
+          <FormattedMessage id="onboarding.create_project.monorepo.add_project" />
+        </ButtonSecondary>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoRepositorySelector.tsx b/server/sonar-web/src/main/js/apps/create/project/monorepo/MonorepoRepositorySelector.tsx
new file mode 100644 (file)
index 0000000..a520d75
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { LinkHighlight, LinkStandalone, Spinner } from '@sonarsource/echoes-react';
+import { DarkLabel, FlagMessage, InputSelect } from 'design-system';
+import React from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { LabelValueSelectOption } from '../../../../helpers/search';
+import { getProjectUrl } from '../../../../helpers/urls';
+import { AlmKeys } from '../../../../types/alm-settings';
+
+interface Props {
+  almKey: AlmKeys;
+  alreadyBoundProjects: {
+    projectId: string;
+    projectName: string;
+  }[];
+  error: boolean;
+  isFetchingAlreadyBoundProjects: boolean;
+  isLoadingAlreadyBoundProjects: boolean;
+  loadingRepositories: boolean;
+  onSearchRepositories: (query: string) => void;
+  onSelectRepository: (repositoryKey: string) => void;
+  repositorySearchQuery: string;
+  repositoryOptions?: LabelValueSelectOption[];
+  selectedOrganization?: LabelValueSelectOption;
+  selectedRepository?: LabelValueSelectOption;
+  showOrganizations?: boolean;
+}
+
+export function MonorepoRepositorySelector({
+  almKey,
+  alreadyBoundProjects,
+  error,
+  isFetchingAlreadyBoundProjects,
+  isLoadingAlreadyBoundProjects,
+  loadingRepositories,
+  onSearchRepositories,
+  onSelectRepository,
+  repositorySearchQuery,
+  repositoryOptions,
+  selectedOrganization,
+  selectedRepository,
+  showOrganizations,
+}: Readonly<Props>) {
+  const { formatMessage } = useIntl();
+
+  const repositorySelectorEnabled =
+    !error &&
+    !loadingRepositories &&
+    ((showOrganizations && !!selectedOrganization) || !showOrganizations);
+  const showWarningMessage =
+    error || (repositorySelectorEnabled && repositoryOptions && repositoryOptions.length === 0);
+
+  return (
+    <>
+      <DarkLabel htmlFor={`${almKey}-monorepo-choose-repository`} className="sw-mb-2">
+        <FormattedMessage id="onboarding.create_project.monorepo.choose_repository" />
+      </DarkLabel>
+      <Spinner isLoading={loadingRepositories && !error}>
+        {showWarningMessage ? (
+          <FormattedMessage
+            id="onboarding.create_project.monorepo.no_projects"
+            defaultMessage={formatMessage({ id: 'onboarding.create_project.monorepo.no_projects' })}
+            values={{
+              almKey: formatMessage({ id: `alm.${almKey}` }),
+            }}
+          />
+        ) : (
+          <>
+            <InputSelect
+              inputId={`${almKey}-monorepo-choose-repository`}
+              inputValue={repositorySearchQuery}
+              isDisabled={!repositorySelectorEnabled}
+              isLoading={loadingRepositories}
+              isSearchable
+              noOptionsMessage={() => formatMessage({ id: 'no_results' })}
+              onChange={({ value }: LabelValueSelectOption) => {
+                onSelectRepository(value);
+              }}
+              onInputChange={onSearchRepositories}
+              options={repositoryOptions}
+              placeholder={formatMessage({
+                id: `onboarding.create_project.monorepo.choose_repository.placeholder`,
+              })}
+              size="full"
+              value={selectedRepository}
+            />
+            {selectedRepository &&
+              !isLoadingAlreadyBoundProjects &&
+              !isFetchingAlreadyBoundProjects && (
+                <FlagMessage className="sw-mt-2" variant="info">
+                  {alreadyBoundProjects.length === 0 ? (
+                    <FormattedMessage id="onboarding.create_project.monorepo.choose_repository.no_already_bound_projects" />
+                  ) : (
+                    <div>
+                      <FormattedMessage id="onboarding.create_project.monorepo.choose_repository.existing_already_bound_projects" />
+                      <ul className="sw-mt-4">
+                        {alreadyBoundProjects.map(({ projectId, projectName }) => (
+                          <li key={projectId}>
+                            <LinkStandalone
+                              to={getProjectUrl(projectId)}
+                              highlight={LinkHighlight.Subdued}
+                            >
+                              {projectName}
+                            </LinkStandalone>
+                          </li>
+                        ))}
+                      </ul>
+                    </div>
+                  )}
+                </FlagMessage>
+              )}
+          </>
+        )}
+      </Spinner>
+    </>
+  );
+}
index 94682636d22ce865c82bf7b3fe1bb878dc7f28ab..b183a9b106097e84f9b63b87804cfbe1ee41b6e2 100644 (file)
@@ -23,8 +23,8 @@ import {
   setAlmPersonalAccessToken,
 } from '../../../api/alm-integrations';
 import { translate } from '../../../helpers/l10n';
-import { AlmSettingsInstance } from '../../../types/alm-settings';
 import { tokenExistedBefore } from './utils';
+import { AlmInstanceBase } from '../../../types/alm-settings';
 
 export interface PATType {
   validationFailed: boolean;
@@ -41,7 +41,7 @@ export interface PATType {
 }
 
 export const usePersonalAccessToken = (
-  almSetting: AlmSettingsInstance,
+  almSetting: AlmInstanceBase,
   resetPat: boolean,
   onPersonalAccessTokenCreated: () => void,
 ): PATType => {
index 035b70bd705b9e5b5be0d4df8859dd0145e38917..3375b2f1bf18e85da992649a9381eee77cb8dbdf 100644 (file)
@@ -21,21 +21,21 @@ import { InputSelect, LabelValueSelectOption, Note } from 'design-system';
 import * as React from 'react';
 import { OptionProps, SingleValueProps, components } from 'react-select';
 import { translate } from '../../helpers/l10n';
-import { AlmSettingsInstance } from '../../types/alm-settings';
+import { AlmInstanceBase } from '../../types/alm-settings';
 
-function optionRenderer(props: OptionProps<LabelValueSelectOption<AlmSettingsInstance>, false>) {
+function optionRenderer(props: OptionProps<LabelValueSelectOption<AlmInstanceBase>, false>) {
   return <components.Option {...props}>{customOptions(props.data.value)}</components.Option>;
 }
 
 function singleValueRenderer(
-  props: SingleValueProps<LabelValueSelectOption<AlmSettingsInstance>, false>,
+  props: SingleValueProps<LabelValueSelectOption<AlmInstanceBase>, false>,
 ) {
   return (
     <components.SingleValue {...props}>{customOptions(props.data.value)}</components.SingleValue>
   );
 }
 
-function customOptions(instance: AlmSettingsInstance) {
+function customOptions(instance: AlmInstanceBase) {
   return instance.url ? (
     <>
       <span>{instance.key} — </span>
@@ -46,14 +46,14 @@ function customOptions(instance: AlmSettingsInstance) {
   );
 }
 
-function orgToOption(alm: AlmSettingsInstance) {
+function orgToOption(alm: AlmInstanceBase) {
   return { value: alm, label: alm.key };
 }
 
 interface Props {
-  instances: AlmSettingsInstance[];
+  instances: AlmInstanceBase[];
   initialValue?: string;
-  onChange: (instance: AlmSettingsInstance) => void;
+  onChange: (instance: AlmInstanceBase) => void;
   className: string;
   inputId: string;
 }
@@ -68,7 +68,7 @@ export default function AlmSettingsInstanceSelector(props: Props) {
       isClearable={false}
       isSearchable={false}
       options={instances.map(orgToOption)}
-      onChange={(data: LabelValueSelectOption<AlmSettingsInstance>) => {
+      onChange={(data: LabelValueSelectOption<AlmInstanceBase>) => {
         props.onChange(data.value);
       }}
       components={{
@@ -76,7 +76,7 @@ export default function AlmSettingsInstanceSelector(props: Props) {
         SingleValue: singleValueRenderer,
       }}
       placeholder={translate('alm.configuration.selector.placeholder')}
-      getOptionValue={(opt: LabelValueSelectOption<AlmSettingsInstance>) => opt.value.key}
+      getOptionValue={(opt: LabelValueSelectOption<AlmInstanceBase>) => opt.value.key}
       value={instances.map(orgToOption).find((opt) => opt.value.key === initialValue) ?? null}
       size="full"
     />
index 8b0ca1beb804078aafdabc6c65db1b0f1e687c42..0a9b89b5763a4841bfc6fdbdfc8cd4bb248f4ce2 100644 (file)
@@ -130,8 +130,11 @@ export interface GitlabProjectAlmBindingParams extends ProjectAlmBindingParams {
   repository?: string;
 }
 
-export interface AlmSettingsInstance {
+export interface AlmSettingsInstance extends AlmInstanceBase {
   alm: AlmKeys;
+}
+
+export interface AlmInstanceBase {
   key: string;
   url?: string;
 }
index 5d6a6baf982069b6eb9aa2cf8634a403361d8b5b..d33090219487b48257bf4d3e29e8d48c6854a2b2 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { AlmKeys } from './alm-settings';
+import { AlmInstanceBase, AlmKeys } from './alm-settings';
 
-export interface DopSetting {
+export interface DopSetting extends AlmInstanceBase {
   appId?: string;
   id: string;
-  key: string;
   type: AlmKeys;
-  url?: string;
 }
 
 export interface BoundProject {
index 0e543d9cdd40991c0b5d61a04d165b98f2c6c15e..ed46414c473c1ca4cee7bc117e5d78721aae3b1b 100644 (file)
@@ -4420,6 +4420,8 @@ onboarding.create_project.github.no_orgs_admin=We couldn't load any organization
 onboarding.create_project.github.no_projects=No projects could be fetched from GitHub. Contact your system administrator.
 onboarding.create_project.gitlab.title=Gitlab project onboarding
 onboarding.create_project.gitlab.subtitle=Import projects from one of your GitLab groups
+onboarding.create_project.gitlab.subtitle.with_monorepo=Import projects from one of your GitLab groups or {monorepoSetupLink}.
+onboarding.create_project.gitlab.subtitle.link=set up a monorepo
 onboarding.create_project.gitlab.no_projects=No projects could be fetched from Gitlab. Contact your system administrator, or {link}.
 onboarding.create_project.gitlab.link=See on GitLab
 onboarding.create_project.monorepo.no_projects=No projects could be fetch from {almKey}. Contact your system administrator.
@@ -4433,17 +4435,23 @@ onboarding.create_project.import_in_progress={count} of {total} projects importe
 onboarding.create_project.monorepo.title={almName} monorepo project onboarding
 onboarding.create_project.monorepo.subtitle=Create multiple SonarQube projects corresponding to the same monorepo and bound to the same repository.
 onboarding.create_project.monorepo.doc_link=Learn more and get help setting up your monorepo
-onboarding.create_project.monorepo.choose_organization_and_repository.github=Choose the organization and the repository
-onboarding.create_project.monorepo.choose_dop_setting.github=Choose the GitHub configuration
-onboarding.create_project.monorepo.choose_organization.github=Choose the organization
-onboarding.create_project.monorepo.choose_organization.github.placeholder=List of organizations
-onboarding.create_project.monorepo.choose_repository.github=Choose the repository
-onboarding.create_project.monorepo.choose_repository.github.placeholder=List of repositories
+onboarding.create_project.monorepo.choose_organization_and_repository=Choose the organization and the repository
+onboarding.create_project.monorepo.choose_dop_setting=Choose the {almKey} configuration
+onboarding.create_project.monorepo.choose_organization=Choose the organization
+onboarding.create_project.monorepo.choose_organization.placeholder=List of organizations
+onboarding.create_project.monorepo.choose_repository=Choose the repository
+onboarding.create_project.monorepo.choose_repository.placeholder=List of repositories
 onboarding.create_project.monorepo.choose_repository.no_already_bound_projects=This repository has no imported projects in SonarQube
 onboarding.create_project.monorepo.choose_repository.existing_already_bound_projects=This repository has already been imported, and it's linked to these projects in SonarQube:
+onboarding.create_project.monorepo.no_orgs=We couldn't load any organizations with your key. Contact an administrator.
+onboarding.create_project.monorepo.no_orgs_admin=We couldn't load any organizations. Make sure the {almKey} App is installed in at least one organization and check the {almKey} instance configuration in the {link}.
+onboarding.create_project.monorepo.no_projects=No projects could be fetch from {almKey}. Contact your system administrator.
 onboarding.create_project.monorepo.project_title=Create new projects
 onboarding.create_project.monorepo.add_project=Add new project
 onboarding.create_project.monorepo.remove_project=Remove project
+onboarding.create_project.monorepo.warning.message=Could not connect to {almKey}. Please contact an administrator to configure {almKey} integration.
+onboarding.create_project.monorepo.warning.message_admin=Could not connect to {almKey}. Please make sure the {almKey} instance is correctly configured in the {link} to create a new project from a repository.
+onboarding.create_project.monorepo.warning.message_admin.link=DevOps Platform integration settings
 
 onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
 onboarding.create_x_project.new_code_definition.title=Set up {count, plural, one {project} other {# projects}} for Clean as You Code