@@ -36,7 +36,7 @@ export interface BitbucketProjectAccordionProps { | |||
onClick?: () => void; | |||
onSelectRepository: (repo: BitbucketRepository) => void; | |||
open: boolean; | |||
project: BitbucketProject; | |||
project?: BitbucketProject; | |||
repositories: BitbucketRepository[]; | |||
selectedRepository?: BitbucketRepository; | |||
showingAllRepositories: boolean; | |||
@@ -54,6 +54,8 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi | |||
const repositoryCount = repositories.length; | |||
const title = project?.name ?? translate('search_results'); | |||
return ( | |||
<BoxedGroupAccordion | |||
className={classNames('big-spacer-bottom', { | |||
@@ -61,7 +63,6 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi | |||
'not-clickable': !props.onClick, | |||
'no-hover': !props.onClick | |||
})} | |||
key={project.key} | |||
onClick={ | |||
props.onClick | |||
? props.onClick | |||
@@ -70,64 +71,66 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi | |||
} | |||
} | |||
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"> | |||
@@ -137,7 +140,7 @@ export default function BitbucketProjectAccordion(props: BitbucketProjectAccordi | |||
)} | |||
</Alert> | |||
)} | |||
</div> | |||
</> | |||
)} | |||
</BoxedGroupAccordion> | |||
); |
@@ -34,6 +34,7 @@ import { | |||
} 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; | |||
@@ -146,11 +147,31 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S | |||
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 => { |
@@ -44,17 +44,32 @@ export default function BitbucketSearchResults(props: BitbucketSearchResultsProp | |||
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); | |||
@@ -39,6 +39,7 @@ it('should render correctly', () => { | |||
'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', () => { |
@@ -32,6 +32,9 @@ it('should render correctly', () => { | |||
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> = {}) { |
@@ -3,7 +3,6 @@ | |||
exports[`should render correctly: closed 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom" | |||
key="project" | |||
onClick={[MockFunction]} | |||
open={false} | |||
title={ | |||
@@ -17,7 +16,6 @@ exports[`should render correctly: closed 1`] = ` | |||
exports[`should render correctly: default 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom open" | |||
key="project" | |||
onClick={[MockFunction]} | |||
open={true} | |||
title={ | |||
@@ -90,7 +88,6 @@ exports[`should render correctly: default 1`] = ` | |||
exports[`should render correctly: disable options 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom open" | |||
key="project" | |||
onClick={[MockFunction]} | |||
open={true} | |||
title={ | |||
@@ -163,7 +160,6 @@ exports[`should render correctly: disable options 1`] = ` | |||
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={ | |||
@@ -233,10 +229,81 @@ exports[`should render correctly: no click handler 1`] = ` | |||
</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={ | |||
@@ -282,7 +349,6 @@ exports[`should render correctly: no repos 1`] = ` | |||
exports[`should render correctly: not showing all repos 1`] = ` | |||
<BoxedGroupAccordion | |||
className="big-spacer-bottom open" | |||
key="project" | |||
onClick={[MockFunction]} | |||
open={true} | |||
title={ | |||
@@ -348,19 +414,18 @@ exports[`should render correctly: not showing all repos 1`] = ` | |||
</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={ |
@@ -53,3 +53,30 @@ exports[`should render correctly: searching 1`] = ` | |||
/> | |||
</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> | |||
`; |
@@ -19,3 +19,5 @@ | |||
*/ | |||
export const PROJECT_NAME_MAX_LEN = 255; | |||
export const DEFAULT_BBS_PAGE_SIZE = 25; |
@@ -168,6 +168,7 @@ review=Review | |||
rule=Rule | |||
rules=Rules | |||
save=Save | |||
search_results=Search results | |||
search_verb=Search | |||
see_all=See all | |||
select_verb=Select |