);
}
+export function importAzureRepository(
+ almSetting: string,
+ projectName: string,
+ repositoryName: string
+): Promise<{ project: ProjectBase }> {
+ return postJSON('/api/alm_integrations/import_azure_project', {
+ almSetting,
+ projectName,
+ repositoryName
+ }).catch(throwGlobalError);
+}
+
export function getBitbucketServerProjects(
almSetting: string
): Promise<{ projects: BitbucketProject[] }> {
import { Link } from 'react-router';
import BoxedGroupAccordion from 'sonar-ui-common/components/controls/BoxedGroupAccordion';
import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
+import Radio from 'sonar-ui-common/components/controls/Radio';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { CreateProjectModes } from './types';
export interface AzureProjectAccordionProps {
+ importing: boolean;
loading: boolean;
onOpen: (key: string) => void;
- startsOpen: boolean;
+ onSelectRepository: (repository: AzureRepository) => void;
project: AzureProject;
repositories?: AzureRepository[];
+ selectedRepository?: AzureRepository;
+ startsOpen: boolean;
}
const PAGE_SIZE = 30;
export default function AzureProjectAccordion(props: AzureProjectAccordionProps) {
- const { loading, startsOpen, project, repositories = [] } = props;
+ const { importing, loading, startsOpen, project, repositories = [], selectedRepository } = props;
const [open, setOpen] = React.useState(startsOpen);
const handleClick = () => {
<>
<div className="display-flex-wrap">
{limitedRepositories.map(repo => (
- <div
- className="abs-width-400 overflow-hidden spacer-top spacer-bottom"
- key={repo.name}>
+ <Radio
+ checked={selectedRepository?.name === repo.name}
+ className={classNames(
+ 'display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden',
+ importing && ['disabled', 'text-muted', 'link-no-underline']
+ )}
+ key={repo.name}
+ onCheck={() => !importing && props.onSelectRepository(repo)}
+ value={repo.name}>
<strong className="text-ellipsis" title={repo.name}>
{repo.name}
</strong>
- </div>
+ </Radio>
))}
</div>
<ListFooter
checkPersonalAccessTokenIsValid,
getAzureProjects,
getAzureRepositories,
+ importAzureRepository,
searchAzureRepositories,
setAlmPersonalAccessToken
} from '../../../api/alm-integrations';
}
interface State {
+ importing: boolean;
loading: boolean;
loadingRepositories: T.Dict<boolean>;
patIsValid?: boolean;
repositories: T.Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: T.Dict<AzureRepository[]>;
+ selectedRepository?: AzureRepository;
settings?: AlmSettingsInstance;
submittingToken?: boolean;
tokenValidationFailed: boolean;
// For now, we only handle a single instance. So we always use the first
// one from the list.
settings: props.settings[0],
+ importing: false,
loading: false,
loadingRepositories: {},
repositories: {},
}
};
+ handleImportRepository = async () => {
+ const { selectedRepository, settings } = this.state;
+
+ if (!settings || !selectedRepository) {
+ return;
+ }
+
+ this.setState({ importing: true });
+
+ const createdProject = await importAzureRepository(
+ settings.key,
+ selectedRepository.projectName,
+ selectedRepository.name
+ )
+ .then(({ project }) => project)
+ .catch(() => undefined);
+
+ if (this.mounted) {
+ this.setState({ importing: false });
+ if (createdProject) {
+ this.props.onProjectCreate([createdProject.key]);
+ }
+ }
+ };
+
+ handleSelectRepository = (selectedRepository: AzureRepository) => {
+ this.setState({ selectedRepository });
+ };
+
checkPersonalAccessToken = () => {
const { settings } = this.state;
render() {
const { canAdmin, loadingBindings, location } = this.props;
const {
+ importing,
loading,
loadingRepositories,
patIsValid,
repositories,
searching,
searchResults,
+ selectedRepository,
settings,
submittingToken,
tokenValidationFailed
return (
<AzureCreateProjectRenderer
canAdmin={canAdmin}
+ importing={importing}
loading={loading || loadingBindings}
loadingRepositories={loadingRepositories}
+ onImportRepository={this.handleImportRepository}
onOpenProject={this.handleOpenProject}
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
onSearch={this.handleSearchRepositories}
+ onSelectRepository={this.handleSelectRepository}
projects={projects}
repositories={repositories}
searching={searching}
searchResults={searchResults}
+ selectedRepository={selectedRepository}
settings={settings}
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
submittingToken={submittingToken}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
export interface AzureProjectCreateRendererProps {
canAdmin?: boolean;
+ importing: boolean;
loading: boolean;
loadingRepositories: T.Dict<boolean>;
+ onImportRepository: () => void;
onOpenProject: (key: string) => void;
onPersonalAccessTokenCreate: (token: string) => void;
onSearch: (query: string) => void;
+ onSelectRepository: (repository: AzureRepository) => void;
projects?: AzureProject[];
repositories: T.Dict<AzureRepository[]>;
searching?: boolean;
searchResults?: T.Dict<AzureRepository[]>;
+ selectedRepository?: AzureRepository;
settings?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
submittingToken?: boolean;
export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) {
const {
canAdmin,
+ importing,
loading,
loadingRepositories,
projects,
repositories,
searching,
searchResults,
- showPersonalAccessTokenForm,
+ selectedRepository,
settings,
+ showPersonalAccessTokenForm,
submittingToken,
tokenValidationFailed
} = props;
return (
<>
<CreateProjectPageHeader
+ additionalActions={
+ !showPersonalAccessTokenForm && (
+ <div className="display-flex-center pull-right">
+ <DeferredSpinner className="spacer-right" loading={importing} />
+ <Button
+ className="button-large button-primary"
+ disabled={!selectedRepository || importing}
+ onClick={props.onImportRepository}>
+ {translate('onboarding.create_project.import_selected_repo')}
+ </Button>
+ </div>
+ )
+ }
title={
<span className="text-middle">
<img
</div>
<DeferredSpinner loading={Boolean(searching)}>
<AzureProjectsList
+ importing={importing}
loadingRepositories={loadingRepositories}
onOpenProject={props.onOpenProject}
+ onSelectRepository={props.onSelectRepository}
projects={projects}
repositories={repositories}
searchResults={searchResults}
+ selectedRepository={selectedRepository}
/>
</DeferredSpinner>
</>
import { CreateProjectModes } from './types';
export interface AzureProjectsListProps {
+ importing: boolean;
loadingRepositories: T.Dict<boolean>;
onOpenProject: (key: string) => void;
+ onSelectRepository: (repository: AzureRepository) => void;
projects?: AzureProject[];
repositories: T.Dict<AzureRepository[]>;
searchResults?: T.Dict<AzureRepository[]>;
+ selectedRepository?: AzureRepository;
}
const PAGE_SIZE = 10;
export default function AzureProjectsList(props: AzureProjectsListProps) {
- const { loadingRepositories, projects = [], repositories, searchResults } = props;
+ const {
+ importing,
+ loadingRepositories,
+ projects = [],
+ repositories,
+ searchResults,
+ selectedRepository
+ } = props;
const [page, setPage] = React.useState(1);
{displayedProjects.map((p, i) => (
<AzureProjectAccordion
key={`${p.key}${keySuffix}`}
+ importing={importing}
loading={Boolean(loadingRepositories[p.key])}
onOpen={props.onOpenProject}
+ onSelectRepository={props.onSelectRepository}
project={p}
repositories={searchResults ? searchResults[p.key] : repositories[p.key]}
+ selectedRepository={selectedRepository}
startsOpen={searchResults !== undefined || i === 0}
/>
))}
expect(shallowRender({ repositories: [mockAzureRepository()] })).toMatchSnapshot(
'with a repository'
);
+ expect(shallowRender({ importing: true, repositories: [mockAzureRepository()] })).toMatchSnapshot(
+ 'importing'
+ );
});
it('should open when clicked', () => {
function shallowRender(overrides: Partial<AzureProjectAccordionProps> = {}) {
return shallow(
<AzureProjectAccordion
+ importing={false}
loading={false}
+ onSelectRepository={jest.fn()}
onOpen={jest.fn()}
project={mockAzureProject()}
startsOpen={true}
checkPersonalAccessTokenIsValid,
getAzureProjects,
getAzureRepositories,
+ importAzureRepository,
searchAzureRepositories,
setAlmPersonalAccessToken
} from '../../../../api/alm-integrations';
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null),
getAzureProjects: jest.fn().mockResolvedValue({ projects: [] }),
getAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }),
- searchAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] })
+ searchAzureRepositories: jest.fn().mockResolvedValue({ repositories: [] }),
+ importAzureRepository: jest.fn().mockResolvedValue({ project: { key: 'baz' } })
};
});
expect(wrapper.state().searchResults).toBeUndefined();
});
+it('should select and import a repository', async () => {
+ const onProjectCreate = jest.fn();
+ const repository = mockAzureRepository();
+ const wrapper = shallowRender({ onProjectCreate });
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().selectedRepository).toBeUndefined();
+ wrapper.instance().handleSelectRepository(repository);
+ expect(wrapper.state().selectedRepository).toBe(repository);
+
+ wrapper.instance().handleImportRepository();
+ expect(wrapper.state().importing).toBe(true);
+ expect(importAzureRepository).toBeCalledWith('foo', repository.projectName, repository.name);
+ await waitAndUpdate(wrapper);
+
+ expect(onProjectCreate).toBeCalledWith(['baz']);
+ expect(wrapper.state().importing).toBe(false);
+});
+
+it('should handle no settings', () => {
+ const wrapper = shallowRender({ settings: [] });
+
+ wrapper.instance().fetchAzureProjects();
+ wrapper.instance().fetchAzureRepositories('whatever');
+ wrapper.instance().handleSearchRepositories('query');
+ wrapper.instance().handleImportRepository();
+ wrapper.instance().checkPersonalAccessToken();
+ wrapper.instance().handlePersonalAccessTokenCreate('');
+
+ expect(getAzureProjects).not.toBeCalled();
+ expect(getAzureRepositories).not.toBeCalled();
+ expect(searchAzureRepositories).not.toBeCalled();
+ expect(importAzureRepository).not.toBeCalled();
+ expect(checkPersonalAccessTokenIsValid).not.toBeCalled();
+ expect(setAlmPersonalAccessToken).not.toBeCalled();
+});
+
function shallowRender(overrides: Partial<AzureProjectCreate['props']> = {}) {
return shallow<AzureProjectCreate>(
<AzureProjectCreate
return shallow(
<AzureProjectCreateRenderer
canAdmin={true}
+ importing={false}
loading={false}
loadingRepositories={{}}
+ onImportRepository={jest.fn()}
onOpenProject={jest.fn()}
onPersonalAccessTokenCreate={jest.fn()}
onSearch={jest.fn()}
+ onSelectRepository={jest.fn()}
projects={[project]}
repositories={{ [project.key]: [mockAzureRepository()] }}
tokenValidationFailed={false}
return shallow(
<AzureProjectsList
+ importing={false}
loadingRepositories={{}}
onOpenProject={jest.fn()}
+ onSelectRepository={jest.fn()}
projects={[project]}
repositories={{ [project.key]: [] }}
{...overrides}
/>
`;
+exports[`should render correctly: importing 1`] = `
+<BoxedGroupAccordion
+ className="big-spacer-bottom open"
+ onClick={[Function]}
+ open={true}
+ title={
+ <h3>
+ Azure Project
+ </h3>
+ }
+>
+ <DeferredSpinner
+ loading={false}
+ >
+ <div
+ className="display-flex-wrap"
+ >
+ <Radio
+ checked={false}
+ className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden disabled text-muted link-no-underline"
+ key="Azure repo 1"
+ onCheck={[Function]}
+ value="Azure repo 1"
+ >
+ <strong
+ className="text-ellipsis"
+ title="Azure repo 1"
+ >
+ Azure repo 1
+ </strong>
+ </Radio>
+ </div>
+ <ListFooter
+ count={1}
+ loadMore={[Function]}
+ total={1}
+ />
+ </DeferredSpinner>
+</BoxedGroupAccordion>
+`;
+
exports[`should render correctly: loading 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
<div
className="display-flex-wrap"
>
- <div
- className="abs-width-400 overflow-hidden spacer-top spacer-bottom"
+ <Radio
+ checked={false}
+ className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden"
key="Azure repo 1"
+ onCheck={[Function]}
+ value="Azure repo 1"
>
<strong
className="text-ellipsis"
>
Azure repo 1
</strong>
- </div>
+ </Radio>
</div>
<ListFooter
count={1}
exports[`should render correctly 1`] = `
<AzureProjectCreateRenderer
canAdmin={true}
+ importing={false}
loading={true}
loadingRepositories={Object {}}
+ onImportRepository={[Function]}
onOpenProject={[Function]}
onPersonalAccessTokenCreate={[Function]}
onSearch={[Function]}
+ onSelectRepository={[Function]}
repositories={Object {}}
settings={
Object {
exports[`should render correctly: loading 1`] = `
<Fragment>
<CreateProjectPageHeader
+ additionalActions={
+ <div
+ className="display-flex-center pull-right"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ />
+ <Button
+ className="button-large button-primary"
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ onboarding.create_project.import_selected_repo
+ </Button>
+ </div>
+ }
title={
<span
className="text-middle"
exports[`should render correctly: no settings 1`] = `
<Fragment>
<CreateProjectPageHeader
+ additionalActions={
+ <div
+ className="display-flex-center pull-right"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ />
+ <Button
+ className="button-large button-primary"
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ onboarding.create_project.import_selected_repo
+ </Button>
+ </div>
+ }
title={
<span
className="text-middle"
exports[`should render correctly: project list 1`] = `
<Fragment>
<CreateProjectPageHeader
+ additionalActions={
+ <div
+ className="display-flex-center pull-right"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ />
+ <Button
+ className="button-large button-primary"
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ onboarding.create_project.import_selected_repo
+ </Button>
+ </div>
+ }
title={
<span
className="text-middle"
loading={false}
>
<AzureProjectsList
+ importing={false}
loadingRepositories={Object {}}
onOpenProject={[MockFunction]}
+ onSelectRepository={[MockFunction]}
projects={
Array [
Object {
exports[`should render correctly: token form 1`] = `
<Fragment>
<CreateProjectPageHeader
+ additionalActions={false}
title={
<span
className="text-middle"
exports[`should render correctly: default 1`] = `
<div>
<AzureProjectAccordion
+ importing={false}
key="azure-project-1"
loading={false}
onOpen={[MockFunction]}
+ onSelectRepository={[MockFunction]}
project={
Object {
"key": "azure-project-1",
exports[`should render search results correctly: default 1`] = `
<div>
<AzureProjectAccordion
+ importing={false}
key="p2 - result"
loading={false}
onOpen={[MockFunction]}
+ onSelectRepository={[MockFunction]}
project={
Object {
"key": "p2",