import { translate } from '../../helpers/l10n';
export interface Props {
+ currentUser: { login: string };
hasProvisionPermission?: boolean;
onVisibilityChange: (visibility: string) => void;
organization: Organization;
/>
<Projects
+ currentUser={this.props.currentUser}
ready={this.state.ready}
projects={this.state.projects}
selection={this.state.selection}
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';
defaultOrganization: string;
qualifiers: string[];
};
+ currentUser: { login: string };
fetchOrganization: (organization: string) => void;
onVisibilityChange: (organization: Organization, visibility: string) => void;
onRequestFail: (error: any) => void;
return (
<App
+ currentUser={this.props.currentUser}
hasProvisionPermission={organization.canProvisionProjects}
onVisibilityChange={this.handleVisibilityChange}
organization={organization}
const mapStateToProps = (state: any, ownProps: Props) => ({
appState: getAppState(state),
+ currentUser: getCurrentUser(state),
organization:
ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
});
*/
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;
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;
</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>
);
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
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;
}
};
- onApplyTemplateClick = (project: Project) => {
+ handleApplyTemplate = (project: Project) => {
new ApplyTemplateView({ project, organization: this.props.organization }).render();
};
<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)}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
function mountRender(props?: { [P in keyof Props]?: Props[P] }) {
return mount(
<App
+ currentUser={{ login: 'foo' }}
hasProvisionPermission={true}
onVisibilityChange={jest.fn()}
organization={organization}
import { shallow } from 'enzyme';
import ProjectRow from '../ProjectRow';
import { Visibility } from '../../../app/types';
-import { click } from '../../../helpers/testUtils';
const project = {
key: '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}
--- /dev/null
+/*
+ * 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;
+}
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}
<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>
`;
<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>
`;
--- /dev/null
+// 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>
+`;
<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 {
selected={true}
/>
<ProjectRow
- onApplyTemplateClick={[Function]}
+ currentUser={
+ Object {
+ "login": "foo",
+ }
+ }
+ onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {
inheritance=Inheritance
key=Key
language=Language
+last_analysis=Last Analysis
learn_more=Learn More
library=Library
line_number=Line Number
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?