return (
<Container
- className={classNames('sw-cursor-pointer', className, {
+ className={classNames(className, {
open,
})}
role="listitem"
}
${tw`sw-rounded-2`}
${tw`sw-overflow-hidden`}
- ${tw`sw-cursor-pointer`}
& > button:hover, & > button:active {
color: ${themeContrast('buttonSecondary')(props)};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import { Accordion, DeferredSpinner, FlagMessage, Link, SearchHighlighter } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { colors } from '../../../../app/theme';
-import Link from '../../../../components/common/Link';
-import BoxedGroupAccordion from '../../../../components/controls/BoxedGroupAccordion';
import ListFooter from '../../../../components/controls/ListFooter';
-import Radio from '../../../../components/controls/Radio';
-import CheckIcon from '../../../../components/icons/CheckIcon';
-import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import { translate } from '../../../../helpers/l10n';
-import { getProjectUrl, queryToSearch } from '../../../../helpers/urls';
+import { getBaseUrl } from '../../../../helpers/system';
+import { queryToSearch } from '../../../../helpers/urls';
import { AzureProject, AzureRepository } from '../../../../types/alm-integration';
+import AlmRepoItem from '../components/AlmRepoItem';
import { CreateProjectModes } from '../types';
export interface AzureProjectAccordionProps {
loading: boolean;
onOpen: (key: string) => void;
- onSelectRepository: (repository: AzureRepository) => void;
+ onImportRepository: (repository: AzureRepository) => void;
project: AzureProject;
repositories?: AzureRepository[];
searchQuery?: string;
- selectedRepository?: AzureRepository;
startsOpen: boolean;
}
-const PAGE_SIZE = 30;
-
-function highlight(text: string, term?: string, underline = false) {
- if (!term || !text.toLowerCase().includes(term.toLowerCase())) {
- return text;
- }
-
- // Capture only the first occurence by using a capturing group to get
- // everything after the first occurence
- const [pre, found, post] = text.split(new RegExp(`(${term})(.*)`, 'i'));
- return (
- <>
- {pre}
- <strong className={classNames({ underline })}>{found}</strong>
- {post}
- </>
- );
-}
+const PAGE_SIZE = 20;
export default function AzureProjectAccordion(props: AzureProjectAccordionProps) {
- const {
- loading,
- startsOpen,
- project,
- repositories = [],
- searchQuery,
- selectedRepository,
- } = props;
+ const { loading, startsOpen, project, repositories = [], searchQuery } = props;
const [open, setOpen] = React.useState(startsOpen);
const handleClick = () => {
const [page, setPage] = React.useState(1);
const limitedRepositories = repositories.slice(0, page * PAGE_SIZE);
- const isSelected = (repo: AzureRepository) =>
- selectedRepository?.projectName === project.name && selectedRepository.name === repo.name;
-
return (
- <BoxedGroupAccordion
- className={classNames('big-spacer-bottom', {
- open,
- })}
+ <Accordion
onClick={handleClick}
open={open}
- title={<h3 title={project.description}>{highlight(project.name, searchQuery, true)}</h3>}
+ header={
+ <span title={project.description}>
+ <SearchHighlighter term={searchQuery}>{project.name}</SearchHighlighter>
+ </span>
+ }
>
+ {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner*/}
{open && (
<DeferredSpinner loading={loading}>
{/* The extra loading guard is to prevent the flash of the Alert */}
{!loading && repositories.length === 0 ? (
- <Alert variant="warning">
+ <FlagMessage variant="warning">
<FormattedMessage
defaultMessage={translate('onboarding.create_project.azure.no_repositories')}
id="onboarding.create_project.azure.no_repositories"
),
}}
/>
- </Alert>
+ </FlagMessage>
) : (
<>
- <div className="display-flex-wrap">
- {limitedRepositories.map((repo) => (
- <div
- className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
- key={repo.name}
- >
- {repo.sqProjectKey ? (
- <>
- <CheckIcon className="spacer-right" fill={colors.green} size={14} />
- <div className="overflow-hidden">
- <div className="little-spacer-bottom text-ellipsis">
- <Link to={getProjectUrl(repo.sqProjectKey)} title={repo.sqProjectName}>
- {highlight(repo.sqProjectName || repo.name, searchQuery)}
- </Link>
- </div>
- <em>{translate('onboarding.create_project.repository_imported')}</em>
- </div>
- </>
- ) : (
- <Radio
- checked={isSelected(repo)}
- className="overflow-hidden"
- alignLabel
- onCheck={() => props.onSelectRepository(repo)}
- value={repo.name}
- >
- <span title={repo.name}>{highlight(repo.name, searchQuery)}</span>
- </Radio>
- )}
- </div>
+ <div className="sw-flex sw-flex-col sw-gap-3">
+ {limitedRepositories.map((r) => (
+ <AlmRepoItem
+ key={r.name}
+ almKey={r.name}
+ almIconSrc={`${getBaseUrl()}/images/alm/azure.svg`}
+ sqProjectKey={r.sqProjectKey}
+ onImport={() => props.onImportRepository(r)}
+ primaryTextNode={
+ <span title={r.name}>
+ <SearchHighlighter term={searchQuery}>{r.name}</SearchHighlighter>
+ </span>
+ }
+ />
))}
</div>
<ListFooter
count={limitedRepositories.length}
total={repositories.length}
loadMore={() => setPage((p) => p + 1)}
+ useMIUIButtons
/>
</>
)}
</DeferredSpinner>
)}
- </BoxedGroupAccordion>
+ </Accordion>
);
}
searching?: boolean;
searchResults?: AzureRepository[];
searchQuery?: string;
- selectedRepository?: AzureRepository;
selectedAlmInstance?: AlmSettingsInstance;
submittingToken?: boolean;
tokenValidationFailed: boolean;
}
};
- handleImportRepository = () => {
- const { selectedRepository, selectedAlmInstance } = this.state;
+ handleImportRepository = (selectedRepository: AzureRepository) => {
+ const { selectedAlmInstance } = this.state;
if (selectedAlmInstance && selectedRepository) {
this.props.onProjectSetupDone(
}
};
- handleSelectRepository = (selectedRepository: AzureRepository) => {
- this.setState({ selectedRepository });
- };
-
checkPersonalAccessToken = () => {
const { selectedAlmInstance } = this.state;
searching,
searchResults,
searchQuery,
- selectedRepository,
selectedAlmInstance,
submittingToken,
tokenValidationFailed,
onOpenProject={this.handleOpenProject}
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
onSearch={this.handleSearchRepositories}
- onSelectRepository={this.handleSelectRepository}
projects={projects}
repositories={repositories}
searching={searching}
searchResults={searchResults}
searchQuery={searchQuery}
- selectedRepository={selectedRepository}
almInstances={almInstances}
selectedAlmInstance={selectedAlmInstance}
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ DeferredSpinner,
+ FlagMessage,
+ InputSearch,
+ LightPrimary,
+ Link,
+ PageContentFontWrapper,
+ Title,
+} from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import Link from '../../../../components/common/Link';
-import SearchBox from '../../../../components/controls/SearchBox';
-import { Button } from '../../../../components/controls/buttons';
-import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
import { getGlobalSettingsUrl } from '../../../../helpers/urls';
import { AzureProject, AzureRepository } from '../../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
import { Dict } from '../../../../types/types';
import { ALM_INTEGRATION_CATEGORY } from '../../../settings/constants';
import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
-import CreateProjectPageHeader from '../components/CreateProjectPageHeader';
import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm';
import AzureProjectsList from './AzureProjectsList';
canAdmin?: boolean;
loading: boolean;
loadingRepositories: Dict<boolean>;
- onImportRepository: () => void;
+ onImportRepository: (resository: AzureRepository) => void;
onOpenProject: (key: string) => void;
onPersonalAccessTokenCreate: (token: string) => void;
onSearch: (query: string) => void;
- onSelectRepository: (repository: AzureRepository) => void;
projects?: AzureProject[];
repositories: Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: AzureRepository[];
searchQuery?: string;
- selectedRepository?: AzureRepository;
almInstances?: AlmSettingsInstance[];
selectedAlmInstance?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
searching,
searchResults,
searchQuery,
- selectedRepository,
almInstances,
showPersonalAccessTokenForm,
submittingToken,
} = props;
const showCountError = !loading && (!almInstances || almInstances?.length === 0);
- const settingIsValid = selectedAlmInstance && selectedAlmInstance.url;
const showUrlError = !loading && selectedAlmInstance && !selectedAlmInstance.url;
return (
- <>
- <CreateProjectPageHeader
- additionalActions={
- !showPersonalAccessTokenForm &&
- settingIsValid && (
- <div className="display-flex-center pull-right">
- <Button
- className="button-large button-primary"
- disabled={!selectedRepository}
- onClick={props.onImportRepository}
- >
- {translate('onboarding.create_project.import_selected_repo')}
- </Button>
- </div>
- )
- }
- title={
- <span className="text-middle">
- <img
- alt="" // Should be ignored by screen readers
- className="spacer-right"
- height="24"
- src={`${getBaseUrl()}/images/alm/azure.svg`}
- />
- {translate('onboarding.create_project.azure.title')}
- </span>
- }
- />
+ <PageContentFontWrapper>
+ <header className="sw-mb-10">
+ <Title className="sw-mb-4">{translate('onboarding.create_project.azure.title')}</Title>
+ <LightPrimary className="sw-body-sm">
+ {translate('onboarding.create_project.azure.subtitle')}
+ </LightPrimary>
+ </header>
<AlmSettingsInstanceDropdown
almKey={AlmKeys.Azure}
onChangeConfig={props.onSelectedAlmInstanceChange}
/>
- {loading && <i className="spinner" />}
+ <DeferredSpinner loading={loading} />
{showUrlError && (
- <Alert variant="error">
+ <FlagMessage variant="error">
{canAdmin ? (
<FormattedMessage
defaultMessage={translate('onboarding.create_project.azure.no_url.admin')}
) : (
translate('onboarding.create_project.azure.no_url')
)}
- </Alert>
+ </FlagMessage>
)}
{showCountError && <WrongBindingCountAlert alm={AlmKeys.Azure} canAdmin={!!canAdmin} />}
</div>
) : (
<>
- <div className="huge-spacer-bottom">
- <SearchBox
+ <div className="sw-mb-10 sw-w-abs-400">
+ <InputSearch
+ clearIconAriaLabel={translate('clear')}
onChange={props.onSearch}
placeholder={translate('onboarding.create_project.search_projects_repositories')}
+ size="full"
/>
</div>
<DeferredSpinner loading={Boolean(searching)}>
<AzureProjectsList
loadingRepositories={loadingRepositories}
onOpenProject={props.onOpenProject}
- onSelectRepository={props.onSelectRepository}
+ onImportRepository={props.onImportRepository}
projects={projects}
repositories={repositories}
searchResults={searchResults}
searchQuery={searchQuery}
- selectedRepository={selectedRepository}
/>
</DeferredSpinner>
</>
))}
- </>
+ </PageContentFontWrapper>
);
}
* 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, Link } from 'design-system';
import { uniqBy } from 'lodash';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import Link from '../../../../components/common/Link';
import ListFooter from '../../../../components/controls/ListFooter';
-import { Alert } from '../../../../components/ui/Alert';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { queryToSearch } from '../../../../helpers/urls';
import { AzureProject, AzureRepository } from '../../../../types/alm-integration';
export interface AzureProjectsListProps {
loadingRepositories: Dict<boolean>;
onOpenProject: (key: string) => void;
- onSelectRepository: (repository: AzureRepository) => void;
+ onImportRepository: (repository: AzureRepository) => void;
projects?: AzureProject[];
repositories: Dict<AzureRepository[]>;
searchResults?: AzureRepository[];
searchQuery?: string;
- selectedRepository?: AzureRepository;
}
const PAGE_SIZE = 10;
export default function AzureProjectsList(props: AzureProjectsListProps) {
- const {
- loadingRepositories,
- projects = [],
- repositories,
- searchResults,
- searchQuery,
- selectedRepository,
- } = props;
+ const { loadingRepositories, projects = [], repositories, searchResults, searchQuery } = props;
const [page, setPage] = React.useState(1);
if (searchResults && searchResults.length === 0) {
return (
- <Alert className="spacer-top" variant="warning">
+ <FlagMessage className="sw-mt-2" variant="warning">
{translate('onboarding.create_project.azure.no_results')}
- </Alert>
+ </FlagMessage>
);
}
if (projects.length === 0) {
return (
- <Alert className="spacer-top" variant="warning">
+ <FlagMessage className="sw-mt-2" variant="warning">
<FormattedMessage
defaultMessage={translate('onboarding.create_project.azure.no_projects')}
id="onboarding.create_project.azure.no_projects"
),
}}
/>
- </Alert>
+ </FlagMessage>
);
}
return (
<div>
- {displayedProjects.map((p, i) => (
- <AzureProjectAccordion
- key={`${p.name}${keySuffix}`}
- loading={Boolean(loadingRepositories[p.name])}
- onOpen={props.onOpenProject}
- onSelectRepository={props.onSelectRepository}
- project={p}
- repositories={
- searchResults
- ? searchResults.filter((s) => s.projectName === p.name)
- : repositories[p.name]
- }
- selectedRepository={selectedRepository}
- searchQuery={searchQuery}
- startsOpen={searchResults !== undefined || i === 0}
- />
- ))}
+ <div className="sw-flex sw-flex-col sw-gap-6">
+ {displayedProjects.map((p, i) => (
+ <AzureProjectAccordion
+ key={`${p.name}${keySuffix}`}
+ loading={Boolean(loadingRepositories[p.name])}
+ onOpen={props.onOpenProject}
+ onImportRepository={props.onImportRepository}
+ project={p}
+ repositories={
+ searchResults
+ ? searchResults.filter((s) => s.projectName === p.name)
+ : repositories[p.name]
+ }
+ searchQuery={searchQuery}
+ startsOpen={searchResults !== undefined || i === 0}
+ />
+ ))}
+ </div>
<ListFooter
+ className="sw-mb-12"
count={displayedProjects.length}
loadMore={() => setPage((p) => p + 1)}
total={filteredProjects.length}
+ useMIUIButtons
/>
</div>
);
import GitlabProjectCreate from './Gitlab/GitlabProjectCreate';
import NewCodeDefinitionSelection from './components/NewCodeDefinitionSelection';
import ManualProjectCreate from './manual/ManualProjectCreate';
-import './style.css';
import { CreateProjectApiCallback, CreateProjectModes } from './types';
export interface CreateProjectPageProps extends WithAvailableFeaturesProps {
<LightPrimary className="sw-body-sm">{translate('no_results')}</LightPrimary>
</div>
) : (
- repositories.map((r) => (
- <AlmRepoItem
- key={r.key}
- almKey={r.key}
- almUrl={r.url}
- almUrlText={translate('onboarding.create_project.see_on_github')}
- almIconSrc={`${getBaseUrl()}/images/tutorials/github-actions.svg`}
- sqProjectKey={r.sqProjectKey}
- onImport={props.onImportRepository}
- primaryTextNode={<span title={r.name}>{r.name}</span>}
- />
- ))
+ <div className="sw-flex sw-flex-col sw-gap-3">
+ {repositories.map((r) => (
+ <AlmRepoItem
+ key={r.key}
+ almKey={r.key}
+ almUrl={r.url}
+ almUrlText={translate('onboarding.create_project.see_on_github')}
+ almIconSrc={`${getBaseUrl()}/images/tutorials/github-actions.svg`}
+ sqProjectKey={r.sqProjectKey}
+ onImport={props.onImportRepository}
+ primaryTextNode={<span title={r.name}>{r.name}</span>}
+ />
+ ))}
+ </div>
)}
<div className="display-flex-justify-center width-100">
await selectEvent.select(ui.instanceSelector.get(), [/conf-azure-2/]);
});
+ expect(screen.getByText('Azure project')).toBeInTheDocument();
expect(screen.getByText('Azure project 2')).toBeInTheDocument();
- const importButton = screen.getByText('onboarding.create_project.import_selected_repo');
- const radioButton = screen.getByRole('radio', { name: 'Azure repo 2' });
- expect(radioButton).toBeInTheDocument();
- expect(importButton).toBeDisabled();
- await user.click(radioButton);
- expect(importButton).toBeEnabled();
+ expect(
+ screen.getByRole('row', {
+ name: 'Azure repo 1 onboarding.create_project.repository_imported',
+ })
+ ).toBeInTheDocument();
+
+ expect(
+ screen.getByRole('row', {
+ name: 'Azure repo 2 onboarding.create_project.import',
+ })
+ ).toBeInTheDocument();
+
+ const importButton = screen.getByText('onboarding.create_project.import');
await user.click(importButton);
expect(
secondaryTextNode?: React.ReactNode;
sqProjectKey?: string;
almKey: string;
- almUrl: string;
- almUrlText: string;
+ almUrl?: string;
+ almUrlText?: string;
onImport: (key: string) => void;
almIconSrc: string;
}
<StyledCard
key={almKey}
role="row"
- className={classNames('sw-flex sw-mb-2 sw-px-4', {
+ className={classNames('sw-flex sw-px-4', {
'sw-py-4': sqProjectKey !== undefined,
'sw-py-2': sqProjectKey === undefined,
})}
>
- <div className="sw-w-[70%] sw-flex sw-mr-1">
+ <div className="sw-w-[70%] sw-min-w-0 sw-flex sw-mr-1">
<div className="sw-max-w-[50%] sw-flex sw-items-center">
<img
alt="" // Should be ignored by screen readers
</LightPrimary>
)}
</div>
- <div className="sw-ml-2 sw-flex sw-items-center sw-truncate">
+ <div className="sw-max-w-[50%] sw-min-w-0 sw-ml-2 sw-flex sw-items-center sw-truncate">
<LightLabel className="sw-body-sm">{secondaryTextNode}</LightLabel>
</div>
</div>
- <div className="sw-flex sw-justify-between sw-items-center sw-flex-1">
- {almUrl !== undefined && (
- <div className="sw-flex sw-items-center">
- <Link
- className="sw-body-sm-highlight"
- onClick={(e) => e.stopPropagation()}
- target="_blank"
- to={almUrl}
- rel="noopener noreferrer"
- >
- {almUrlText}
- </Link>
- </div>
- )}
+ {almUrl !== undefined && (
+ <div className="sw-flex sw-items-center sw-flex-shrink-0 sw-ml-2">
+ <Link
+ className="sw-body-sm-highlight"
+ onClick={(e) => e.stopPropagation()}
+ target="_blank"
+ to={almUrl}
+ rel="noopener noreferrer"
+ >
+ {almUrlText ?? almUrl}
+ </Link>
+ </div>
+ )}
+ <div className="sw-ml-2 sw-flex sw-justify-end sw-flex-grow sw-flex-shrink-0 sw-w-abs-150">
{sqProjectKey ? (
<div className="sw-flex sw-items-center">
<CheckIcon />
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-#create-project {
- padding-top: 0 !important;
-}
-
-#create-project header {
- padding-top: 20px;
- background-color: var(--barBackgroundColor);
- position: sticky;
- top: var(--globalNavHeight);
- z-index: var(--pageMainZIndex);
-}
-
-.create-project-mode-type-alm.disabled img {
- filter: grayscale(100%);
-}
-
-.create-project-azdo-repo {
- width: 410px;
- min-height: 40px;
- box-sizing: border-box;
- margin-right: auto;
-}
-
-.create-project-import-bbs .open .boxed-group-header {
- border-bottom: 1px solid var(--barBorderColor);
-}
-
-.create-project-import-bbs .boxed-group-inner {
- padding-top: calc(3 * var(--gridSize));
-}
-
-.create-project-import-bbs-repo {
- width: 250px;
- min-height: 40px;
-}
-
-.create-project-github-repository {
- box-sizing: border-box;
- width: 33.33%;
-}
-
-.create-project-github-repository .notice {
- display: block;
- position: absolute;
-}
-
-.create-project-github-repository .notice svg {
- color: var(--green);
-}
-
-.create-project-import table > tbody > tr > td {
- vertical-align: middle;
-}
-
-.create-project-import .project-name,
-.create-project-import .project-path {
- max-width: 400px;
-}
-
-.create-project-import .sq-project-link {
- max-width: 300px;
-}
-
-.create-project-import .already-set-up svg {
- color: var(--green);
-}
onboarding.create_project.search_prompt=Search for projects
onboarding.create_project.set_up=Set up
onboarding.create_project.azure.title=Azure project onboarding
+onboarding.create_project.azure.subtitle=Import projects from one of your Azure projects
onboarding.create_project.azure.no_projects=No projects could be fetched from Azure DevOps. Contact your system administrator, or {link}.
onboarding.create_project.azure.search_results_for_project_X=Search results for "{0}"
onboarding.create_project.azure.no_repositories=Could not fetch repositories for this project. Contact your system administrator, or {link}.