@@ -27,13 +27,15 @@ import org.junit.Before; | |||
import org.junit.ClassRule; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonarqube.ws.WsComponents; | |||
import org.sonarqube.ws.client.component.SearchProjectsRequest; | |||
import org.sonarqube.ws.client.permission.RemoveGroupWsRequest; | |||
import org.sonarqube.ws.client.project.UpdateVisibilityRequest; | |||
import pageobjects.Navigation; | |||
import pageobjects.ProjectsManagementPage; | |||
import util.ItUtils; | |||
import static com.codeborne.selenide.Condition.text; | |||
import static com.codeborne.selenide.Selenide.$; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static util.ItUtils.newAdminWsClient; | |||
import static util.ItUtils.projectDir; | |||
@@ -58,10 +60,34 @@ public class ProjectsAdministrationTest { | |||
// Remove 'Admin' permission for admin group on project 2 -> No one can access or admin this project, expect System Admin | |||
newAdminWsClient(orchestrator).permissions().removeGroup(new RemoveGroupWsRequest().setProjectKey("sample2").setGroupName("sonar-administrators").setPermission("admin")); | |||
nav.logIn().asAdmin().open("/projects_admin"); | |||
$(".data.zebra") | |||
.shouldHave(text("sample1")) | |||
.shouldHave(text("sample2")); | |||
nav.logIn().asAdmin().openProjectsManagement() | |||
.shouldHaveProject("sample1") | |||
.shouldHaveProject("sample2"); | |||
} | |||
@Test | |||
public void create_public_project() { | |||
createProjectAndVerify("public"); | |||
} | |||
@Test | |||
public void create_private_project() { | |||
createProjectAndVerify("private"); | |||
} | |||
private void createProjectAndVerify(String visibility) { | |||
ProjectsManagementPage page = nav.logIn().asAdmin().openProjectsManagement(); | |||
page | |||
.shouldHaveProjectsCount(0) | |||
.createProject("foo", "foo", visibility) | |||
.shouldHaveProjectsCount(1); | |||
WsComponents.SearchProjectsWsResponse response = newAdminWsClient(orchestrator).components().searchProjects( | |||
SearchProjectsRequest.builder().build()); | |||
assertThat(response.getComponentsCount()).isEqualTo(1); | |||
assertThat(response.getComponents(0).getKey()).isEqualTo("foo"); | |||
assertThat(response.getComponents(0).getName()).isEqualTo("foo"); | |||
assertThat(response.getComponents(0).getVisibility()).isEqualTo(visibility); | |||
} | |||
} |
@@ -136,6 +136,10 @@ public class Navigation extends ExternalResource { | |||
return open(url, ProjectPermissionsPage.class); | |||
} | |||
public ProjectsManagementPage openProjectsManagement() { | |||
return open("/projects_admin", ProjectsManagementPage.class); | |||
} | |||
public LoginPage openLogin() { | |||
return open("/sessions/login", LoginPage.class); | |||
} |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
package pageobjects; | |||
import static com.codeborne.selenide.Condition.exist; | |||
import static com.codeborne.selenide.Condition.text; | |||
import static com.codeborne.selenide.Selenide.$; | |||
import static com.codeborne.selenide.Selenide.$$; | |||
public class ProjectsManagementPage { | |||
public ProjectsManagementPage() { | |||
$("#projects-management-page").should(exist); | |||
} | |||
public ProjectsManagementPage shouldHaveProjectsCount(int count) { | |||
$$("#projects-management-page-projects tr").shouldHaveSize(count); | |||
return this; | |||
} | |||
public ProjectsManagementPage shouldHaveProject(String key) { | |||
$("#projects-management-page-projects").shouldHave(text(key)); | |||
return this; | |||
} | |||
public ProjectsManagementPage createProject(String key, String name, String visibility) { | |||
$("#create-project").click(); | |||
$("#create-project-name").val(key); | |||
$("#create-project-key").val(name); | |||
$("#visibility-" + visibility).click(); | |||
$("#create-project-submit").submit(); | |||
return this; | |||
} | |||
} |
@@ -21,7 +21,7 @@ | |||
import React from 'react'; | |||
import { without } from 'lodash'; | |||
import PageHeader from './PageHeader'; | |||
import VisibilitySelector from './VisibilitySelector'; | |||
import VisibilitySelector from '../../../../components/common/VisibilitySelector'; | |||
import AllHoldersList from './AllHoldersList'; | |||
import PublicProjectDisclaimer from './PublicProjectDisclaimer'; | |||
import PageError from '../../shared/components/PageError'; | |||
@@ -331,6 +331,7 @@ export default class App extends React.PureComponent { | |||
<PageError /> | |||
{this.props.component.qualifier === 'TRK' && | |||
<VisibilitySelector | |||
className="big-spacer-top big-spacer-bottom" | |||
onChange={this.handleVisibilityChange} | |||
visibility={this.props.component.visibility} | |||
/>} |
@@ -40,6 +40,7 @@ function AppContainer(props) { | |||
hasProvisionPermission={hasProvisionPermission} | |||
topLevelQualifiers={topLevelQualifiers} | |||
onVisibilityChange={props.onVisibilityChange} | |||
onRequestFail={props.onRequestFail} | |||
organization={props.organization} | |||
/> | |||
); | |||
@@ -60,7 +61,8 @@ const onVisibilityChange = (organization, visibility) => dispatch => { | |||
}; | |||
const mapDispatchToProps = (dispatch, ownProps) => ({ | |||
onVisibilityChange: visibility => dispatch(onVisibilityChange(ownProps.organization, visibility)) | |||
onVisibilityChange: visibility => dispatch(onVisibilityChange(ownProps.organization, visibility)), | |||
onRequestFail: error => onFail(dispatch)(error) | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(AppContainer); |
@@ -0,0 +1,221 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import { Link } from 'react-router'; | |||
import VisibilitySelector from '../../components/common/VisibilitySelector'; | |||
import { createProject } from '../../api/components'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { getProjectUrl } from '../../helpers/urls'; | |||
import type { Organization } from '../../store/organizations/duck'; | |||
type Props = {| | |||
onClose: () => void, | |||
onProjectCreated: () => void, | |||
onRequestFail: Object => void, | |||
organization?: Organization | |||
|}; | |||
type State = { | |||
branch: string, | |||
createdProject?: Object, | |||
key: string, | |||
loading: boolean, | |||
name: string, | |||
visibility: string | |||
}; | |||
export default class CreateProjectForm extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
branch: '', | |||
key: '', | |||
loading: false, | |||
name: '', | |||
visibility: props.organization ? props.organization.projectVisibility : 'public' | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleCancelClick = (event: Event) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
handleInputChange = (event: { currentTarget: HTMLInputElement }) => { | |||
const { name, value } = event.currentTarget; | |||
this.setState({ [name]: value }); | |||
}; | |||
handleVisibilityChange = (visibility: string) => { | |||
this.setState({ visibility }); | |||
}; | |||
handleFormSubmit = (event: Event) => { | |||
event.preventDefault(); | |||
const data: { [string]: string } = { | |||
name: this.state.name, | |||
branch: this.state.branch, | |||
project: this.state.key, | |||
visibility: this.state.visibility | |||
}; | |||
if (this.props.organization) { | |||
data.organization = this.props.organization.key; | |||
} | |||
this.setState({ loading: true }); | |||
createProject(data).then( | |||
response => { | |||
if (this.mounted) { | |||
this.setState({ createdProject: response.project, loading: false }); | |||
this.props.onProjectCreated(); | |||
} | |||
}, | |||
error => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
this.props.onRequestFail(error); | |||
} | |||
} | |||
); | |||
}; | |||
render() { | |||
const { createdProject } = this.state; | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel="modal form" | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={this.props.onClose}> | |||
{createdProject | |||
? <div> | |||
<header className="modal-head"> | |||
<h2>{translate('qualifiers.create.TRK')}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
<div className="alert alert-success"> | |||
Project | |||
{' '} | |||
<Link to={getProjectUrl(createdProject.key)}>{createdProject.name}</Link> | |||
{' '} | |||
has been successfully created. | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
<a href="#" id="create-project-close" onClick={this.handleCancelClick}> | |||
{translate('close')} | |||
</a> | |||
</footer> | |||
</div> | |||
: <form id="create-project-form" onSubmit={this.handleFormSubmit}> | |||
<header className="modal-head"> | |||
<h2>{translate('qualifiers.create.TRK')}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
<div className="modal-field"> | |||
<label htmlFor="create-project-name"> | |||
{translate('name')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength="2000" | |||
name="name" | |||
onChange={this.handleInputChange} | |||
required={true} | |||
type="text" | |||
value={this.state.name} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="create-project-branch"> | |||
{translate('branch')} | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength="200" | |||
name="branch" | |||
onChange={this.handleInputChange} | |||
type="text" | |||
value={this.state.branch} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="create-project-key"> | |||
{translate('key')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength="400" | |||
name="key" | |||
onChange={this.handleInputChange} | |||
required={true} | |||
type="text" | |||
value={this.state.key} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label> {translate('visibility')} </label> | |||
<VisibilitySelector | |||
className="little-spacer-top" | |||
onChange={this.handleVisibilityChange} | |||
visibility={this.state.visibility} | |||
/> | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
{this.state.loading && <i className="spinner spacer-right" />} | |||
<button disabled={this.state.loading} id="create-project-submit" type="submit"> | |||
{translate('create')} | |||
</button> | |||
<a href="#" id="create-project-cancel" onClick={this.handleCancelClick}> | |||
{translate('cancel')} | |||
</a> | |||
</footer> | |||
</form>} | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -1,73 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
import ModalForm from '../../components/common/modal-form'; | |||
import { createProject } from '../../api/components'; | |||
import Template from './templates/projects-create-form.hbs'; | |||
export default ModalForm.extend({ | |||
template: Template, | |||
onRender() { | |||
ModalForm.prototype.onRender.apply(this, arguments); | |||
this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); | |||
}, | |||
onDestroy() { | |||
ModalForm.prototype.onDestroy.apply(this, arguments); | |||
this.$('[data-toggle="tooltip"]').tooltip('destroy'); | |||
}, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
this.sendRequest(); | |||
}, | |||
sendRequest() { | |||
const data = { | |||
name: this.$('#create-project-name').val(), | |||
branch: this.$('#create-project-branch').val(), | |||
project: this.$('#create-project-key').val() | |||
}; | |||
if (this.options.organization) { | |||
data.organization = this.options.organization.key; | |||
} | |||
this.disableForm(); | |||
return createProject(data) | |||
.then(project => { | |||
if (this.options.refresh) { | |||
this.options.refresh(); | |||
} | |||
this.enableForm(); | |||
this.createdProject = project; | |||
this.render(); | |||
}) | |||
.catch(error => { | |||
this.enableForm(); | |||
error.response.json().then(r => this.showErrors(r.errors, r.warnings)); | |||
}); | |||
}, | |||
serializeData() { | |||
return { | |||
...ModalForm.prototype.serializeData.apply(this, arguments), | |||
createdProject: this.createdProject | |||
}; | |||
} | |||
}); |
@@ -19,16 +19,15 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import CreateView from './create-view'; | |||
import ChangeVisibilityForm from './ChangeVisibilityForm'; | |||
import { translate } from '../../helpers/l10n'; | |||
import type { Organization } from '../../store/organizations/duck'; | |||
type Props = {| | |||
hasProvisionPermission: boolean, | |||
onProjectCreate: () => void, | |||
onVisibilityChange: string => void, | |||
organization?: Organization, | |||
refresh: () => void | |||
organization?: Organization | |||
|}; | |||
type State = { | |||
@@ -39,12 +38,10 @@ export default class Header extends React.PureComponent { | |||
props: Props; | |||
state: State = { visibilityForm: false }; | |||
createProject() { | |||
new CreateView({ | |||
refresh: this.props.refresh, | |||
organization: this.props.organization | |||
}).render(); | |||
} | |||
handleCreateProjectClick = (event: Event) => { | |||
event.preventDefault(); | |||
this.props.onProjectCreate(); | |||
}; | |||
handleChangeVisibilityClick = (event: Event) => { | |||
event.preventDefault(); | |||
@@ -55,17 +52,6 @@ export default class Header extends React.PureComponent { | |||
this.setState({ visibilityForm: false }); | |||
}; | |||
renderCreateButton() { | |||
if (!this.props.hasProvisionPermission) { | |||
return null; | |||
} | |||
return ( | |||
<button onClick={this.createProject.bind(this)}> | |||
Create Project | |||
</button> | |||
); | |||
} | |||
render() { | |||
const { organization } = this.props; | |||
@@ -86,7 +72,10 @@ export default class Header extends React.PureComponent { | |||
onClick={this.handleChangeVisibilityClick} | |||
/> | |||
</span>} | |||
{this.renderCreateButton()} | |||
{this.props.hasProvisionPermission && | |||
<button id="create-project" onClick={this.handleCreateProjectClick}> | |||
{translate('qualifiers.create.TRK')} | |||
</button>} | |||
</div> | |||
<p className="page-description"> | |||
{translate('projects_management.page.description')} |
@@ -23,6 +23,7 @@ import { debounce, uniq, without } from 'lodash'; | |||
import Header from './header'; | |||
import Search from './search'; | |||
import Projects from './projects'; | |||
import CreateProjectForm from './CreateProjectForm'; | |||
import { PAGE_SIZE, TYPE } from './constants'; | |||
import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components'; | |||
import ListFooter from '../../components/controls/ListFooter'; | |||
@@ -31,10 +32,12 @@ import type { Organization } from '../../store/organizations/duck'; | |||
type Props = {| | |||
hasProvisionPermission: boolean, | |||
onVisibilityChange: string => void, | |||
onRequestFail: Object => void, | |||
organization?: Organization | |||
|}; | |||
type State = { | |||
createProjectForm: boolean, | |||
ready: boolean, | |||
projects: Array<{ key: string }>, | |||
total: number, | |||
@@ -52,6 +55,7 @@ export default class Main extends React.PureComponent { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
createProjectForm: false, | |||
ready: false, | |||
projects: [], | |||
total: 0, | |||
@@ -216,12 +220,20 @@ export default class Main extends React.PureComponent { | |||
}); | |||
}; | |||
openCreateProjectForm = () => { | |||
this.setState({ createProjectForm: true }); | |||
}; | |||
closeCreateProjectForm = () => { | |||
this.setState({ createProjectForm: false }); | |||
}; | |||
render() { | |||
return ( | |||
<div className="page page-limited"> | |||
<div className="page page-limited" id="projects-management-page"> | |||
<Header | |||
hasProvisionPermission={this.props.hasProvisionPermission} | |||
refresh={this.requestProjects} | |||
onProjectCreate={this.openCreateProjectForm} | |||
onVisibilityChange={this.props.onVisibilityChange} | |||
organization={this.props.organization} | |||
/> | |||
@@ -253,6 +265,14 @@ export default class Main extends React.PureComponent { | |||
total={this.state.total} | |||
loadMore={this.loadMore} | |||
/> | |||
{this.state.createProjectForm && | |||
<CreateProjectForm | |||
onClose={this.closeCreateProjectForm} | |||
onProjectCreated={this.requestProjects} | |||
onRequestFail={this.props.onRequestFail} | |||
organization={this.props.organization} | |||
/>} | |||
</div> | |||
); | |||
} |
@@ -114,7 +114,7 @@ export default class Projects extends React.PureComponent { | |||
const className = classNames('data', 'zebra', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<table className={className}> | |||
<table className={className} id="projects-management-page-projects"> | |||
<tbody>{this.props.projects.map(this.renderProject)}</tbody> | |||
</table> | |||
); |
@@ -1,41 +0,0 @@ | |||
<form id="create-project-form" autocomplete="off"> | |||
<div class="modal-head"> | |||
<h2>Create Project</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
{{#if createdProject}} | |||
<div class="alert alert-success"> | |||
Project <a href="{{componentPermalink createdProject.k}}">{{createdProject.nm}}</a> has been successfully | |||
created. | |||
</div> | |||
{{else}} | |||
<div class="modal-field"> | |||
<label for="create-project-name">Name<em class="mandatory">*</em></label> | |||
{{! keep this fake field to hack browser autofill }} | |||
<input id="create-project-name-fake" name="name-fake" type="text" class="hidden"> | |||
<input id="create-project-name" name="name" type="text" maxlength="2000" required> | |||
</div> | |||
<div class="modal-field"> | |||
<label for="create-project-branch">Branch</label> | |||
{{! keep this fake field to hack browser autofill }} | |||
<input id="create-project-branch-fake" name="branch-fake" type="text" class="hidden"> | |||
<input id="create-project-branch" name="branch" type="text" maxlength="200"> | |||
</div> | |||
<div class="modal-field"> | |||
<label for="create-project-key">Key<em class="mandatory">*</em></label> | |||
{{! keep this fake field to hack browser autofill }} | |||
<input id="create-project-key-fake" name="key-fake" type="text" class="hidden"> | |||
<input id="create-project-key" name="key" type="text" maxlength="400" required> | |||
</div> | |||
{{/if}} | |||
</div> | |||
<div class="modal-foot"> | |||
{{#if createdProject}} | |||
<a href="#" class="js-modal-close">{{t 'close'}}</a> | |||
{{else}} | |||
<button id="create-project-submit">Create</button> | |||
<a href="#" class="js-modal-close" id="create-project-cancel">Cancel</a> | |||
{{/if}} | |||
</div> | |||
</form> |
@@ -20,12 +20,13 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { translate } from '../../helpers/l10n'; | |||
type Props = { | |||
type Props = {| | |||
className?: string, | |||
onChange: string => void, | |||
visibility: string | |||
}; | |||
|}; | |||
export default class VisibilitySelector extends React.PureComponent { | |||
props: Props; | |||
@@ -44,7 +45,7 @@ export default class VisibilitySelector extends React.PureComponent { | |||
render() { | |||
return ( | |||
<div className="big-spacer-top big-spacer-bottom"> | |||
<div className={this.props.className}> | |||
<a | |||
className="link-base-color link-no-underline" | |||
id="visibility-public" |
@@ -199,6 +199,7 @@ version=Version | |||
view=View | |||
views=Views | |||
violations=Violations | |||
visibility=Visibility | |||
with=With | |||
worst=Worst | |||