From dda0900b46b558e6cfe0479552852fc5c9456a22 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 5 Dec 2017 17:02:00 +0100 Subject: SONAR-10159 display sub-portfolios before projects --- .../src/main/js/apps/portfolio/components/App.tsx | 17 ++++++++--------- .../js/apps/portfolio/components/__tests__/App-test.tsx | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'server') diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index ec7f35fc0b2..3ca9185e50c 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -26,12 +26,12 @@ import ReliabilityBox from './ReliabilityBox'; import SecurityBox from './SecurityBox'; import MaintainabilityBox from './MaintainabilityBox'; import Activity from './Activity'; +import { SubComponent } from '../types'; +import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../utils'; import { getMeasures } from '../../../api/measures'; import { getChildren } from '../../../api/components'; -import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../utils'; -import { SubComponent } from '../types'; -import '../styles.css'; import { translate } from '../../../helpers/l10n'; +import '../styles.css'; interface Props { component: { key: string; name: string }; @@ -75,7 +75,7 @@ export default class App extends React.PureComponent { this.setState({ loading: true }); Promise.all([ getMeasures(this.props.component.key, PORTFOLIO_METRICS), - getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20 }) + getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20, s: 'qualifier' }) ]).then( ([measures, subComponents]) => { if (this.mounted) { @@ -98,10 +98,9 @@ export default class App extends React.PureComponent { ); } - isEmpty = () => this.state.measures == undefined || this.state.measures['ncloc'] == undefined; + isEmpty = () => !this.state.measures || !this.state.measures['ncloc']; - isNotComputed = () => - this.state.measures && this.state.measures['reliability_rating'] == undefined; + isNotComputed = () => this.state.measures && !this.state.measures['reliability_rating']; renderSpinner() { return ( @@ -151,8 +150,8 @@ export default class App extends React.PureComponent { - {subComponents != undefined && - totalSubComponents != undefined && ( + {subComponents !== undefined && + totalSubComponents !== undefined && ( { 'sqale_rating', 'alert_status' ], - { ps: 20 } + { ps: 20, s: 'qualifier' } ); }); -- cgit v1.2.3 From da80cca3a9fc255e0ebaa1f7ea2ba098725ee7c2 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 5 Dec 2017 17:08:22 +0100 Subject: SONAR-10076 Impossible to create a QP using IE11 --- .../src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx | 2 ++ 1 file changed, 2 insertions(+) (limited to 'server') diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx index ed4dbb325ac..bdc3ac85a88 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx @@ -176,6 +176,8 @@ export default class CreateProfileForm extends React.PureComponent

))} + {/* drop me when we stop supporting ie11 */} + )} -- cgit v1.2.3 From ae63a6af4780af4527dd453af7ed8923ed6bd07f Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 7 Dec 2017 09:57:36 +0100 Subject: SONAR-10067 add "Restore Access" action on projects management page --- .../src/main/js/apps/projectsManagement/App.tsx | 2 + .../js/apps/projectsManagement/AppContainer.tsx | 5 +- .../main/js/apps/projectsManagement/ProjectRow.tsx | 34 ++--- .../apps/projectsManagement/ProjectRowActions.tsx | 153 +++++++++++++++++++++ .../main/js/apps/projectsManagement/Projects.tsx | 13 +- .../apps/projectsManagement/RestoreAccessModal.tsx | 113 +++++++++++++++ .../apps/projectsManagement/__tests__/App-test.tsx | 1 + .../__tests__/ProjectRow-test.tsx | 11 +- .../__tests__/ProjectRowActions-test.tsx | 75 ++++++++++ .../projectsManagement/__tests__/Projects-test.tsx | 3 +- .../__snapshots__/ProjectRow-test.tsx.snap | 119 +++++----------- .../__snapshots__/ProjectRowActions-test.tsx.snap | 131 ++++++++++++++++++ .../__tests__/__snapshots__/Projects-test.tsx.snap | 20 ++- .../main/resources/org/sonar/l10n/core.properties | 3 + 14 files changed, 550 insertions(+), 133 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap (limited to 'server') diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index f92519a6a9e..c714647d45a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -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 { /> void; onVisibilityChange: (organization: Organization, visibility: string) => void; onRequestFail: (error: any) => void; @@ -64,6 +65,7 @@ class AppContainer extends React.PureComponent { return ( { const mapStateToProps = (state: any, ownProps: Props) => ({ appState: getAppState(state), + currentUser: getCurrentUser(state), organization: ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization) }); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index ce7df2a1c43..9b243f7ec6b 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -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 { this.props.onProjectCheck(this.props.project, checked); }; - handleApplyTemplateClick = (event: React.SyntheticEvent) => { - 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 { -
- - -
+ ); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx new file mode 100644 index 00000000000..f05ba7eef69 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -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 { + 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) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onApplyTemplate(this.props.project); + }; + + handleRestoreAccessClick = (event: React.SyntheticEvent) => { + 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 ( +
+ + {loading ? ( +
+ +
+ ) : ( + + )} + + {this.state.restoreAccessModal && ( + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx index 2a2b3c1cb2c..d3399685066 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx @@ -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 { } }; - 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 { - Name + {translate('name')} - Key - Last Analysis + {translate('key')} + {translate('last_analysis')} {this.props.projects.map(project => ( void; + onRestoreAccess: () => void; + project: Project; +} + +interface State { + loading: boolean; +} + +export default class BulkApplyTemplateModal extends React.PureComponent { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleFormSubmit = (event: React.SyntheticEvent) => { + 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 ( + +
+

{header}

+
+ +
+
+ {translate('projects_role.user')}, + administer: {translate('projects_role.admin')} + }} + /> +
+ + + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx index 0fb6b1d21d0..36dde628a47 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -138,6 +138,7 @@ it('changes default project visibility', () => { function mountRender(props?: { [P in keyof Props]?: Props[P] }) { return mount( { 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( ({ + 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 = {}) { + const wrapper = shallow( + + ); + (wrapper.instance() as ProjectRowActions).mounted = true; + return wrapper; +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx index 792e29ccc48..23f93bc8d26 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx @@ -59,13 +59,14 @@ it('opens modal to apply permission template', () => { wrapper .find('ProjectRow') .first() - .prop('onApplyTemplateClick')(projects[0]); + .prop('onApplyTemplate')(projects[0]); expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] }); }); function shallowRender(props?: any) { return shallow( -
- - -
+ `; @@ -173,49 +146,23 @@ exports[`renders 2`] = ` -
- - -
+ `; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap new file mode 100644 index 00000000000..b376ea3f6a6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRowActions-test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`restores access 1`] = ` +
+ + +
+`; + +exports[`restores access 2`] = ` + +`; + +exports[`restores access 3`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap index 2c60880eb28..7bae7a95d3e 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap @@ -9,23 +9,28 @@ exports[`renders list of projects 1`] = ` - Name + name - Key + key - Last Analysis + last_analysis