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