ソースを参照

SONAR-9167 Allow to set a visibility when creating a project

tags/6.4-RC1
Stas Vilchik 7年前
コミット
d0a3c62a54

+ 32
- 6
it/it-tests/src/test/java/it/administration/ProjectsAdministrationTest.java ファイルの表示

@@ -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);
}

}

+ 4
- 0
it/it-tests/src/test/java/pageobjects/Navigation.java ファイルの表示

@@ -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);
}

+ 51
- 0
it/it-tests/src/test/java/pageobjects/ProjectsManagementPage.java ファイルの表示

@@ -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;
}
}

+ 2
- 1
server/sonar-web/src/main/js/apps/permissions/project/components/App.js ファイルの表示

@@ -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}
/>}

+ 3
- 1
server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js ファイルの表示

@@ -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);

+ 221
- 0
server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js ファイルの表示

@@ -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>
);
}
}

+ 0
- 73
server/sonar-web/src/main/js/apps/projects-admin/create-view.js ファイルの表示

@@ -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
};
}
});

+ 10
- 21
server/sonar-web/src/main/js/apps/projects-admin/header.js ファイルの表示

@@ -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')}

+ 22
- 2
server/sonar-web/src/main/js/apps/projects-admin/main.js ファイルの表示

@@ -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>
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/projects-admin/projects.js ファイルの表示

@@ -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>
);

+ 0
- 41
server/sonar-web/src/main/js/apps/projects-admin/templates/projects-create-form.hbs ファイルの表示

@@ -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>

server/sonar-web/src/main/js/apps/permissions/project/components/VisibilitySelector.js → server/sonar-web/src/main/js/components/common/VisibilitySelector.js ファイルの表示

@@ -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"

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties ファイルの表示

@@ -199,6 +199,7 @@ version=Version
view=View
views=Views
violations=Violations
visibility=Visibility
with=With
worst=Worst


読み込み中…
キャンセル
保存