Browse Source

SONAR-9165 Allow to change project visibility on its permissions page

tags/6.4-RC1
Stas Vilchik 7 years ago
parent
commit
cfa382b33e

+ 22
- 3
it/it-tests/src/test/java/it/projectAdministration/ProjectPermissionsTest.java View File

@@ -22,8 +22,12 @@ package it.projectAdministration;
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import it.Category1Suite;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import pageobjects.Navigation;
import pageobjects.ProjectPermissionsPage;

import static util.ItUtils.projectDir;
import static util.selenium.Selenese.runSelenese;
@@ -33,15 +37,30 @@ public class ProjectPermissionsTest {
@ClassRule
public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;

@Test
public void test_project_permissions_page_shows_only_single_project() throws Exception {
@Rule
public Navigation nav = Navigation.get(orchestrator);

@BeforeClass
public static void beforeClass() {
executeBuild("project-permissions-project", "Test Project");
executeBuild("project-permissions-project-2", "Another Test Project");
}

@Test
public void test_project_permissions_page_shows_only_single_project() throws Exception {
runSelenese(orchestrator, "/projectAdministration/ProjectPermissionsTest/test_project_permissions_page_shows_only_single_project.html");
}

private void executeBuild(String projectKey, String projectName) {
@Test
public void change_project_visibility() {
ProjectPermissionsPage page = nav.logIn().asAdmin().openProjectPermissions("project-permissions-project");
page
.shouldBePublic()
.turnToPrivate()
.turnToPublic();
}

private static void executeBuild(String projectKey, String projectName) {
orchestrator.executeBuild(
SonarScanner.create(projectDir("shared/xoo-sample"))
.setProjectKey(projectKey)

+ 5
- 0
it/it-tests/src/test/java/pageobjects/Navigation.java View File

@@ -131,6 +131,11 @@ public class Navigation extends ExternalResource {
return open("/account/notifications", NotificationsPage.class);
}

public ProjectPermissionsPage openProjectPermissions(String projectKey) {
String url = "/project_roles?id=" + projectKey;
return open(url, ProjectPermissionsPage.class);
}

public LoginPage openLogin() {
return open("/sessions/login", LoginPage.class);
}

+ 54
- 0
it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java View File

@@ -0,0 +1,54 @@
/*
* 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.visible;
import static com.codeborne.selenide.Selenide.$;

public class ProjectPermissionsPage {

public ProjectPermissionsPage() {
$("#project-permissions-page").should(exist);
}

public ProjectPermissionsPage shouldBePublic() {
$("#visibility-public .icon-radio.is-checked").shouldBe(visible);
return this;
}

public ProjectPermissionsPage shouldBePrivate() {
$("#visibility-private .icon-radio.is-checked").shouldBe(visible);
return this;
}

public ProjectPermissionsPage turnToPublic() {
$("#visibility-public").click();
$("#confirm-turn-to-public").click();
shouldBePublic();
return this;
}

public ProjectPermissionsPage turnToPrivate() {
$("#visibility-private").click();
shouldBePrivate();
return this;
}
}

+ 6
- 0
server/sonar-web/src/main/js/api/permissions.js View File

@@ -289,3 +289,9 @@ export function getPermissionTemplateGroups(
}
return getJSON(url, data).then(r => r.groups);
}

export function changeProjectVisibility(project: string, visibility: string): Promise<void> {
const url = '/api/projects/update_visibility';
const data = { project, visibility };
return post(url, data);
}

+ 14
- 3
server/sonar-web/src/main/js/app/components/ProjectContainer.js View File

@@ -24,6 +24,7 @@ import ComponentNav from './nav/component/ComponentNav';
import { fetchProject } from '../../store/rootActions';
import { getComponent } from '../../store/rootReducer';
import { addGlobalErrorMessage } from '../../store/globalMessages/duck';
import { receiveComponents } from '../../store/components/actions';
import { parseError } from '../../apps/code/utils';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';

@@ -38,8 +39,10 @@ class ProjectContainer extends React.PureComponent {
configuration: {},
qualifier: string
},
fetchProject: string => Promise<*>
fetchProject: string => Promise<*>,
receiveComponents: Array<*> => void
};

componentDidMount() {
this.fetchProject();
}
@@ -60,6 +63,10 @@ class ProjectContainer extends React.PureComponent {
});
}

handleProjectChange = (changes: {}) => {
this.props.receiveComponents([{ ...this.props.project, ...changes }]);
};

render() {
// check `breadcrumbs` to be sure that /api/navigation/component has been already called
if (!this.props.project || this.props.project.breadcrumbs == null) {
@@ -79,7 +86,11 @@ class ProjectContainer extends React.PureComponent {
conf={configuration}
location={this.props.location}
/>}
{this.props.children}
{/* $FlowFixMe */}
{React.cloneElement(this.props.children, {
component: this.props.project,
onComponentChange: this.handleProjectChange
})}
</div>
);
}
@@ -89,6 +100,6 @@ const mapStateToProps = (state, ownProps) => ({
project: getComponent(state, ownProps.location.query.id)
});

const mapDispatchToProps = { addGlobalErrorMessage, fetchProject };
const mapDispatchToProps = { addGlobalErrorMessage, fetchProject, receiveComponents };

export default connect(mapStateToProps, mapDispatchToProps)(ProjectContainer);

+ 57
- 86
server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js View File

@@ -19,72 +19,77 @@
*/
// @flow
import React from 'react';
import { connect } from 'react-redux';
import { without } from 'lodash';
import SearchForm from '../../shared/components/SearchForm';
import HoldersList from '../../shared/components/HoldersList';
import {
loadHolders,
grantToUser,
revokeFromUser,
grantToGroup,
revokeFromGroup,
updateQuery,
updateFilter,
selectPermission
} from '../store/actions';
import { translate } from '../../../../helpers/l10n';
import { PERMISSIONS_ORDER_BY_QUALIFIER } from '../constants';
import {
getPermissionsAppUsers,
getPermissionsAppGroups,
getPermissionsAppQuery,
getPermissionsAppFilter,
getPermissionsAppSelectedPermission
} from '../../../../store/rootReducer';

class AllHoldersList extends React.PureComponent {
static propTypes = {
project: React.PropTypes.object.isRequired
};

componentDidMount() {
this.props.loadHolders(this.props.project.key);
}

handleSearch(query) {
this.props.onSearch(this.props.project.key, query);
}

handleFilter(filter) {
this.props.onFilter(this.props.project.key, filter);
}

handleToggleUser(user, permission) {
type Props = {|
component: {
configuration?: {
canApplyPermissionTemplate: boolean
},
key: string,
organization: string,
qualifier: string,
visibility: string
},
filter: string,
grantPermissionToGroup: (group: string, permission: string) => void,
grantPermissionToUser: (user: string, permission: string) => void,
groups: Array<{
name: string,
permissions: Array<string>
}>,
onFilterChange: string => void,
onPermissionSelect: (string | void) => void,
onQueryChange: string => void,
query: string,
revokePermissionFromGroup: (group: string, permission: string) => void,
revokePermissionFromUser: (user: string, permission: string) => void,
selectedPermission: ?string,
visibility: string,
users: Array<{
login: string,
name: string,
permissions: Array<string>
}>
|};

export default class AllHoldersList extends React.PureComponent {
props: Props;

handleToggleUser = (user: Object, permission: string) => {
const hasPermission = user.permissions.includes(permission);

if (hasPermission) {
this.props.revokePermissionFromUser(this.props.project.key, user.login, permission);
this.props.revokePermissionFromUser(user.login, permission);
} else {
this.props.grantPermissionToUser(this.props.project.key, user.login, permission);
this.props.grantPermissionToUser(user.login, permission);
}
}
};

handleToggleGroup(group, permission) {
handleToggleGroup = (group: Object, permission: string) => {
const hasPermission = group.permissions.includes(permission);

if (hasPermission) {
this.props.revokePermissionFromGroup(this.props.project.key, group.name, permission);
this.props.revokePermissionFromGroup(group.name, permission);
} else {
this.props.grantPermissionToGroup(this.props.project.key, group.name, permission);
this.props.grantPermissionToGroup(group.name, permission);
}
}
};

handleSelectPermission(permission) {
this.props.onSelectPermission(this.props.project.key, permission);
}
handleSelectPermission = (permission?: string) => {
this.props.onPermissionSelect(permission);
};

render() {
const order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.project.qualifier];
let order = PERMISSIONS_ORDER_BY_QUALIFIER[this.props.component.qualifier];
if (this.props.visibility === 'public') {
order = without(order, 'user', 'codeviewer');
}

const permissions = order.map(p => ({
key: p,
name: translate('projects_role', p),
@@ -97,52 +102,18 @@ class AllHoldersList extends React.PureComponent {
selectedPermission={this.props.selectedPermission}
users={this.props.users}
groups={this.props.groups}
onSelectPermission={this.handleSelectPermission.bind(this)}
onToggleUser={this.handleToggleUser.bind(this)}
onToggleGroup={this.handleToggleGroup.bind(this)}>
onSelectPermission={this.handleSelectPermission}
onToggleUser={this.handleToggleUser}
onToggleGroup={this.handleToggleGroup}>

<SearchForm
query={this.props.query}
filter={this.props.filter}
onSearch={this.handleSearch.bind(this)}
onFilter={this.handleFilter.bind(this)}
onSearch={this.props.onQueryChange}
onFilter={this.props.onFilterChange}
/>

</HoldersList>
);
}
}

const mapStateToProps = state => ({
users: getPermissionsAppUsers(state),
groups: getPermissionsAppGroups(state),
query: getPermissionsAppQuery(state),
filter: getPermissionsAppFilter(state),
selectedPermission: getPermissionsAppSelectedPermission(state)
});

type OwnProps = {
project: {
organization?: string
}
};

const mapDispatchToProps = (dispatch: Function, ownProps: OwnProps) => ({
loadHolders: projectKey => dispatch(loadHolders(projectKey, ownProps.project.organization)),
onSearch: (projectKey, query) =>
dispatch(updateQuery(projectKey, query, ownProps.project.organization)),
onFilter: (projectKey, filter) =>
dispatch(updateFilter(projectKey, filter, ownProps.project.organization)),
onSelectPermission: (projectKey, permission) =>
dispatch(selectPermission(projectKey, permission, ownProps.project.organization)),
grantPermissionToUser: (projectKey, login, permission) =>
dispatch(grantToUser(projectKey, login, permission, ownProps.project.organization)),
revokePermissionFromUser: (projectKey, login, permission) =>
dispatch(revokeFromUser(projectKey, login, permission, ownProps.project.organization)),
grantPermissionToGroup: (projectKey, groupName, permission) =>
dispatch(grantToGroup(projectKey, groupName, permission, ownProps.project.organization)),
revokePermissionFromGroup: (projectKey, groupName, permission) =>
dispatch(revokeFromGroup(projectKey, groupName, permission, ownProps.project.organization))
});

export default connect(mapStateToProps, mapDispatchToProps)(AllHoldersList);

+ 325
- 18
server/sonar-web/src/main/js/apps/permissions/project/components/App.js View File

@@ -17,39 +17,346 @@
* 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 { connect } from 'react-redux';
import { without } from 'lodash';
import PageHeader from './PageHeader';
import VisibilitySelector from './VisibilitySelector';
import AllHoldersList from './AllHoldersList';
import PublicProjectDisclaimer from './PublicProjectDisclaimer';
import PageError from '../../shared/components/PageError';
import { getComponent, getCurrentUser } from '../../../../store/rootReducer';
import * as api from '../../../../api/permissions';
import '../../styles.css';

// TODO helmet

class App extends React.PureComponent {
static propTypes = {
component: React.PropTypes.object
export type Props = {|
component: {
configuration?: {
canApplyPermissionTemplate: boolean
},
key: string,
name: string,
organization: string,
qualifier: string,
visibility: string
},
onComponentChange: () => void,
onRequestFail: Object => void
|};

export type State = {|
disclaimer: boolean,
filter: string,
groups: Array<{
name: string,
permissions: Array<string>
}>,
loading: boolean,
query: string,
selectedPermission?: string,
users: Array<{
login: string,
name: string,
permissions: Array<string>
}>
|};

export default class App extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = {
disclaimer: false,
filter: 'all',
groups: [],
loading: true,
query: '',
users: []
};
}

componentDidMount() {
this.mounted = true;
this.loadHolders();
}

componentWillUnmount() {
this.mounted = false;
}

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

render() {
if (!this.props.component) {
return null;
loadHolders = () => {
if (this.mounted) {
this.setState({ loading: true });

const { component } = this.props;
const { filter, query, selectedPermission } = this.state;

const getUsers = filter !== 'groups'
? api.getPermissionsUsersForComponent(
component.key,
query,
selectedPermission,
component.organization
)
: Promise.resolve([]);

const getGroups = filter !== 'users'
? api.getPermissionsGroupsForComponent(
component.key,
query,
selectedPermission,
component.organization
)
: Promise.resolve([]);

Promise.all([getUsers, getGroups]).then(
responses => {
if (this.mounted) {
this.setState({ loading: false, groups: responses[1], users: responses[0] });
}
},
error => {
if (this.mounted) {
this.props.onRequestFail(error);
this.setState({ loading: false });
}
}
);
}
};

handleFilterChange = (filter: string) => {
if (this.mounted) {
this.setState({ filter }, this.loadHolders);
}
};

handleQueryChange = (query: string) => {
if (this.mounted) {
this.setState({ query }, () => {
if (query.length === 0 || query.length > 2) {
this.loadHolders();
}
});
}
};

handlePermissionSelect = (selectedPermission?: string) => {
if (this.mounted) {
this.setState(
(state: State) => ({
selectedPermission: state.selectedPermission === selectedPermission
? undefined
: selectedPermission
}),
this.loadHolders
);
}
};

addPermissionToGroup = (group: string, permission: string) =>
this.state.groups.map(
candidate =>
(candidate.name === group
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate)
);

addPermissionToUser = (user: string, permission: string) =>
this.state.users.map(
candidate =>
(candidate.login === user
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate)
);

removePermissionFromGroup = (group: string, permission: string) =>
this.state.groups.map(
candidate =>
(candidate.name === group
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate)
);

removePermissionFromUser = (user: string, permission: string) =>
this.state.users.map(
candidate =>
(candidate.login === user
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate)
);

grantPermissionToGroup = (group: string, permission: string) => {
if (this.mounted) {
this.setState({ loading: true, groups: this.addPermissionToGroup(group, permission) });
api
.grantPermissionToGroup(
this.props.component.key,
group,
permission,
this.props.component.organization
)
.then(this.stopLoading, error => {
if (this.mounted) {
this.setState({
loading: false,
groups: this.removePermissionFromGroup(group, permission)
});
this.props.onRequestFail(error);
}
});
}
};

grantPermissionToUser = (user: string, permission: string) => {
if (this.mounted) {
this.setState({ loading: true, users: this.addPermissionToUser(user, permission) });
api
.grantPermissionToUser(
this.props.component.key,
user,
permission,
this.props.component.organization
)
.then(this.stopLoading, error => {
if (this.mounted) {
this.setState({
loading: false,
users: this.removePermissionFromUser(user, permission)
});
this.props.onRequestFail(error);
}
});
}
};

revokePermissionFromGroup = (group: string, permission: string) => {
if (this.mounted) {
this.setState({ loading: true, groups: this.removePermissionFromGroup(group, permission) });
api
.revokePermissionFromGroup(
this.props.component.key,
group,
permission,
this.props.component.organization
)
.then(this.stopLoading, error => {
if (this.mounted) {
this.setState({
loading: false,
groups: this.addPermissionToGroup(group, permission)
});
this.props.onRequestFail(error);
}
});
}
};

revokePermissionFromUser = (user: string, permission: string) => {
if (this.mounted) {
this.setState({ loading: true, users: this.removePermissionFromUser(user, permission) });
api
.revokePermissionFromUser(
this.props.component.key,
user,
permission,
this.props.component.organization
)
.then(this.stopLoading, error => {
if (this.mounted) {
this.setState({
loading: false,
users: this.addPermissionToUser(user, permission)
});
this.props.onRequestFail(error);
}
});
}
};

handleVisibilityChange = (visibility: string) => {
if (visibility === 'public') {
this.openDisclaimer();
} else {
this.turnProjectToPrivate();
}
};

turnProjectToPublic = () => {
this.props.onComponentChange({ visibility: 'public' });
api.changeProjectVisibility(this.props.component.key, 'public').catch(error => {
this.props.onComponentChange({ visibility: 'private' });
this.props.onRequestFail(error);
});
};

turnProjectToPrivate = () => {
this.props.onComponentChange({ visibility: 'private' });
api.changeProjectVisibility(this.props.component.key, 'private').catch(error => {
this.props.onComponentChange({ visibility: 'public' });
this.props.onRequestFail(error);
});
};

openDisclaimer = () => {
if (this.mounted) {
this.setState({ disclaimer: true });
}
};

closeDisclaimer = () => {
if (this.mounted) {
this.setState({ disclaimer: false });
}
};

render() {
return (
<div className="page page-limited">
<PageHeader project={this.props.component} currentUser={this.props.currentUser} />
<div className="page page-limited" id="project-permissions-page">
<PageHeader
component={this.props.component}
loading={this.state.loading}
loadHolders={this.loadHolders}
/>
<PageError />
<AllHoldersList project={this.props.component} />
{this.props.component.qualifier === 'TRK' &&
<VisibilitySelector
onChange={this.handleVisibilityChange}
visibility={this.props.component.visibility}
/>}
{this.state.disclaimer &&
<PublicProjectDisclaimer
component={this.props.component}
onClose={this.closeDisclaimer}
onConfirm={this.turnProjectToPublic}
/>}
<AllHoldersList
component={this.props.component}
filter={this.state.filter}
grantPermissionToGroup={this.grantPermissionToGroup}
grantPermissionToUser={this.grantPermissionToUser}
groups={this.state.groups}
onFilterChange={this.handleFilterChange}
onPermissionSelect={this.handlePermissionSelect}
onQueryChange={this.handleQueryChange}
query={this.state.query}
revokePermissionFromGroup={this.revokePermissionFromGroup}
revokePermissionFromUser={this.revokePermissionFromUser}
selectedPermission={this.state.selectedPermission}
visibility={this.props.component.visibility}
users={this.state.users}
/>
</div>
);
}
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(App);

+ 33
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.js View File

@@ -0,0 +1,33 @@
/*
* 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 { connect } from 'react-redux';
import App from './App';
import { onFail } from '../../../../store/rootActions';
import { getCurrentUser } from '../../../../store/rootReducer';

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

const mapDispatchToProps = dispatch => ({
onRequestFail: onFail(dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

+ 30
- 35
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js View File

@@ -17,47 +17,51 @@
* 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 { connect } from 'react-redux';
import { translate } from '../../../../helpers/l10n';
import ApplyTemplateView from '../views/ApplyTemplateView';
import { loadHolders } from '../store/actions';
import { isPermissionsAppLoading } from '../../../../store/rootReducer';

class PageHeader extends React.PureComponent {
static propTypes = {
project: React.PropTypes.object.isRequired,
loadHolders: React.PropTypes.func.isRequired,
loading: React.PropTypes.bool
};
type Props = {|
component: {
configuration?: {
canApplyPermissionTemplate: boolean
},
key: string,
qualifier: string,
visibility: string
},
loadHolders: () => void,
loading: boolean
|};

static defaultProps = {
loading: false
};
export default class PageHeader extends React.PureComponent {
props: Props;

componentWillMount() {
this.handleApplyTemplate = this.handleApplyTemplate.bind(this);
}

handleApplyTemplate(e) {
handleApplyTemplate = (e: Event & { target: HTMLButtonElement }) => {
e.preventDefault();
e.target.blur();
const { project, loadHolders } = this.props;
const organization = project.organization ? { key: project.organization } : null;
new ApplyTemplateView({ project, organization })
.on('done', () => loadHolders(project.key))
const { component, loadHolders } = this.props;
const organization = component.organization ? { key: component.organization } : null;
new ApplyTemplateView({ project: component, organization })
.on('done', () => loadHolders())
.render();
}
};

render() {
const configuration = this.props.project.configuration;
const { component } = this.props;
const configuration = component.configuration;
const canApplyPermissionTemplate =
configuration != null && configuration.canApplyPermissionTemplate;

const description = ['VW', 'SVW'].includes(this.props.project.qualifier)
const description = ['VW', 'SVW'].includes(component.qualifier)
? translate('roles.page.description_portfolio')
: translate('roles.page.description2');

const visibilityDescription = component.qualifier === 'TRK'
? translate('visibility', component.visibility, 'description')
: null;

return (
<header className="page-header">
<h1 className="page-title">
@@ -74,19 +78,10 @@ class PageHeader extends React.PureComponent {
</div>}

<div className="page-description">
{description}
<p>{description}</p>
{visibilityDescription != null && <p>{visibilityDescription}</p>}
</div>
</header>
);
}
}

const mapStateToProps = state => ({
loading: isPermissionsAppLoading(state)
});

const mapDispatchToProps = (dispatch, ownProps) => ({
loadHolders: projectKey => dispatch(loadHolders(projectKey, ownProps.project.organization))
});

export default connect(mapStateToProps, mapDispatchToProps)(PageHeader);

+ 79
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.js View File

@@ -0,0 +1,79 @@
/*
* 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 { translate, translateWithParameters } from '../../../../helpers/l10n';

type Props = {
component: {
name: string
},
onClose: () => void,
onConfirm: () => void
};

export default class PublicProjectDisclaimer extends React.PureComponent {
props: Props;

handleCancelClick = (event: Event) => {
event.preventDefault();
this.props.onClose();
};

handleConfirmClick = (event: Event) => {
event.preventDefault();
this.props.onConfirm();
this.props.onClose();
};

render() {
return (
<Modal
isOpen={true}
contentLabel="modal form"
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.props.onClose}>

<header className="modal-head">
<h2>
{translateWithParameters('projects_role.turn_x_to_public', this.props.component.name)}
</h2>
</header>

<div className="modal-body">
<p>{translate('projects_role.are_you_sure_to_turn_project_to_public')}</p>
<p className="spacer-top">
{translate('projects_role.are_you_sure_to_turn_project_to_public.2')}
</p>
</div>

<footer className="modal-foot">
<button id="confirm-turn-to-public" onClick={this.handleConfirmClick}>
{translate('projects_role.turn_project_to_public')}
</button>
<a href="#" onClick={this.handleCancelClick}>{translate('cancel')}</a>
</footer>

</Modal>
);
}
}

+ 76
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/VisibilitySelector.js View File

@@ -0,0 +1,76 @@
/*
* 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 classNames from 'classnames';
import { translate } from '../../../../helpers/l10n';

type Props = {
onChange: string => void,
visibility: string
};

export default class VisibilitySelector extends React.PureComponent {
props: Props;

handlePublicClick = (event: Event & { currentTarget: HTMLElement }) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onChange('public');
};

handlePrivateClick = (event: Event & { currentTarget: HTMLElement }) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onChange('private');
};

render() {
return (
<div className="big-spacer-top big-spacer-bottom">
<a
className="link-base-color link-no-underline"
id="visibility-public"
href="#"
onClick={this.handlePublicClick}>
<i
className={classNames('icon-radio', {
'is-checked': this.props.visibility === 'public'
})}
/>
<span className="spacer-left">{translate('visibility.public')}</span>
</a>

<a
className="link-base-color link-no-underline huge-spacer-left"
id="visibility-private"
href="#"
onClick={this.handlePrivateClick}>
<i
className={classNames('icon-radio', {
'is-checked': this.props.visibility === 'private'
})}
/>
<span className="spacer-left">{translate('visibility.private')}</span>
</a>
</div>
);
}
}

+ 0
- 185
server/sonar-web/src/main/js/apps/permissions/project/store/actions.js View File

@@ -1,185 +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.
*/
// @flow
import * as api from '../../../../api/permissions';
import { parseError } from '../../../code/utils';
import {
raiseError,
REQUEST_HOLDERS,
RECEIVE_HOLDERS_SUCCESS,
UPDATE_QUERY,
UPDATE_FILTER,
SELECT_PERMISSION,
GRANT_PERMISSION_TO_USER,
REVOKE_PERMISSION_TO_USER,
GRANT_PERMISSION_TO_GROUP,
REVOKE_PERMISSION_FROM_GROUP
} from '../../shared/store/actions';
import {
getPermissionsAppQuery,
getPermissionsAppFilter,
getPermissionsAppSelectedPermission
} from '../../../../store/rootReducer';

type Dispatch = Object => void;
type GetState = () => Object;

export const loadHolders = (project: string, organization?: string) => (
dispatch: Dispatch,
getState: GetState
) => {
const query = getPermissionsAppQuery(getState());
const filter = getPermissionsAppFilter(getState());
const selectedPermission = getPermissionsAppSelectedPermission(getState());

dispatch({ type: REQUEST_HOLDERS, query });

const requests = [];

if (filter !== 'groups') {
requests.push(
api.getPermissionsUsersForComponent(project, query, selectedPermission, organization)
);
} else {
requests.push(Promise.resolve([]));
}

if (filter !== 'users') {
requests.push(
api.getPermissionsGroupsForComponent(project, query, selectedPermission, organization)
);
} else {
requests.push(Promise.resolve([]));
}

return Promise.all(requests)
.then(responses =>
dispatch({
type: RECEIVE_HOLDERS_SUCCESS,
users: responses[0],
groups: responses[1],
query
})
)
.catch(e => {
return parseError(e).then(message => dispatch(raiseError(message)));
});
};

export const updateQuery = (project: string, query: string, organization?: string) => (
dispatch: Dispatch
) => {
dispatch({ type: UPDATE_QUERY, query });
if (query.length === 0 || query.length > 2) {
dispatch(loadHolders(project, organization));
}
};

export const updateFilter = (project: string, filter: string, organization?: string) => (
dispatch: Dispatch
) => {
dispatch({ type: UPDATE_FILTER, filter });
dispatch(loadHolders(project, organization));
};

export const selectPermission = (project: string, permission: string, organization?: string) => (
dispatch: Dispatch,
getState: GetState
) => {
const selectedPermission = getPermissionsAppSelectedPermission(getState());
if (selectedPermission !== permission) {
dispatch({ type: SELECT_PERMISSION, permission });
} else {
dispatch({ type: SELECT_PERMISSION, permission: null });
}
dispatch(loadHolders(project, organization));
};

export const grantToUser = (
project: string,
login: string,
permission: string,
organization?: string
) => (dispatch: Dispatch) => {
api
.grantPermissionToUser(project, login, permission, organization)
.then(() => {
dispatch({ type: GRANT_PERMISSION_TO_USER, login, permission });
})
.catch(e => {
return parseError(e).then(message => dispatch(raiseError(message)));
});
};

export const revokeFromUser = (
project: string,
login: string,
permission: string,
organization?: string
) => (dispatch: Dispatch) => {
api
.revokePermissionFromUser(project, login, permission, organization)
.then(() => {
dispatch({ type: REVOKE_PERMISSION_TO_USER, login, permission });
})
.catch(e => {
return parseError(e).then(message => dispatch(raiseError(message)));
});
};

export const grantToGroup = (
project: string,
groupName: string,
permission: string,
organization?: string
) => (dispatch: Dispatch) => {
api
.grantPermissionToGroup(project, groupName, permission, organization)
.then(() => {
dispatch({
type: GRANT_PERMISSION_TO_GROUP,
groupName,
permission
});
})
.catch(e => {
return parseError(e).then(message => dispatch(raiseError(message)));
});
};

export const revokeFromGroup = (
project: string,
groupName: string,
permission: string,
organization?: string
) => (dispatch: Dispatch) => {
api
.revokePermissionFromGroup(project, groupName, permission, organization)
.then(() => {
dispatch({
type: REVOKE_PERMISSION_FROM_GROUP,
groupName,
permission
});
})
.catch(e => {
return parseError(e).then(message => dispatch(raiseError(message)));
});
};

+ 1
- 1
server/sonar-web/src/main/js/apps/permissions/routes.js View File

@@ -34,7 +34,7 @@ export const projectPermissionsRoutes = [
{
getIndexRoute(_, callback) {
require.ensure([], require =>
callback(null, { component: require('./project/components/App').default })
callback(null, { component: require('./project/components/AppContainer').default })
);
}
}

+ 1
- 1
server/sonar-web/src/main/js/helpers/request.js View File

@@ -192,7 +192,7 @@ export function postJSON(url: string, data?: Object): Promise<Object> {
* @param url
* @param data
*/
export function post(url: string, data?: Object): Promise<Object> {
export function post(url: string, data?: Object): Promise<void> {
return request(url).setMethod('POST').setData(data).submit().then(checkStatus);
}


+ 40
- 0
server/sonar-web/src/main/less/init/icons.less View File

@@ -228,6 +228,46 @@ a[class^="icon-"], a[class*=" icon-"] {
}


/*
* Radio
*/

.icon-radio {
position: relative;
display: inline-block;
vertical-align: top;
width: 14px;
height: 14px;
margin: 1px;
border: 1px solid #cdcdcd;
border-radius: 12px;
box-sizing: border-box;
transition: border-color 0.3s ease;

&:after {
position: absolute;
top: 2px;
left: 2px;
display: block;
width: 8px;
height: 8px;
border-radius: 8px;
background-color: @darkBlue;
content: "";
opacity: 0;
transition: opacity 0.3s ease;
}
}

a:hover > .icon-radio {
border-color: @blue;
}

.icon-radio.is-checked:after {
opacity: 1;
}


/*
* Common
*/

+ 18
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -484,6 +484,20 @@ sidebar.system=System
sidebar.tools=Tools


#------------------------------------------------------------------------------
#
# VISIBILITY
#
#------------------------------------------------------------------------------

visibility.public=Public
visibility.public.description=This project is public. Anyone can browse and see the source code.
visibility.public.description.short=Anyone can browse and see the source code.
visibility.private=Private
visibility.private.description=This project is private. Only authorized members can browse and see the source code.
visibility.private.description.short=Only authorized members can browse and see the source code.


#------------------------------------------------------------------------------
#
# ADMIN PAGE TITLES and descriptions
@@ -2419,6 +2433,10 @@ projects_role.apply_template=Apply Permission Template
projects_role.apply_template_to_xxx=Apply Permission Template To "{0}"
projects_role.apply_template.success=Permission template was successfully applied.
projects_role.no_projects=There are currently no results to apply the permission template to.
projects_role.turn_x_to_public=Turn "{0}" to Public
projects_role.turn_project_to_public=Turn Project to Public
projects_role.are_you_sure_to_turn_project_to_public=Are you sure you want to turn your project to public?
projects_role.are_you_sure_to_turn_project_to_public.2=Everybody will be able to browse and see the source code of your project.




Loading…
Cancel
Save