diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2017-04-28 17:04:22 +0200 |
---|---|---|
committer | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-05-02 14:45:47 +0200 |
commit | cfa382b33eb58413abcf84635d5e45d1f21c4f21 (patch) | |
tree | 580acac50f46303d34b4605e44111420ed5637ad | |
parent | 9165e7a3a22bb6790a85d9b78ea679ce7559bea4 (diff) | |
download | sonarqube-cfa382b33eb58413abcf84635d5e45d1f21c4f21.tar.gz sonarqube-cfa382b33eb58413abcf84635d5e45d1f21c4f21.zip |
SONAR-9165 Allow to change project visibility on its permissions page
16 files changed, 761 insertions, 332 deletions
diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectPermissionsTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectPermissionsTest.java index c2dac064a8a..8df89782810 100644 --- a/it/it-tests/src/test/java/it/projectAdministration/ProjectPermissionsTest.java +++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectPermissionsTest.java @@ -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) diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java index 668c1180c30..412698a7ea4 100644 --- a/it/it-tests/src/test/java/pageobjects/Navigation.java +++ b/it/it-tests/src/test/java/pageobjects/Navigation.java @@ -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); } diff --git a/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java b/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java new file mode 100644 index 00000000000..11f23543177 --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java @@ -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; + } +} diff --git a/server/sonar-web/src/main/js/api/permissions.js b/server/sonar-web/src/main/js/api/permissions.js index 2487ec4b1b6..50839634a6a 100644 --- a/server/sonar-web/src/main/js/api/permissions.js +++ b/server/sonar-web/src/main/js/api/permissions.js @@ -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); +} diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js index 7277845431b..d1cf872b14f 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.js +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js index 8be316b38bc..8d6489bbfab 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js index c12071e4b67..7579c1d2f4d 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.js new file mode 100644 index 00000000000..36f87cf0320 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js index 2615d72517a..cee4ec29d2c 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.js new file mode 100644 index 00000000000..498487ae3d4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/VisibilitySelector.js b/server/sonar-web/src/main/js/apps/permissions/project/components/VisibilitySelector.js new file mode 100644 index 00000000000..da7b0f447fe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/VisibilitySelector.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js deleted file mode 100644 index b29f3e4d481..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js +++ /dev/null @@ -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))); - }); -}; diff --git a/server/sonar-web/src/main/js/apps/permissions/routes.js b/server/sonar-web/src/main/js/apps/permissions/routes.js index d96aaf4ed2f..71a6d7aba3a 100644 --- a/server/sonar-web/src/main/js/apps/permissions/routes.js +++ b/server/sonar-web/src/main/js/apps/permissions/routes.js @@ -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 }) ); } } diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js index 92fbe6b1b35..8c80e71df44 100644 --- a/server/sonar-web/src/main/js/helpers/request.js +++ b/server/sonar-web/src/main/js/helpers/request.js @@ -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); } diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less index f14284a2ed7..4c933326403 100644 --- a/server/sonar-web/src/main/less/init/icons.less +++ b/server/sonar-web/src/main/less/init/icons.less @@ -229,6 +229,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 */ diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index a4f253456a7..5b5e056dfd3 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -486,6 +486,20 @@ 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. |