aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2017-04-28 17:04:22 +0200
committerStas Vilchik <stas-vilchik@users.noreply.github.com>2017-05-02 14:45:47 +0200
commitcfa382b33eb58413abcf84635d5e45d1f21c4f21 (patch)
tree580acac50f46303d34b4605e44111420ed5637ad
parent9165e7a3a22bb6790a85d9b78ea679ce7559bea4 (diff)
downloadsonarqube-cfa382b33eb58413abcf84635d5e45d1f21c4f21.tar.gz
sonarqube-cfa382b33eb58413abcf84635d5e45d1f21c4f21.zip
SONAR-9165 Allow to change project visibility on its permissions page
-rw-r--r--it/it-tests/src/test/java/it/projectAdministration/ProjectPermissionsTest.java25
-rw-r--r--it/it-tests/src/test/java/pageobjects/Navigation.java5
-rw-r--r--it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java54
-rw-r--r--server/sonar-web/src/main/js/api/permissions.js6
-rw-r--r--server/sonar-web/src/main/js/app/components/ProjectContainer.js17
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js143
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/App.js343
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.js33
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js65
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.js79
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/VisibilitySelector.js76
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/store/actions.js185
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/routes.js2
-rw-r--r--server/sonar-web/src/main/js/helpers/request.js2
-rw-r--r--server/sonar-web/src/main/less/init/icons.less40
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties18
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.