@@ -22,11 +22,12 @@ import * as classNames from 'classnames'; | |||
import AlmRepositoryItem from './AlmRepositoryItem'; | |||
import SetupProjectBox from './SetupProjectBox'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { getRepositories } from '../../../api/alm-integration'; | |||
import { isDefined } from '../../../helpers/types'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { translateWithParameters, translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
almApplication: T.AlmApplication; | |||
@@ -41,6 +42,7 @@ interface State { | |||
highlight: boolean; | |||
loading: boolean; | |||
repositories: T.AlmRepository[]; | |||
search: string; | |||
selectedRepositories: SelectedRepositories; | |||
successfullyUpgraded: boolean; | |||
} | |||
@@ -51,6 +53,7 @@ export default class RemoteRepositories extends React.PureComponent<Props, State | |||
highlight: false, | |||
loading: true, | |||
repositories: [], | |||
search: '', | |||
selectedRepositories: {}, | |||
successfullyUpgraded: false | |||
}; | |||
@@ -116,6 +119,10 @@ export default class RemoteRepositories extends React.PureComponent<Props, State | |||
}); | |||
}; | |||
handleSearch = (search: string) => { | |||
this.setState({ search }); | |||
}; | |||
toggleRepository = (repository: T.AlmRepository) => { | |||
this.setState(({ selectedRepositories }) => ({ | |||
selectedRepositories: { | |||
@@ -128,16 +135,29 @@ export default class RemoteRepositories extends React.PureComponent<Props, State | |||
}; | |||
render() { | |||
const { highlight, loading, repositories, selectedRepositories } = this.state; | |||
const { highlight, loading, repositories, search, selectedRepositories } = this.state; | |||
const { almApplication, organization } = this.props; | |||
const isPaidOrg = organization.subscription === 'PAID'; | |||
const hasPrivateRepositories = repositories.some(repository => Boolean(repository.private)); | |||
const showSearchBox = repositories.length > 5; | |||
const showUpgradebox = | |||
!isPaidOrg && hasPrivateRepositories && organization.actions && organization.actions.admin; | |||
const filteredRepositories = repositories.filter( | |||
repo => !search || repo.label.toLowerCase().includes(search.toLowerCase()) | |||
); | |||
return ( | |||
<div className="create-project"> | |||
<div className="flex-1 huge-spacer-right"> | |||
{showSearchBox && ( | |||
<div className="spacer-bottom"> | |||
<SearchBox | |||
minLength={1} | |||
onChange={this.handleSearch} | |||
placeholder={translate('search.search_for_repositories')} | |||
value={this.state.search} | |||
/> | |||
</div> | |||
)} | |||
{this.state.successfullyUpgraded && ( | |||
<Alert variant="success"> | |||
{translateWithParameters( | |||
@@ -148,7 +168,14 @@ export default class RemoteRepositories extends React.PureComponent<Props, State | |||
)} | |||
<DeferredSpinner loading={loading}> | |||
<ul> | |||
{repositories.map(repo => ( | |||
{filteredRepositories.length === 0 && ( | |||
<li className="big-spacer-top note"> | |||
{showUpgradebox | |||
? translateWithParameters('no_results_for_x', search) | |||
: translate('onboarding.create_project.no_repositories')} | |||
</li> | |||
)} | |||
{filteredRepositories.map(repo => ( | |||
<AlmRepositoryItem | |||
disabled={Boolean(repo.private && !isPaidOrg)} | |||
highlightUpgradeBox={this.handleHighlightUpgradeBox} | |||
@@ -163,22 +190,24 @@ export default class RemoteRepositories extends React.PureComponent<Props, State | |||
</DeferredSpinner> | |||
</div> | |||
{organization && ( | |||
<div className="huge-spacer-left"> | |||
<SetupProjectBox | |||
onProjectCreate={this.props.onProjectCreate} | |||
onProvisionFail={this.handleProvisionFail} | |||
organization={organization} | |||
selectedRepositories={Object.keys(selectedRepositories) | |||
.map(r => selectedRepositories[r]) | |||
.filter(isDefined)} | |||
/> | |||
{showUpgradebox && ( | |||
<UpgradeOrganizationBox | |||
className={classNames({ highlight })} | |||
onOrganizationUpgrade={this.handleOrganizationUpgrade} | |||
<div className={classNames({ 'create-project-side-with-search': showSearchBox })}> | |||
<div className="create-project-side-sticky"> | |||
<SetupProjectBox | |||
onProjectCreate={this.props.onProjectCreate} | |||
onProvisionFail={this.handleProvisionFail} | |||
organization={organization} | |||
selectedRepositories={Object.keys(selectedRepositories) | |||
.map(r => selectedRepositories[r]) | |||
.filter(isDefined)} | |||
/> | |||
)} | |||
{showUpgradebox && ( | |||
<UpgradeOrganizationBox | |||
className={classNames({ highlight })} | |||
onOrganizationUpgrade={this.handleOrganizationUpgrade} | |||
organization={organization} | |||
/> | |||
)} | |||
</div> | |||
</div> | |||
)} | |||
</div> |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { times } from 'lodash'; | |||
import { shallow } from 'enzyme'; | |||
import RemoteRepositories from '../RemoteRepositories'; | |||
import { getRepositories } from '../../../../api/alm-integration'; | |||
@@ -92,6 +93,20 @@ it('should not display the organization upgrade box', () => { | |||
expect(wrapper.find('UpgradeOrganizationBox').exists()).toBe(false); | |||
}); | |||
it('should display a search box to filter repositories', async () => { | |||
(getRepositories as jest.Mock<any>).mockResolvedValueOnce({ | |||
repositories: times(6, i => ({ label: `Project ${i}`, installationKey: `key-${i}` })) | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.find('SearchBox').exists()).toBe(true); | |||
expect(wrapper.find('AlmRepositoryItem')).toHaveLength(6); | |||
wrapper.find('SearchBox').prop<Function>('onChange')('3'); | |||
expect(wrapper.find('AlmRepositoryItem')).toHaveLength(1); | |||
}); | |||
function shallowRender(props: Partial<RemoteRepositories['props']> = {}) { | |||
return shallow( | |||
<RemoteRepositories |
@@ -11,28 +11,38 @@ exports[`should display the list of repositories 1`] = ` | |||
loading={true} | |||
timeout={100} | |||
> | |||
<ul /> | |||
<ul> | |||
<li | |||
className="big-spacer-top note" | |||
> | |||
onboarding.create_project.no_repositories | |||
</li> | |||
</ul> | |||
</DeferredSpinner> | |||
</div> | |||
<div | |||
className="huge-spacer-left" | |||
className="" | |||
> | |||
<SetupProjectBox | |||
onProjectCreate={[MockFunction]} | |||
onProvisionFail={[Function]} | |||
organization={ | |||
Object { | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "sonarsource", | |||
"name": "SonarSource", | |||
"subscription": "FREE", | |||
<div | |||
className="create-project-side-sticky" | |||
> | |||
<SetupProjectBox | |||
onProjectCreate={[MockFunction]} | |||
onProvisionFail={[Function]} | |||
organization={ | |||
Object { | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "sonarsource", | |||
"name": "SonarSource", | |||
"subscription": "FREE", | |||
} | |||
} | |||
} | |||
selectedRepositories={Array []} | |||
/> | |||
selectedRepositories={Array []} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
@@ -99,24 +109,28 @@ exports[`should display the list of repositories 2`] = ` | |||
</DeferredSpinner> | |||
</div> | |||
<div | |||
className="huge-spacer-left" | |||
className="" | |||
> | |||
<SetupProjectBox | |||
onProjectCreate={[MockFunction]} | |||
onProvisionFail={[Function]} | |||
organization={ | |||
Object { | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "sonarsource", | |||
"name": "SonarSource", | |||
"subscription": "FREE", | |||
<div | |||
className="create-project-side-sticky" | |||
> | |||
<SetupProjectBox | |||
onProjectCreate={[MockFunction]} | |||
onProvisionFail={[Function]} | |||
organization={ | |||
Object { | |||
"alm": Object { | |||
"key": "github", | |||
"url": "", | |||
}, | |||
"key": "sonarsource", | |||
"name": "SonarSource", | |||
"subscription": "FREE", | |||
} | |||
} | |||
} | |||
selectedRepositories={Array []} | |||
/> | |||
selectedRepositories={Array []} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; |
@@ -57,6 +57,15 @@ | |||
box-shadow: none; | |||
} | |||
.create-project-side-with-search { | |||
margin-top: calc(4 * var(--gridSize)); | |||
} | |||
.create-project-side-sticky { | |||
position: sticky; | |||
top: 68px; | |||
} | |||
.create-project-setup { | |||
display: flex; | |||
overflow: hidden; |
@@ -939,6 +939,7 @@ search.search_by_login_or_name=Search by login or name... | |||
search.search_by_name=Search by name... | |||
search.search_by_name_or_key=Search by name or key... | |||
search.search_for_tags=Search for tags... | |||
search.search_for_repositories=Search for repositories... | |||
search.search_for_rules=Search for rules... | |||
search.search_for_languages=Search for languages... | |||
search.search_for_cwe=Search for CWEs... | |||
@@ -2759,6 +2760,7 @@ onboarding.create_project.1_repository_created_as_public=1 repository will be cr | |||
onboarding.create_project.x_repository_created_as_public={0} repositories will be created as public projects on SonarCloud | |||
onboarding.create_project.1_repository_created_as_private=1 repository will be created as a private project on SonarCloud | |||
onboarding.create_project.x_repository_created_as_private={0} repositories will be created as private projects on SonarCloud | |||
onboarding.create_project.no_repositories=No repositories found for this organization. | |||
onboarding.create_project.organization=Organization | |||
onboarding.create_project.project_key=Project key | |||
onboarding.create_project.project_key.description=Up to 400 characters. All letters, digits, dash, underscore, point or colon. |