onClick?: () => void;
onSelectRepository: (repo: BitbucketRepository) => void;
open: boolean;
- project: BitbucketProject;
+ project?: BitbucketProject;
repositories: BitbucketRepository[];
selectedRepository?: BitbucketRepository;
showingAllRepositories: boolean;
const repositoryCount = repositories.length;
+ const title = project?.name ?? translate('search_results');
+
return (
<BoxedGroupAccordion
className={classNames('big-spacer-bottom', {
'not-clickable': !props.onClick,
'no-hover': !props.onClick
})}
- key={project.key}
onClick={
props.onClick
? props.onClick
}
}
open={open}
- title={<h3>{project.name}</h3>}>
+ title={<h3>{title}</h3>}>
{open && (
- <div className="display-flex-wrap">
- {repositoryCount === 0 && (
- <Alert variant="warning">
- <FormattedMessage
- defaultMessage={translate('onboarding.create_project.no_bbs_repos')}
- id="onboarding.create_project.no_bbs_repos"
- values={{
- link: (
- <Link
- to={{
- pathname: '/projects/create',
- query: { mode: CreateProjectModes.BitbucketServer, resetPat: 1 }
- }}>
- {translate('onboarding.create_project.update_your_token')}
- </Link>
- )
- }}
- />
- </Alert>
- )}
+ <>
+ <div className="display-flex-wrap">
+ {repositoryCount === 0 && (
+ <Alert variant="warning">
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_project.no_bbs_repos')}
+ id="onboarding.create_project.no_bbs_repos"
+ values={{
+ link: (
+ <Link
+ to={{
+ pathname: '/projects/create',
+ query: { mode: CreateProjectModes.BitbucketServer, resetPat: 1 }
+ }}>
+ {translate('onboarding.create_project.update_your_token')}
+ </Link>
+ )
+ }}
+ />
+ </Alert>
+ )}
- {repositories.map(repo =>
- repo.sqProjectKey ? (
- <div
- className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo"
- key={repo.id}>
- <CheckIcon className="spacer-right" fill={colors.green} size={14} />
- <div className="overflow-hidden">
- <div className="little-spacer-bottom text-ellipsis">
- <strong title={repo.name}>
- <Link to={getProjectUrl(repo.sqProjectKey)}>{repo.name}</Link>
- </strong>
+ {repositories.map(repo =>
+ repo.sqProjectKey ? (
+ <div
+ className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo"
+ key={repo.id}>
+ <CheckIcon className="spacer-right" fill={colors.green} size={14} />
+ <div className="overflow-hidden">
+ <div className="little-spacer-bottom text-ellipsis">
+ <strong title={repo.name}>
+ <Link to={getProjectUrl(repo.sqProjectKey)}>{repo.name}</Link>
+ </strong>
+ </div>
+ <em>{translate('onboarding.create_project.repository_imported')}</em>
</div>
- <em>{translate('onboarding.create_project.repository_imported')}</em>
</div>
- </div>
- ) : (
- <Radio
- checked={selectedRepository?.id === repo.id}
- className={classNames(
- 'display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden',
- {
- disabled: disableRepositories,
- 'text-muted': disableRepositories,
- 'link-no-underline': disableRepositories
- }
- )}
- key={repo.id}
- onCheck={() => props.onSelectRepository(repo)}
- value={String(repo.id)}>
- <strong className="text-ellipsis" title={repo.name}>
- {repo.name}
- </strong>
- </Radio>
- )
- )}
+ ) : (
+ <Radio
+ checked={selectedRepository?.id === repo.id}
+ className={classNames(
+ 'display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden',
+ {
+ disabled: disableRepositories,
+ 'text-muted': disableRepositories,
+ 'link-no-underline': disableRepositories
+ }
+ )}
+ key={repo.id}
+ onCheck={() => props.onSelectRepository(repo)}
+ value={String(repo.id)}>
+ <strong className="text-ellipsis" title={repo.name}>
+ {repo.name}
+ </strong>
+ </Radio>
+ )
+ )}
+ </div>
{!showingAllRepositories && repositoryCount > 0 && (
<Alert variant="warning">
)}
</Alert>
)}
- </div>
+ </>
)}
</BoxedGroupAccordion>
);
} from '../../../types/alm-integration';
import { AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
+import { DEFAULT_BBS_PAGE_SIZE } from './constants';
interface Props extends Pick<WithRouterProps, 'location'> {
canAdmin: boolean;
return Promise.all(
projects.map(p => {
return getBitbucketServerRepositories(bitbucketSetting.key, p.name).then(
- ({ isLastPage, repositories }) => ({
- isLastPage,
- repositories,
- projectKey: p.key
- })
+ ({ isLastPage, repositories }) => {
+ // Because the WS uses the project name rather than its key to find
+ // repositories, we can match more repositories than we expect. For
+ // example, p.name = "A1" would find repositories for projects "A1",
+ // "A10", "A11", etc. This is a limitation of BBS. To make sure we
+ // don't display incorrect information, filter on the project key.
+ const filteredRepositories = repositories.filter(r => r.projectKey === p.key);
+
+ // And because of the above, the "isLastPage" cannot be relied upon
+ // either. This one is impossible to get 100% for now. We can only
+ // make some assumptions: by default, the page size for BBS is 25
+ // (this is not part of the payload, so we don't know the actual
+ // number; but changing this implies changing some advanced config,
+ // so it's not likely). If the filtered repos is larger than this
+ // number AND isLastPage is false, we'll keep it at false.
+ // Otherwise, we assume it's true.
+ const realIsLastPage =
+ isLastPage || filteredRepositories.length < DEFAULT_BBS_PAGE_SIZE;
+
+ return {
+ repositories: filteredRepositories,
+ isLastPage: realIsLastPage,
+ projectKey: p.key
+ };
+ }
);
})
).then(results => {
selectedRepository
} = props;
+ if (searchResults.length === 0 && !searching) {
+ return (
+ <Alert className="big-spacer-top" variant="warning">
+ {translate('onboarding.create_project.no_bbs_repos.filter')}
+ </Alert>
+ );
+ }
+
const filteredProjects = uniq(
searchResults.map(r => projects.find(p => p.key === r.projectKey)).filter(isDefined)
);
- return filteredProjects.length === 0 && !searching ? (
- <Alert className="big-spacer-top" variant="warning">
- {translate('onboarding.create_project.no_bbs_repos.filter')}
- </Alert>
- ) : (
+ return (
<div className="big-spacer-top">
<DeferredSpinner loading={searching}>
+ {filteredProjects.length === 0 && searchResults.length > 0 && (
+ <BitbucketProjectAccordion
+ disableRepositories={disableRepositories}
+ onSelectRepository={props.onSelectRepository}
+ open={true}
+ repositories={searchResults}
+ selectedRepository={selectedRepository}
+ showingAllRepositories={true}
+ />
+ )}
+
{filteredProjects.map(project => {
const repositories = searchResults.filter(r => r.projectKey === project.key);
'selected repo'
);
expect(shallowRender({ showingAllRepositories: false })).toMatchSnapshot('not showing all repos');
+ expect(shallowRender({ project: undefined })).toMatchSnapshot('no project info');
});
it('should correctly handle selecting repos', () => {
shallowRender({ searching: true, projects: undefined, searchResults: undefined })
).toMatchSnapshot('searching');
expect(shallowRender({ searchResults: undefined })).toMatchSnapshot('no results');
+ expect(
+ shallowRender({ searchResults: [mockBitbucketRepository({ projectKey: 'unknown' })] })
+ ).toMatchSnapshot('unknown project in search results');
});
function shallowRender(props: Partial<BitbucketSearchResultsProps> = {}) {
exports[`should render correctly: closed 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom"
- key="project"
onClick={[MockFunction]}
open={false}
title={
exports[`should render correctly: default 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
- key="project"
onClick={[MockFunction]}
open={true}
title={
exports[`should render correctly: disable options 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
- key="project"
onClick={[MockFunction]}
open={true}
title={
exports[`should render correctly: no click handler 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open not-clickable no-hover"
- key="project"
onClick={[Function]}
open={true}
title={
</BoxedGroupAccordion>
`;
+exports[`should render correctly: no project info 1`] = `
+<BoxedGroupAccordion
+ className="big-spacer-bottom open"
+ onClick={[MockFunction]}
+ open={true}
+ title={
+ <h3>
+ search_results
+ </h3>
+ }
+>
+ <div
+ className="display-flex-wrap"
+ >
+ <Radio
+ checked={false}
+ className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo overflow-hidden"
+ key="1"
+ onCheck={[Function]}
+ value="1"
+ >
+ <strong
+ className="text-ellipsis"
+ title="Repo"
+ >
+ Repo
+ </strong>
+ </Radio>
+ <div
+ className="display-flex-start spacer-right spacer-bottom create-project-import-bbs-repo"
+ key="2"
+ >
+ <CheckIcon
+ className="spacer-right"
+ fill="#00aa00"
+ size={14}
+ />
+ <div
+ className="overflow-hidden"
+ >
+ <div
+ className="little-spacer-bottom text-ellipsis"
+ >
+ <strong
+ title="Bar"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "bar",
+ },
+ }
+ }
+ >
+ Bar
+ </Link>
+ </strong>
+ </div>
+ <em>
+ onboarding.create_project.repository_imported
+ </em>
+ </div>
+ </div>
+ </div>
+</BoxedGroupAccordion>
+`;
+
exports[`should render correctly: no repos 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
- key="project"
onClick={[MockFunction]}
open={true}
title={
exports[`should render correctly: not showing all repos 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
- key="project"
onClick={[MockFunction]}
open={true}
title={
</em>
</div>
</div>
- <Alert
- variant="warning"
- >
- onboarding.create_project.only_showing_X_first_repos.2
- </Alert>
</div>
+ <Alert
+ variant="warning"
+ >
+ onboarding.create_project.only_showing_X_first_repos.2
+ </Alert>
</BoxedGroupAccordion>
`;
exports[`should render correctly: selected repo 1`] = `
<BoxedGroupAccordion
className="big-spacer-bottom open"
- key="project"
onClick={[MockFunction]}
open={true}
title={
/>
</div>
`;
+
+exports[`should render correctly: unknown project in search results 1`] = `
+<div
+ className="big-spacer-top"
+>
+ <DeferredSpinner
+ loading={false}
+ >
+ <BitbucketProjectAccordion
+ disableRepositories={false}
+ onSelectRepository={[MockFunction]}
+ open={true}
+ repositories={
+ Array [
+ Object {
+ "id": 1,
+ "name": "Repo",
+ "projectKey": "unknown",
+ "slug": "project__repo",
+ },
+ ]
+ }
+ showingAllRepositories={true}
+ />
+ </DeferredSpinner>
+</div>
+`;
*/
export const PROJECT_NAME_MAX_LEN = 255;
+
+export const DEFAULT_BBS_PAGE_SIZE = 25;
rule=Rule
rules=Rules
save=Save
+search_results=Search results
search_verb=Search
see_all=See all
select_verb=Select