From: Stas Vilchik Date: Fri, 1 Sep 2017 14:33:48 +0000 (+0200) Subject: SONAR-9736 fix access to project admin pages X-Git-Tag: 6.6-RC1~380^2~14 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c2954e35714c703c84b0e4c2af4e0249751516b6;p=sonarqube.git SONAR-9736 fix access to project admin pages --- diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index b07f1a89104..2839607c23c 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -24,11 +24,21 @@ export function fetchQualityGatesAppDetails(): Promise { return getJSON('/api/qualitygates/app').catch(throwGlobalError); } -export function fetchQualityGates(): Promise { +export interface QualityGate { + isDefault?: boolean; + id: string; + name: string; +} + +export function fetchQualityGates(): Promise { return getJSON('/api/qualitygates/list').then( r => r.qualitygates.map((qualityGate: any) => { - return { ...qualityGate, isDefault: qualityGate.id === r.default }; + return { + ...qualityGate, + id: String(qualityGate.id), + isDefault: qualityGate.id === r.default + }; }), throwGlobalError ); @@ -74,8 +84,15 @@ export function deleteCondition(id: string): Promise { return post('/api/qualitygates/delete_condition', { id }); } -export function getGateForProject(projectKey: string): Promise { - return getJSON('/api/qualitygates/get_by_project', { projectKey }).then(r => r.qualityGate); +export function getGateForProject(projectKey: string): Promise { + return getJSON('/api/qualitygates/get_by_project', { projectKey }).then( + r => + r.qualityGate && { + id: r.qualityGate.id, + isDefault: r.qualityGate.default, + name: r.qualityGate.name + } + ); } export function associateGateWithProject( diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts index 630effa1831..9ee43f3eabf 100644 --- a/server/sonar-web/src/main/js/api/quality-profiles.ts +++ b/server/sonar-web/src/main/js/api/quality-profiles.ts @@ -27,10 +27,29 @@ import { RequestData } from '../helpers/request'; +export interface Profile { + key: string; + name: string; + language: string; + languageName: string; + isInherited?: boolean; + parentKey?: string; + parentName?: string; + isDefault?: boolean; + activeRuleCount: number; + activeDeprecatedRuleCount: number; + rulesUpdatedAt?: string; + lastUsed?: string; + userUpdatedAt?: string; + organization: string; + isBuiltIn?: boolean; + projectCount?: number; +} + export function searchQualityProfiles(data: { organization?: string; projectKey?: string; -}): Promise { +}): Promise { return getJSON('/api/qualityprofiles/search', data).then(r => r.profiles); } diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js index fed3232e2c2..52292826872 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js +++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js @@ -55,8 +55,7 @@ export default class ProjectAdminContainer extends React.PureComponent { return null; } - return React.cloneElement(this.props.children, { - component: this.props.component - }); + const { children, ...props } = this.props; + return React.cloneElement(children, props); } } diff --git a/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts b/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts new file mode 100644 index 00000000000..3563651a54c --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/addGlobalSuccessMessage.ts @@ -0,0 +1,26 @@ +/* + * 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 getStore from './getStore'; +import * as globalMessages from '../../store/globalMessages/duck'; + +export default function addGlobalSuccessMessage(message: string): void { + const store = getStore(); + store.dispatch(globalMessages.addGlobalSuccessMessage(message)); +} diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index cab8a95008e..53e2290fbaa 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -56,6 +56,8 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; import projectActivityRoutes from '../../apps/projectActivity/routes'; import projectAdminRoutes from '../../apps/project-admin/routes'; import projectBranchesRoutes from '../../apps/projectBranches/routes'; +import projectQualityGateRoutes from '../../apps/projectQualityGate/routes'; +import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes'; import projectsRoutes from '../../apps/projects/routes'; import projectsManagementRoutes from '../../apps/projectsManagement/routes'; import qualityGatesRoutes from '../../apps/quality-gates/routes'; @@ -173,28 +175,31 @@ const startReactApp = () => { import('../components/ComponentContainer').then(i => i.default)}> - - - - - - + + + + + + + + - - - - - {projectAdminRoutes} + + + + - - + {projectAdminRoutes} diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js deleted file mode 100644 index 69b5fdc2126..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js +++ /dev/null @@ -1,122 +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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import Select from 'react-select'; -import { some } from 'lodash'; -import { translate } from '../../../helpers/l10n'; - -export default class Form extends React.PureComponent { - static propTypes = { - allGates: PropTypes.array.isRequired, - gate: PropTypes.object, - onChange: PropTypes.func.isRequired - }; - - state = { - loading: false - }; - - componentWillMount() { - this.handleChange = this.handleChange.bind(this); - this.renderGateName = this.renderGateName.bind(this); - } - - componentDidUpdate(prevProps) { - if (prevProps.gate !== this.props.gate) { - this.stopLoading(); - } - } - - stopLoading() { - this.setState({ loading: false }); - } - - handleChange(option) { - const { gate } = this.props; - - const isSet = gate == null && option.value != null; - const isUnset = gate != null && option.value == null; - const isChanged = gate != null && gate.id !== option.value; - const hasChanged = isSet || isUnset || isChanged; - - if (hasChanged) { - this.setState({ loading: true }); - this.props.onChange(gate && gate.id, option.value); - } - } - - renderGateName(gateOption) { - if (gateOption.isDefault) { - return ( - - - {translate('default')} - - {': '} - {gateOption.label} - - ); - } - - return gateOption.label; - } - - renderSelect() { - const { gate, allGates } = this.props; - - const options = allGates.map(gate => ({ - value: gate.id, - label: gate.name, - isDefault: gate.isDefault - })); - - const hasDefault = some(allGates, gate => gate.isDefault); - if (!hasDefault) { - options.unshift({ - value: null, - label: translate('none') - }); - } - - return ( - - ); - } - - render() { - const { profile } = this.props; - - return ( - - - {profile.languageName} - - - {this.renderProfileSelect()} - - - {this.state.loading && } - - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js deleted file mode 100644 index 02ff3c3eac5..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js +++ /dev/null @@ -1,89 +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 React from 'react'; -import Helmet from 'react-helmet'; -import { connect } from 'react-redux'; -import Header from './Header'; -import Table from './Table'; -import { fetchProjectProfiles, setProjectProfile } from '../store/actions'; -import { - areThereCustomOrganizations, - getProjectAdminAllProfiles, - getProjectAdminProjectProfiles -} from '../../../store/rootReducer'; -import { translate } from '../../../helpers/l10n'; - -/*:: -type Props = { - allProfiles: Array<{}>, - component: { key: string, organization: string }, - customOrganizations: boolean, - fetchProjectProfiles: (componentKey: string, organization?: string) => void, - profiles: Array<{}>, - setProjectProfile: (string, string, string) => void -}; -*/ - -class QualityProfiles extends React.PureComponent { - /*:: props: Props; */ - - componentDidMount() { - if (this.props.customOrganizations) { - this.props.fetchProjectProfiles(this.props.component.key, this.props.component.organization); - } else { - this.props.fetchProjectProfiles(this.props.component.key); - } - } - - handleChangeProfile = (oldKey, newKey) => { - this.props.setProjectProfile(this.props.component.key, oldKey, newKey); - }; - - render() { - const { allProfiles, profiles } = this.props; - - return ( -
- - -
- - {profiles.length > 0 - ? - : } - - ); - } -} - -const mapStateToProps = (state, ownProps) => ({ - customOrganizations: areThereCustomOrganizations(state), - allProfiles: getProjectAdminAllProfiles(state), - profiles: getProjectAdminProjectProfiles(state, ownProps.location.query.id) -}); - -const mapDispatchToProps = { fetchProjectProfiles, setProjectProfile }; - -export default connect(mapStateToProps, mapDispatchToProps)(QualityProfiles); diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js deleted file mode 100644 index 24a099134b2..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js +++ /dev/null @@ -1,73 +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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { groupBy, orderBy } from 'lodash'; -import ProfileRow from './ProfileRow'; -import { translate } from '../../../helpers/l10n'; - -export default class Table extends React.PureComponent { - static propTypes = { - allProfiles: PropTypes.array.isRequired, - profiles: PropTypes.array.isRequired, - onChangeProfile: PropTypes.func.isRequired - }; - - renderHeader() { - // keep one empty cell for the spinner - return ( - - - - - - - - ); - } - - render() { - const profilesByLanguage = groupBy(this.props.allProfiles, 'language'); - const orderedProfiles = orderBy(this.props.profiles, 'languageName'); - - // set key to language to avoid destroying of component - const profileRows = orderedProfiles.map(profile => - - ); - - return ( -
- {translate('language')} - - {translate('quality_profile')} -  
- {this.renderHeader()} - - {profileRows} - -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/project-admin/routes.js b/server/sonar-web/src/main/js/apps/project-admin/routes.js index a7b363c2c7b..f5efaa8e722 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/routes.js +++ b/server/sonar-web/src/main/js/apps/project-admin/routes.js @@ -20,15 +20,11 @@ import React from 'react'; import { Route } from 'react-router'; import Deletion from './deletion/Deletion'; -import QualityProfiles from './quality-profiles/QualityProfiles'; -import QualityGate from './quality-gate/QualityGate'; import Links from './links/Links'; import Key from './key/Key'; export default [ - , - , - , - , - + , + , + ]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js index 6701e8f07db..28f7473e260 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js @@ -17,116 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { - searchQualityProfiles, - associateProject, - dissociateProject -} from '../../../api/quality-profiles'; -import { - fetchQualityGates, - getGateForProject, - associateGateWithProject, - dissociateGateWithProject -} from '../../../api/quality-gates'; import { getProjectLinks, createLink } from '../../../api/projectLinks'; import { getTree, changeKey as changeKeyApi } from '../../../api/components'; -import { addGlobalSuccessMessage } from '../../../store/globalMessages/duck'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { getProjectAdminProfileByKey } from '../../../store/rootReducer'; - -export const RECEIVE_PROFILES = 'projectAdmin/RECEIVE_PROFILES'; -export const receiveProfiles = profiles => ({ - type: RECEIVE_PROFILES, - profiles -}); - -export const RECEIVE_PROJECT_PROFILES = 'projectAdmin/RECEIVE_PROJECT_PROFILES'; -export const receiveProjectProfiles = (projectKey, profiles) => ({ - type: RECEIVE_PROJECT_PROFILES, - projectKey, - profiles -}); - -export const fetchProjectProfiles = (projectKey, organization) => dispatch => { - Promise.all([ - organization ? searchQualityProfiles({ organization }) : searchQualityProfiles(), - organization - ? searchQualityProfiles({ organization, projectKey }) - : searchQualityProfiles({ projectKey }) - ]).then(responses => { - const [allProfiles, projectProfiles] = responses; - dispatch(receiveProfiles(allProfiles)); - dispatch(receiveProjectProfiles(projectKey, projectProfiles)); - }); -}; - -export const SET_PROJECT_PROFILE = 'projectAdmin/SET_PROJECT_PROFILE'; -const setProjectProfileAction = (projectKey, oldProfileKey, newProfileKey) => ({ - type: SET_PROJECT_PROFILE, - projectKey, - oldProfileKey, - newProfileKey -}); - -export const setProjectProfile = (projectKey, oldKey, newKey) => (dispatch, getState) => { - const state = getState(); - const newProfile = getProjectAdminProfileByKey(state, newKey); - const request = newProfile.isDefault - ? dissociateProject(oldKey, projectKey) - : associateProject(newKey, projectKey); - - request.then(() => { - dispatch(setProjectProfileAction(projectKey, oldKey, newKey)); - dispatch( - addGlobalSuccessMessage( - translateWithParameters( - 'project_quality_profile.successfully_updated', - newProfile.languageName - ) - ) - ); - }); -}; - -export const RECEIVE_GATES = 'projectAdmin/RECEIVE_GATES'; -export const receiveGates = gates => ({ - type: RECEIVE_GATES, - gates -}); - -export const RECEIVE_PROJECT_GATE = 'projectAdmin/RECEIVE_PROJECT_GATE'; -export const receiveProjectGate = (projectKey, gate) => ({ - type: RECEIVE_PROJECT_GATE, - projectKey, - gate -}); - -export const fetchProjectGate = projectKey => dispatch => { - Promise.all([fetchQualityGates(), getGateForProject(projectKey)]).then(responses => { - const [allGates, projectGate] = responses; - dispatch(receiveGates(allGates)); - dispatch(receiveProjectGate(projectKey, projectGate)); - }); -}; - -export const SET_PROJECT_GATE = 'projectAdmin/SET_PROJECT_GATE'; -const setProjectGateAction = (projectKey, gateId) => ({ - type: SET_PROJECT_GATE, - projectKey, - gateId -}); - -export const setProjectGate = (projectKey, oldId, newId) => dispatch => { - const request = - newId != null - ? associateGateWithProject(newId, projectKey) - : dissociateGateWithProject(oldId, projectKey); - - request.then(() => { - dispatch(setProjectGateAction(projectKey, newId)); - dispatch(addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated'))); - }); -}; export const RECEIVE_PROJECT_LINKS = 'projectAdmin/RECEIVE_PROJECT_LINKS'; export const receiveProjectLinks = (projectKey, links) => ({ diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js deleted file mode 100644 index 451f531d1ca..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js +++ /dev/null @@ -1,37 +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. - */ -import { RECEIVE_PROJECT_GATE, SET_PROJECT_GATE } from './actions'; - -const gateByProject = (state = {}, action = {}) => { - if (action.type === RECEIVE_PROJECT_GATE) { - const gateId = action.gate ? action.gate.id : null; - return { ...state, [action.projectKey]: gateId }; - } - - if (action.type === SET_PROJECT_GATE) { - return { ...state, [action.projectKey]: action.gateId }; - } - - return state; -}; - -export default gateByProject; - -export const getProjectGate = (state, projectKey) => state[projectKey]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/gates.js b/server/sonar-web/src/main/js/apps/project-admin/store/gates.js deleted file mode 100644 index d68aae11508..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/gates.js +++ /dev/null @@ -1,36 +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. - */ -import { keyBy, values } from 'lodash'; -import { RECEIVE_GATES } from './actions'; - -const gates = (state = {}, action = {}) => { - if (action.type === RECEIVE_GATES) { - const newGatesById = keyBy(action.gates, 'id'); - return { ...state, ...newGatesById }; - } - - return state; -}; - -export default gates; - -export const getAllGates = state => values(state); - -export const getGate = (state, id) => state[id]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js b/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js deleted file mode 100644 index ed6a8345d0e..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js +++ /dev/null @@ -1,36 +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. - */ -import { keyBy, values } from 'lodash'; -import { RECEIVE_PROFILES } from './actions'; - -const profiles = (state = {}, action = {}) => { - if (action.type === RECEIVE_PROFILES) { - const newProfilesByKey = keyBy(action.profiles, 'key'); - return { ...state, ...newProfilesByKey }; - } - - return state; -}; - -export default profiles; - -export const getAllProfiles = state => values(state); - -export const getProfile = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js deleted file mode 100644 index afc7ea0143b..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js +++ /dev/null @@ -1,40 +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. - */ -import { without } from 'lodash'; -import { RECEIVE_PROJECT_PROFILES, SET_PROJECT_PROFILE } from './actions'; - -const profilesByProject = (state = {}, action = {}) => { - if (action.type === RECEIVE_PROJECT_PROFILES) { - const profileKeys = action.profiles.map(profile => profile.key); - return { ...state, [action.projectKey]: profileKeys }; - } - - if (action.type === SET_PROJECT_PROFILE) { - const profileKeys = state[action.projectKey]; - const nextProfileKeys = [...without(profileKeys, action.oldProfileKey), action.newProfileKey]; - return { ...state, [action.projectKey]: nextProfileKeys }; - } - - return state; -}; - -export default profilesByProject; - -export const getProfiles = (state, projectKey) => state[projectKey] || []; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js index 4eb6dec8e0e..43ab5760cfd 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js @@ -18,10 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { combineReducers } from 'redux'; -import profiles, { getProfile, getAllProfiles as nextGetAllProfiles } from './profiles'; -import profilesByProject, { getProfiles } from './profilesByProject'; -import gates, { getAllGates as nextGetAllGates, getGate } from './gates'; -import gateByProject, { getProjectGate as nextGetProjectGate } from './gateByProject'; import links, { getLink } from './links'; import linksByProject, { getLinks } from './linksByProject'; import components, { getComponentByKey as nextGetComponentByKey } from './components'; @@ -31,10 +27,6 @@ import globalMessages, { } from '../../../store/globalMessages/duck'; const rootReducer = combineReducers({ - profiles, - profilesByProject, - gates, - gateByProject, links, linksByProject, components, @@ -44,22 +36,6 @@ const rootReducer = combineReducers({ export default rootReducer; -export const getProfileByKey = (state, profileKey) => getProfile(state.profiles, profileKey); - -export const getAllProfiles = state => nextGetAllProfiles(state.profiles); - -export const getProjectProfiles = (state, projectKey) => - getProfiles(state.profilesByProject, projectKey).map(profileKey => - getProfileByKey(state, profileKey) - ); - -export const getGateById = (state, gateId) => getGate(state.gates, gateId); - -export const getAllGates = state => nextGetAllGates(state.gates); - -export const getProjectGate = (state, projectKey) => - getGateById(state, nextGetProjectGate(state.gateByProject, projectKey)); - export const getLinkById = (state, linkId) => getLink(state.links, linkId); export const getProjectLinks = (state, projectKey) => diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx new file mode 100644 index 00000000000..51d42a94526 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx @@ -0,0 +1,128 @@ +/* + * 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 * as React from 'react'; +import Helmet from 'react-helmet'; +import Header from './Header'; +import Form from './Form'; +import { + fetchQualityGates, + getGateForProject, + associateGateWithProject, + dissociateGateWithProject, + QualityGate +} from '../../api/quality-gates'; +import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; +import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; +import { Component } from '../../app/types'; +import { translate } from '../../helpers/l10n'; + +interface Props { + component: Component; +} + +interface State { + allGates?: QualityGate[]; + gate?: QualityGate; + loading: boolean; +} + +export default class App extends React.PureComponent { + mounted: boolean; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + if (this.checkPermissions()) { + this.fetchQualityGates(); + } else { + handleRequiredAuthorization(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + checkPermissions() { + const { configuration } = this.props.component; + const hasPermission = configuration && configuration.showQualityGates; + return !!hasPermission; + } + + fetchQualityGates() { + this.setState({ loading: true }); + Promise.all([fetchQualityGates(), getGateForProject(this.props.component.key)]).then( + ([allGates, gate]) => { + if (this.mounted) { + this.setState({ allGates, gate, loading: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + + handleChangeGate = (oldId: string | undefined, newId: string | undefined) => { + const { allGates } = this.state; + + if ((!oldId && !newId) || !allGates) { + return Promise.resolve(); + } + + const request = newId + ? associateGateWithProject(newId, this.props.component.key) + : dissociateGateWithProject(oldId!, this.props.component.key); + + return request.then(() => { + if (this.mounted) { + addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated')); + if (newId) { + const newGate = allGates.find(gate => gate.id === newId); + if (newGate) { + this.setState({ gate: newGate }); + } + } else { + this.setState({ gate: undefined }); + } + } + }); + }; + + render() { + if (!this.checkPermissions()) { + return null; + } + + const { allGates, gate, loading } = this.state; + + return ( +
+ +
+ {loading + ? + : allGates &&
} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx new file mode 100644 index 00000000000..2e0e502db14 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx @@ -0,0 +1,131 @@ +/* + * 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 * as React from 'react'; +import * as Select from 'react-select'; +import { some } from 'lodash'; +import { QualityGate } from '../../api/quality-gates'; +import { translate } from '../../helpers/l10n'; + +interface Props { + allGates: QualityGate[]; + gate?: QualityGate; + onChange: (oldGate: string | undefined, newGate: string) => Promise; +} + +interface State { + loading: boolean; +} + +interface Option { + isDefault?: boolean; + label: string; + value: string; +} + +export default class Form extends React.PureComponent { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + handleChange = (option: { value: string }) => { + const { gate } = this.props; + + const isSet = gate == null && option.value != null; + const isUnset = gate != null && option.value == null; + const isChanged = gate != null && gate.id !== option.value; + const hasChanged = isSet || isUnset || isChanged; + + if (hasChanged) { + this.setState({ loading: true }); + this.props.onChange(gate && gate.id, option.value).then(this.stopLoading, this.stopLoading); + } + }; + + renderGateName = (option: { isDefault?: boolean; label: string }) => { + if (option.isDefault) { + return ( + + + {translate('default')} + + {': '} + {option.label} + + ); + } + + return ( + + {option.label} + + ); + }; + + renderSelect() { + const { gate, allGates } = this.props; + + const options: Option[] = allGates.map(gate => ({ + value: gate.id, + label: gate.name, + isDefault: gate.isDefault + })); + + const hasDefault = some(allGates, gate => gate.isDefault); + if (!hasDefault) { + options.unshift({ value: '', label: translate('none') }); + } + + return ( + +
+`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap new file mode 100644 index 00000000000..eaade8b5468 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
+

+ project_quality_gate.page +

+
+ project_quality_gate.page.description +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts new file mode 100644 index 00000000000..e342e0f4070 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts @@ -0,0 +1,30 @@ +/* + * 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 { RouterState, IndexRouteProps } from 'react-router'; + +const routes = [ + { + getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { + import('./App').then(i => callback(null, { component: i.default })); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx new file mode 100644 index 00000000000..ccc85320a62 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx @@ -0,0 +1,139 @@ +/* + * 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 * as React from 'react'; +import Helmet from 'react-helmet'; +import Header from './Header'; +import Table from './Table'; +import { + associateProject, + dissociateProject, + searchQualityProfiles, + Profile +} from '../../api/quality-profiles'; +import { Component } from '../../app/types'; +import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; +import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; +import { translate, translateWithParameters } from '../../helpers/l10n'; + +interface Props { + component: Component; + customOrganizations: boolean; +} + +interface State { + allProfiles?: Profile[]; + loading: boolean; + profiles?: Profile[]; +} + +export default class QualityProfiles extends React.PureComponent { + mounted: boolean; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + if (this.checkPermissions()) { + this.fetchProfiles(); + } else { + handleRequiredAuthorization(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + checkPermissions() { + const { configuration } = this.props.component; + const hasPermission = configuration && configuration.showQualityProfiles; + return !!hasPermission; + } + + fetchProfiles() { + const { component } = this.props; + const organization = this.props.customOrganizations ? component.organization : undefined; + Promise.all([ + searchQualityProfiles({ organization }), + searchQualityProfiles({ organization, projectKey: component.key }) + ]).then( + ([allProfiles, profiles]) => { + if (this.mounted) { + this.setState({ loading: false, allProfiles, profiles }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + + handleChangeProfile = (oldKey: string, newKey: string) => { + const { component } = this.props; + const { allProfiles, profiles } = this.state; + const newProfile = allProfiles && allProfiles.find(profile => profile.key === newKey); + const request = + newProfile && newProfile.isDefault + ? dissociateProject(oldKey, component.key) + : associateProject(newKey, component.key); + + return request.then(() => { + if (this.mounted && profiles && newProfile) { + // remove old profile, add new one + const nextProfiles = [...profiles.filter(profile => profile.key !== oldKey), newProfile]; + this.setState({ profiles: nextProfiles }); + + addGlobalSuccessMessage( + translateWithParameters( + 'project_quality_profile.successfully_updated', + newProfile.languageName + ) + ); + } + }); + }; + + render() { + if (!this.checkPermissions()) { + return null; + } + + const { allProfiles, loading, profiles } = this.state; + + return ( +
+ + +
+ + {loading + ? + : allProfiles && + profiles && + } + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx new file mode 100644 index 00000000000..a758189099d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx @@ -0,0 +1,34 @@ +/* + * 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 * as React from 'react'; +import { translate } from '../../helpers/l10n'; + +export default function Header() { + return ( +
+

+ {translate('project_quality_profiles.page')} +

+
+ {translate('project_quality_profiles.page.description')} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx new file mode 100644 index 00000000000..0679b4463b4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx @@ -0,0 +1,122 @@ +/* + * 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 * as React from 'react'; +import * as Select from 'react-select'; +import { Profile } from '../../api/quality-profiles'; +import { translate } from '../../helpers/l10n'; + +interface Props { + onChangeProfile: (oldProfile: string, newProfile: string) => Promise; + possibleProfiles: Profile[]; + profile: Profile; +} + +interface State { + loading: boolean; +} + +export default class ProfileRow extends React.PureComponent { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + handleChange = (option: { value: string }) => { + if (this.props.profile.key !== option.value) { + this.setState({ loading: true }); + this.props + .onChangeProfile(this.props.profile.key, option.value) + .then(this.stopLoading, this.stopLoading); + } + }; + + renderProfileName = (profileOption: { isDefault: boolean; label: string }) => { + if (profileOption.isDefault) { + return ( + + + {translate('default')} + + {': '} + {profileOption.label} + + ); + } + + return ( + + {profileOption.label} + + ); + }; + + renderProfileSelect() { + const { profile, possibleProfiles } = this.props; + + const options = possibleProfiles.map(profile => ({ + value: profile.key, + label: profile.name, + isDefault: profile.isDefault + })); + + return ( + + + + + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx new file mode 100644 index 00000000000..43fca05f7ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx @@ -0,0 +1,65 @@ +/* + * 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 * as React from 'react'; +import { groupBy, orderBy } from 'lodash'; +import ProfileRow from './ProfileRow'; +import { Profile } from '../../api/quality-profiles'; +import { translate } from '../../helpers/l10n'; + +interface Props { + allProfiles: Profile[]; + profiles: Profile[]; + onChangeProfile: (oldProfile: string, newProfile: string) => Promise; +} + +export default function Table(props: Props) { + const profilesByLanguage = groupBy(props.allProfiles, 'language'); + const orderedProfiles = orderBy(props.profiles, 'languageName'); + + // set key to language to avoid destroying of component + const profileRows = orderedProfiles.map(profile => + + ); + + return ( +
+ {profile.languageName} + + {this.renderProfileSelect()} + + {this.state.loading && } +
+ + + + + {/* keep one empty cell for the spinner */} + + + + + {profileRows} + +
+ {translate('language')} + + {translate('quality_profile')} +  
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx new file mode 100644 index 00000000000..ab1f2ec7071 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx @@ -0,0 +1,122 @@ +/* + * 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. + */ +jest.mock('../../../api/quality-profiles', () => ({ + associateProject: jest.fn(() => Promise.resolve()), + dissociateProject: jest.fn(() => Promise.resolve()), + searchQualityProfiles: jest.fn() +})); + +jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ + default: jest.fn() +})); + +jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ + default: jest.fn() +})); + +import * as React from 'react'; +import { mount } from 'enzyme'; +import App from '../App'; + +const associateProject = require('../../../api/quality-profiles').associateProject as jest.Mock< + any +>; + +const dissociateProject = require('../../../api/quality-profiles') + .dissociateProject as jest.Mock; + +const searchQualityProfiles = require('../../../api/quality-profiles') + .searchQualityProfiles as jest.Mock; + +const addGlobalSuccessMessage = require('../../../app/utils/addGlobalSuccessMessage') + .default as jest.Mock; + +const handleRequiredAuthorization = require('../../../app/utils/handleRequiredAuthorization') + .default as jest.Mock; + +const component = { + analysisDate: '', + breadcrumbs: [], + configuration: { showQualityProfiles: true }, + key: 'foo', + name: 'foo', + organization: 'org', + qualifier: 'TRK', + version: '0.0.1' +}; + +it('checks permissions', () => { + handleRequiredAuthorization.mockClear(); + mount(); + expect(handleRequiredAuthorization).toBeCalled(); +}); + +it('fetches profiles', () => { + searchQualityProfiles.mockClear(); + mount(); + expect(searchQualityProfiles.mock.calls).toHaveLength(2); + expect(searchQualityProfiles).toBeCalledWith({ organization: undefined }); + expect(searchQualityProfiles).toBeCalledWith({ organization: undefined, projectKey: 'foo' }); +}); + +it('fetches profiles with organization', () => { + searchQualityProfiles.mockClear(); + mount(); + expect(searchQualityProfiles.mock.calls).toHaveLength(2); + expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' }); + expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', projectKey: 'foo' }); +}); + +it('changes profile', () => { + associateProject.mockClear(); + dissociateProject.mockClear(); + addGlobalSuccessMessage.mockClear(); + const wrapper = mount(); + + const fooJava = randomProfile('foo-java', 'java'); + const fooJs = randomProfile('foo-js', 'js'); + const allProfiles = [ + fooJava, + randomProfile('bar-java', 'java'), + randomProfile('baz-java', 'java', true), + fooJs + ]; + const profiles = [fooJava, fooJs]; + wrapper.setState({ allProfiles, loading: false, profiles }); + + wrapper.find('Table').prop('onChangeProfile')('foo-java', 'bar-java'); + expect(associateProject).toBeCalledWith('bar-java', 'foo'); + + wrapper.find('Table').prop('onChangeProfile')('foo-java', 'baz-java'); + expect(dissociateProject).toBeCalledWith('foo-java', 'foo'); +}); + +function randomProfile(key: string, language: string, isDefault = false) { + return { + activeRuleCount: 17, + activeDeprecatedRuleCount: 0, + isDefault, + key, + name: key, + language: language, + languageName: language, + organization: 'org' + }; +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx new file mode 100644 index 00000000000..adeee211e56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx @@ -0,0 +1,26 @@ +/* + * 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 { shallow } from 'enzyme'; +import Header from '../Header'; + +it('renders', () => { + expect(shallow(
)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx new file mode 100644 index 00000000000..cec351e32db --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx @@ -0,0 +1,65 @@ +/* + * 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 { shallow } from 'enzyme'; +import ProfileRow from '../ProfileRow'; +import { doAsync } from '../../../helpers/testUtils'; + +it('renders', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('changes profile', () => { + const onChangeProfile = jest.fn(() => Promise.resolve()); + const wrapper = shallow( + + ); + (wrapper.instance() as ProfileRow).mounted = true; + wrapper.find('Select').prop('onChange')({ value: 'baz' }); + expect(onChangeProfile).toBeCalledWith('foo', 'baz'); + expect(wrapper.state().loading).toBeTruthy(); + return doAsync().then(() => { + expect(wrapper.state().loading).toBeFalsy(); + }); +}); + +function randomProfile(key: string) { + return { + activeRuleCount: 17, + activeDeprecatedRuleCount: 0, + key, + name: key, + language: 'xoo', + languageName: 'xoo', + organization: 'org' + }; +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx new file mode 100644 index 00000000000..84e70359fab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { shallow } from 'enzyme'; +import Table from '../Table'; + +it('renders', () => { + const fooJava = randomProfile('foo-java', 'java'); + const fooJs = randomProfile('foo-js', 'js'); + const allProfiles = [ + fooJava, + randomProfile('bar-java', 'java'), + randomProfile('baz-java', 'java'), + fooJs + ]; + const profiles = [fooJava, fooJs]; + expect( + shallow() + ).toMatchSnapshot(); +}); + +function randomProfile(key: string, language: string) { + return { + activeRuleCount: 17, + activeDeprecatedRuleCount: 0, + key, + name: key, + language: language, + languageName: language, + organization: 'org' + }; +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap new file mode 100644 index 00000000000..4f0e35f4a2f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
+

+ project_quality_profiles.page +

+
+ project_quality_profiles.page.description +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap new file mode 100644 index 00000000000..541a117c889 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + + + + +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap new file mode 100644 index 00000000000..b4b991157eb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
+ xoo + + +
+ + + + + + + + + + + +
+ language + + quality_profile + +   +
+`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts new file mode 100644 index 00000000000..e342e0f4070 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts @@ -0,0 +1,30 @@ +/* + * 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 { RouterState, IndexRouteProps } from 'react-router'; + +const routes = [ + { + getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { + import('./App').then(i => callback(null, { component: i.default })); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx index 91e5ed96cfc..df6462d30b0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx @@ -25,18 +25,19 @@ import * as apiRules from '../../../../api/rules'; import * as apiQP from '../../../../api/quality-profiles'; const PROFILE = { - key: 'foo', - name: 'Foo', + activeRuleCount: 68, + activeDeprecatedRuleCount: 0, + childrenCount: 0, + depth: 0, isBuiltIn: false, isDefault: false, isInherited: false, + key: 'foo', language: 'java', languageName: 'Java', - activeRuleCount: 68, - activeDeprecatedRuleCount: 0, - rulesUpdatedAt: '2017-06-28T12:58:44+0000', - depth: 0, - childrenCount: 0 + name: 'Foo', + organization: 'org', + rulesUpdatedAt: '2017-06-28T12:58:44+0000' }; const apiResponseAll = { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx index 5252d1c1840..957fa4790a2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx @@ -60,16 +60,17 @@ export default function EvolutionStagnant(props: Props) { {profile.name}
- - {formattedDate => -
- {translateWithParameters( - 'quality_profiles.x_updated_on_y', - profile.languageName, - formattedDate - )} -
} -
+ {profile.rulesUpdatedAt && + + {formattedDate => +
+ {translateWithParameters( + 'quality_profiles.x_updated_on_y', + profile.languageName, + formattedDate + )} +
} +
} )} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/types.ts b/server/sonar-web/src/main/js/apps/quality-profiles/types.ts index 415ab3c03bb..88758d46591 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/types.ts +++ b/server/sonar-web/src/main/js/apps/quality-profiles/types.ts @@ -17,22 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export interface Profile { - key: string; - name: string; - isBuiltIn: boolean; - isDefault: boolean; - isInherited: boolean; - language: string; - languageName: string; - activeRuleCount: number; - activeDeprecatedRuleCount: number; - projectCount?: number; - parentKey?: string; - parentName?: string; - userUpdatedAt?: string; - lastUsed?: string; - rulesUpdatedAt: string; +import { Profile as BaseProfile } from '../../api/quality-profiles'; + +export interface Profile extends BaseProfile { depth: number; childrenCount: number; } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts index 6d5c9db9d9f..a23ab59a6c6 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts @@ -18,20 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { sortBy } from 'lodash'; +import { Profile as BaseProfile } from '../../api/quality-profiles'; import { differenceInYears, isValidDate, parseDate } from '../../helpers/dates'; import { Profile } from './types'; -export function sortProfiles(profiles: Profile[]) { +export function sortProfiles(profiles: BaseProfile[]): Profile[] { const result: Profile[] = []; const sorted = sortBy(profiles, 'name'); - function retrieveChildren(parent: Profile | null) { + function retrieveChildren(parent: BaseProfile | null) { return sorted.filter( p => (parent == null && p.parentKey == null) || (parent != null && p.parentKey === parent.key) ); } - function putProfile(profile: Profile | null = null, depth: number = 1) { + function putProfile(profile: BaseProfile | null = null, depth: number = 1) { const children = retrieveChildren(profile); if (profile != null) { diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 9cdd198e860..ef3e95d1931 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -189,24 +189,6 @@ export const getSettingsAppEncryptionState = state => export const getSettingsAppGlobalMessages = state => fromSettingsApp.getGlobalMessages(state.settingsApp); -export const getProjectAdminProfileByKey = (state, profileKey) => - fromProjectAdminApp.getProfileByKey(state.projectAdminApp, profileKey); - -export const getProjectAdminAllProfiles = state => - fromProjectAdminApp.getAllProfiles(state.projectAdminApp); - -export const getProjectAdminProjectProfiles = (state, projectKey) => - fromProjectAdminApp.getProjectProfiles(state.projectAdminApp, projectKey); - -export const getProjectAdminGateById = (state, gateId) => - fromProjectAdminApp.getGateById(state.projectAdminApp, gateId); - -export const getProjectAdminAllGates = state => - fromProjectAdminApp.getAllGates(state.projectAdminApp); - -export const getProjectAdminProjectGate = (state, projectKey) => - fromProjectAdminApp.getProjectGate(state.projectAdminApp, projectKey); - export const getProjectAdminLinkById = (state, linkId) => fromProjectAdminApp.getLinkById(state.projectAdminApp, linkId);