bitbucketSettings: AlmSettingsInstance[];
bitbucketCloudSettings: AlmSettingsInstance[];
githubSettings: DopSetting[];
- gitlabSettings: AlmSettingsInstance[];
+ gitlabSettings: DopSetting[];
loading: boolean;
creatingAlmDefinition?: AlmKeys;
importProjects?: ImportProjectParam;
.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,
});
})
return (
<GitlabProjectCreate
canAdmin={!!canAdmin}
- loadingBindings={loading}
- location={location}
- router={router}
- almInstances={gitlabSettings}
+ dopSettings={gitlabSettings}
+ isLoadingBindings={loading}
onProjectSetupDone={this.handleProjectSetupDone}
/>
);
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';
import { CreateProjectModes } from '../types';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
import { redirectToGithub } from './utils';
+import { LabelValueSelectOption } from 'design-system';
interface Props {
canAdmin: boolean;
selectedDopSetting={selectedDopSetting}
selectedOrganization={selectedOrganization && transformToOption(selectedOrganization)}
selectedRepository={selectedRepository && transformToOption(selectedRepository)}
+ showOrganizations
/>
) : (
<GitHubProjectCreateRenderer
function transformToOption({
key,
name,
-}: GithubOrganization | GithubRepository): LabelValueSelectOption {
+}: GithubOrganization | GithubRepository): LabelValueSelectOption<string> {
return { value: key, label: name };
}
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;
}
* 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 };
}
* 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;
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,
<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>
onChangeConfig={props.onSelectedAlmInstanceChange}
/>
- <Spinner loading={loading} />
+ <Spinner isLoading={loading} />
{!loading && !selectedAlmInstance && (
<WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} />
* 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';
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;
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) {
* 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';
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');
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;
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();
* 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();
).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],
});
}
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', {
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',
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';
}
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;
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
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ </>
+ )
+ );
+}
* 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;
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);
const location = useLocation();
const { push } = useRouter();
- const { formatMessage } = useIntl();
const projectKeys = React.useMemo(() => projects.map(({ key }) => key), [projects]);
const {
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 = [
];
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 },
});
};
useEffect(() => {
if (selectedRepository !== undefined && projects.length === 0) {
- addProject();
+ onAddProject();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedRepository]);
<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}
+ />
</>
)}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ </>
+ );
+}
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;
}
export const usePersonalAccessToken = (
- almSetting: AlmSettingsInstance,
+ almSetting: AlmInstanceBase,
resetPat: boolean,
onPersonalAccessTokenCreated: () => void,
): PATType => {
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>
);
}
-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;
}
isClearable={false}
isSearchable={false}
options={instances.map(orgToOption)}
- onChange={(data: LabelValueSelectOption<AlmSettingsInstance>) => {
+ onChange={(data: LabelValueSelectOption<AlmInstanceBase>) => {
props.onChange(data.value);
}}
components={{
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"
/>
repository?: string;
}
-export interface AlmSettingsInstance {
+export interface AlmSettingsInstance extends AlmInstanceBase {
alm: AlmKeys;
+}
+
+export interface AlmInstanceBase {
key: string;
url?: string;
}
* 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 {
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.
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