Browse Source

SONAR-10067 add "Restore Access" action on projects management page

tags/6.7.1
Stas Vilchik 6 years ago
parent
commit
ae63a6af47

+ 2
- 0
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx View File

@@ -31,6 +31,7 @@ import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';

export interface Props {
currentUser: { login: string };
hasProvisionPermission?: boolean;
onVisibilityChange: (visibility: string) => void;
organization: Organization;
@@ -191,6 +192,7 @@ export default class App extends React.PureComponent<Props, State> {
/>

<Projects
currentUser={this.props.currentUser}
ready={this.state.ready}
projects={this.state.projects}
selection={this.state.selection}

+ 4
- 1
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx View File

@@ -22,7 +22,7 @@ import { connect } from 'react-redux';
import App from './App';
import { Organization } from '../../app/types';
import { onFail } from '../../store/rootActions';
import { getAppState, getOrganizationByKey } from '../../store/rootReducer';
import { getAppState, getOrganizationByKey, getCurrentUser } from '../../store/rootReducer';
import { receiveOrganizations } from '../../store/organizations/duck';
import { changeProjectVisibility } from '../../api/organizations';
import { fetchOrganization } from '../../apps/organizations/actions';
@@ -32,6 +32,7 @@ interface Props {
defaultOrganization: string;
qualifiers: string[];
};
currentUser: { login: string };
fetchOrganization: (organization: string) => void;
onVisibilityChange: (organization: Organization, visibility: string) => void;
onRequestFail: (error: any) => void;
@@ -64,6 +65,7 @@ class AppContainer extends React.PureComponent<Props> {

return (
<App
currentUser={this.props.currentUser}
hasProvisionPermission={organization.canProvisionProjects}
onVisibilityChange={this.handleVisibilityChange}
organization={organization}
@@ -75,6 +77,7 @@ class AppContainer extends React.PureComponent<Props> {

const mapStateToProps = (state: any, ownProps: Props) => ({
appState: getAppState(state),
currentUser: getCurrentUser(state),
organization:
ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
});

+ 8
- 26
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx View File

@@ -19,17 +19,17 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
import ProjectRowActions from './ProjectRowActions';
import { Project } from './utils';
import { Visibility } from '../../app/types';
import PrivateBadge from '../../components/common/PrivateBadge';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/shared/QualifierIcon';
import { translate } from '../../helpers/l10n';
import { getComponentPermissionsUrl } from '../../helpers/urls';
import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';

interface Props {
onApplyTemplateClick: (project: Project) => void;
currentUser: { login: string };
onApplyTemplate: (project: Project) => void;
onProjectCheck: (project: Project, checked: boolean) => void;
project: Project;
selected: boolean;
@@ -40,12 +40,6 @@ export default class ProjectRow extends React.PureComponent<Props> {
this.props.onProjectCheck(this.props.project, checked);
};

handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onApplyTemplateClick(this.props.project);
};

render() {
const { project, selected } = this.props;

@@ -82,23 +76,11 @@ export default class ProjectRow extends React.PureComponent<Props> {
</td>

<td className="thin nowrap">
<div className="dropdown">
<button className="dropdown-toggle" data-toggle="dropdown">
{translate('actions')} <i className="icon-dropdown" />
</button>
<ul className="dropdown-menu dropdown-menu-right">
<li>
<Link to={getComponentPermissionsUrl(project.key)}>
{translate('edit_permissions')}
</Link>
</li>
<li>
<a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}>
{translate('projects_role.apply_template')}
</a>
</li>
</ul>
</div>
<ProjectRowActions
currentUser={this.props.currentUser}
onApplyTemplate={this.props.onApplyTemplate}
project={project}
/>
</td>
</tr>
);

+ 153
- 0
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx View File

@@ -0,0 +1,153 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { Link } from 'react-router';
import RestoreAccessModal from './RestoreAccessModal';
import { Project } from './utils';
import { getComponentShow } from '../../api/components';
import { getComponentNavigation } from '../../api/nav';
import { translate } from '../../helpers/l10n';
import { getComponentPermissionsUrl } from '../../helpers/urls';

export interface Props {
currentUser: { login: string };
onApplyTemplate: (project: Project) => void;
project: Project;
}

interface State {
hasAccess?: boolean;
loading: boolean;
restoreAccessModal: boolean;
}

export default class ProjectRowActions extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: false, restoreAccessModal: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

fetchPermissions = () => {
this.setState({ loading: false });
// call `getComponentNavigation` to check if user has the "Administer" permission
// call `getComponentShow` to check if user has the "Browse" permission
Promise.all([
getComponentNavigation(this.props.project.key),
getComponentShow(this.props.project.key)
]).then(
([navResponse]) => {
if (this.mounted) {
const hasAccess = Boolean(
navResponse.configuration && navResponse.configuration.showPermissions
);
this.setState({ hasAccess, loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ hasAccess: false, loading: false });
}
}
);
};

handleDropdownClick = () => {
if (this.state.hasAccess === undefined && !this.state.loading) {
this.fetchPermissions();
}
};

handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onApplyTemplate(this.props.project);
};

handleRestoreAccessClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ restoreAccessModal: true });
};

handleRestoreAccessClose = () => this.setState({ restoreAccessModal: false });

handleRestoreAccessDone = () => {
this.setState({ hasAccess: true, restoreAccessModal: false });
};

render() {
const { hasAccess, loading } = this.state;

return (
<div className="dropdown">
<button
className="dropdown-toggle"
data-toggle="dropdown"
onClick={this.handleDropdownClick}>
{translate('actions')} <i className="icon-dropdown" />
</button>
{loading ? (
<div className="dropdown-menu dropdown-menu-right">
<i className="spinner spacer-left" />
</div>
) : (
<ul className="dropdown-menu dropdown-menu-right">
{hasAccess === true && (
<li>
<Link to={getComponentPermissionsUrl(this.props.project.key)}>
{translate('edit_permissions')}
</Link>
</li>
)}

{hasAccess === false && (
<li>
<a className="js-restore-access" href="#" onClick={this.handleRestoreAccessClick}>
{translate('global_permissions.restore_access')}
</a>
</li>
)}

<li>
<a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}>
{translate('projects_role.apply_template')}
</a>
</li>
</ul>
)}

{this.state.restoreAccessModal && (
<RestoreAccessModal
currentUser={this.props.currentUser}
onClose={this.handleRestoreAccessClose}
onRestoreAccess={this.handleRestoreAccessDone}
project={this.props.project}
/>
)}
</div>
);
}
}

+ 8
- 5
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx View File

@@ -23,8 +23,10 @@ import ProjectRow from './ProjectRow';
import { Project } from './utils';
import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';

interface Props {
currentUser: { login: string };
onProjectDeselected: (project: string) => void;
onProjectSelected: (project: string) => void;
organization: Organization;
@@ -42,7 +44,7 @@ export default class Projects extends React.PureComponent<Props> {
}
};

onApplyTemplateClick = (project: Project) => {
handleApplyTemplate = (project: Project) => {
new ApplyTemplateView({ project, organization: this.props.organization }).render();
};

@@ -54,18 +56,19 @@ export default class Projects extends React.PureComponent<Props> {
<thead>
<tr>
<th />
<th>Name</th>
<th>{translate('name')}</th>
<th />
<th>Key</th>
<th className="thin nowrap text-right">Last Analysis</th>
<th>{translate('key')}</th>
<th className="thin nowrap text-right">{translate('last_analysis')}</th>
<th />
</tr>
</thead>
<tbody>
{this.props.projects.map(project => (
<ProjectRow
currentUser={this.props.currentUser}
key={project.key}
onApplyTemplateClick={this.onApplyTemplateClick}
onApplyTemplate={this.handleApplyTemplate}
onProjectCheck={this.onProjectCheck}
project={project}
selected={this.props.selection.includes(project.key)}

+ 113
- 0
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx View File

@@ -0,0 +1,113 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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 * as React from 'react';
import Modal from 'react-modal';
import { Project } from './utils';
import { grantPermissionToUser } from '../../api/permissions';
import { translate } from '../../helpers/l10n';
import { FormattedMessage } from 'react-intl';

interface Props {
currentUser: { login: string };
onClose: () => void;
onRestoreAccess: () => void;
project: Project;
}

interface State {
loading: boolean;
}

export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.onClose();
};

handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ loading: true });
Promise.all([this.grantPermission('user'), this.grantPermission('admin')]).then(
this.props.onRestoreAccess,
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

grantPermission = (permission: string) =>
grantPermissionToUser(
this.props.project.key,
this.props.currentUser.login,
permission,
this.props.project.organization
);

render() {
const header = translate('global_permissions.restore_access');

return (
<Modal
isOpen={true}
contentLabel={header}
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<form onSubmit={this.handleFormSubmit}>
<div className="modal-body">
<FormattedMessage
defaultMessage={translate('global_permissions.restore_access.message')}
id="global_permissions.restore_access.message"
values={{
browse: <strong>{translate('projects_role.user')}</strong>,
administer: <strong>{translate('projects_role.admin')}</strong>
}}
/>
</div>

<footer className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<button disabled={this.state.loading}>{translate('restore')}</button>
<a className="js-modal-close" href="#" onClick={this.handleCancelClick}>
{translate('cancel')}
</a>
</footer>
</form>
</Modal>
);
}
}

+ 1
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx View File

@@ -138,6 +138,7 @@ it('changes default project visibility', () => {
function mountRender(props?: { [P in keyof Props]?: Props[P] }) {
return mount(
<App
currentUser={{ login: 'foo' }}
hasProvisionPermission={true}
onVisibilityChange={jest.fn()}
organization={organization}

+ 2
- 9
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx View File

@@ -21,7 +21,6 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectRow from '../ProjectRow';
import { Visibility } from '../../../app/types';
import { click } from '../../../helpers/testUtils';

const project = {
key: 'project',
@@ -44,17 +43,11 @@ it('checks project', () => {
expect(onProjectCheck).toBeCalledWith(project, false);
});

it('applies permission template', () => {
const onApplyTemplateClick = jest.fn();
const wrapper = shallowRender({ onApplyTemplateClick });
click(wrapper.find('.js-apply-template'));
expect(onApplyTemplateClick).toBeCalledWith(project);
});

function shallowRender(props?: any) {
return shallow(
<ProjectRow
onApplyTemplateClick={jest.fn()}
currentUser={{ login: 'foo' }}
onApplyTemplate={jest.fn()}
onProjectCheck={jest.fn()}
project={project}
selected={true}

+ 75
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx View File

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact 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 * as React from 'react';
import { shallow } from 'enzyme';
import ProjectRowActions, { Props } from '../ProjectRowActions';
import { Visibility } from '../../../app/types';
import { click } from '../../../helpers/testUtils';

jest.mock('../../../api/components', () => ({
getComponentShow: jest.fn(() => Promise.reject(undefined))
}));

jest.mock('../../../api/nav', () => ({
getComponentNavigation: jest.fn(() => Promise.resolve())
}));

const project = {
id: '',
key: 'project',
name: 'Project',
organization: 'org',
qualifier: 'TRK',
visibility: Visibility.Private
};

it('restores access', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();

click(wrapper.find('.dropdown-toggle'));
await new Promise(setImmediate);
wrapper.update();
expect(wrapper).toMatchSnapshot();

click(wrapper.find('.js-restore-access'));
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

it('applies permission template', () => {
const onApplyTemplate = jest.fn();
const wrapper = shallowRender({ onApplyTemplate });
click(wrapper.find('.js-apply-template'));
expect(onApplyTemplate).toBeCalledWith(project);
});

function shallowRender(props: Partial<Props> = {}) {
const wrapper = shallow(
<ProjectRowActions
currentUser={{ login: 'admin' }}
onApplyTemplate={jest.fn()}
project={project}
{...props}
/>
);
(wrapper.instance() as ProjectRowActions).mounted = true;
return wrapper;
}

+ 2
- 1
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx View File

@@ -59,13 +59,14 @@ it('opens modal to apply permission template', () => {
wrapper
.find('ProjectRow')
.first()
.prop<Function>('onApplyTemplateClick')(projects[0]);
.prop<Function>('onApplyTemplate')(projects[0]);
expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] });
});

function shallowRender(props?: any) {
return shallow(
<Projects
currentUser={{ login: 'foo' }}
onProjectDeselected={jest.fn()}
onProjectSelected={jest.fn()}
organization={organization}

+ 33
- 86
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap View File

@@ -64,49 +64,22 @@ exports[`renders 1`] = `
<td
className="thin nowrap"
>
<div
className="dropdown"
>
<button
className="dropdown-toggle"
data-toggle="dropdown"
>
actions
<i
className="icon-dropdown"
/>
</button>
<ul
className="dropdown-menu dropdown-menu-right"
>
<li>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project_roles",
"query": Object {
"id": "project",
},
}
}
>
edit_permissions
</Link>
</li>
<li>
<a
className="js-apply-template"
href="#"
onClick={[Function]}
>
projects_role.apply_template
</a>
</li>
</ul>
</div>
<ProjectRowActions
currentUser={
Object {
"login": "foo",
}
}
onApplyTemplate={[Function]}
project={
Object {
"key": "project",
"name": "Project",
"qualifier": "TRK",
"visibility": "private",
}
}
/>
</td>
</tr>
`;
@@ -173,49 +146,23 @@ exports[`renders 2`] = `
<td
className="thin nowrap"
>
<div
className="dropdown"
>
<button
className="dropdown-toggle"
data-toggle="dropdown"
>
actions
<i
className="icon-dropdown"
/>
</button>
<ul
className="dropdown-menu dropdown-menu-right"
>
<li>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project_roles",
"query": Object {
"id": "project",
},
}
}
>
edit_permissions
</Link>
</li>
<li>
<a
className="js-apply-template"
href="#"
onClick={[Function]}
>
projects_role.apply_template
</a>
</li>
</ul>
</div>
<ProjectRowActions
currentUser={
Object {
"login": "foo",
}
}
onApplyTemplate={[Function]}
project={
Object {
"key": "project",
"lastAnalysisDate": "2017-04-08T00:00:00.000Z",
"name": "Project",
"qualifier": "TRK",
"visibility": "private",
}
}
/>
</td>
</tr>
`;

+ 131
- 0
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap View File

@@ -0,0 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`restores access 1`] = `
<div
className="dropdown"
>
<button
className="dropdown-toggle"
data-toggle="dropdown"
onClick={[Function]}
>
actions
<i
className="icon-dropdown"
/>
</button>
<ul
className="dropdown-menu dropdown-menu-right"
>
<li>
<a
className="js-apply-template"
href="#"
onClick={[Function]}
>
projects_role.apply_template
</a>
</li>
</ul>
</div>
`;

exports[`restores access 2`] = `
<div
className="dropdown"
>
<button
className="dropdown-toggle"
data-toggle="dropdown"
onClick={[Function]}
>
actions
<i
className="icon-dropdown"
/>
</button>
<ul
className="dropdown-menu dropdown-menu-right"
>
<li>
<a
className="js-restore-access"
href="#"
onClick={[Function]}
>
global_permissions.restore_access
</a>
</li>
<li>
<a
className="js-apply-template"
href="#"
onClick={[Function]}
>
projects_role.apply_template
</a>
</li>
</ul>
</div>
`;

exports[`restores access 3`] = `
<div
className="dropdown"
>
<button
className="dropdown-toggle"
data-toggle="dropdown"
onClick={[Function]}
>
actions
<i
className="icon-dropdown"
/>
</button>
<ul
className="dropdown-menu dropdown-menu-right"
>
<li>
<a
className="js-restore-access"
href="#"
onClick={[Function]}
>
global_permissions.restore_access
</a>
</li>
<li>
<a
className="js-apply-template"
href="#"
onClick={[Function]}
>
projects_role.apply_template
</a>
</li>
</ul>
<BulkApplyTemplateModal
currentUser={
Object {
"login": "admin",
}
}
onClose={[Function]}
onRestoreAccess={[Function]}
project={
Object {
"id": "",
"key": "project",
"name": "Project",
"organization": "org",
"qualifier": "TRK",
"visibility": "private",
}
}
/>
</div>
`;

+ 15
- 5
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap View File

@@ -9,23 +9,28 @@ exports[`renders list of projects 1`] = `
<tr>
<th />
<th>
Name
name
</th>
<th />
<th>
Key
key
</th>
<th
className="thin nowrap text-right"
>
Last Analysis
last_analysis
</th>
<th />
</tr>
</thead>
<tbody>
<ProjectRow
onApplyTemplateClick={[Function]}
currentUser={
Object {
"login": "foo",
}
}
onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {
@@ -38,7 +43,12 @@ exports[`renders list of projects 1`] = `
selected={true}
/>
<ProjectRow
onApplyTemplateClick={[Function]}
currentUser={
Object {
"login": "foo",
}
}
onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {

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

@@ -76,6 +76,7 @@ issues=Issues
inheritance=Inheritance
key=Key
language=Language
last_analysis=Last Analysis
learn_more=Learn More
library=Library
line_number=Line Number
@@ -1891,6 +1892,8 @@ global_permissions.scan.desc=Ability to get all settings required to perform an
global_permissions.provisioning=Create Projects
global_permissions.provisioning.desc=Ability to initialize a project so its settings can be configured before the first analysis.
global_permissions.filter_by_x_permission=Filter by "{0}" permission
global_permissions.restore_access=Restore Access
global_permissions.restore_access.message=You will receive {browse} and {administer} permissions on the project. Do you want to continue?




Loading…
Cancel
Save