From 2907c50b0aaf6b323ab70b91a3aa4436ef323006 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Fri, 24 Nov 2017 11:41:05 +0100 Subject: [PATCH] SONAR-10088 SONAR-10114 Allow/prevent QG actions based on list of authorized actions --- .../src/main/js/api/quality-gates.ts | 46 +++++++++-------- .../main/js/apps/projectQualityGate/App.tsx | 2 +- .../projectQualityGate/__tests__/App-test.tsx | 4 +- .../components/AddConditionForm.js | 14 +++--- .../quality-gates/components/Conditions.js | 17 +++---- .../apps/quality-gates/components/Details.js | 49 +++++++++---------- .../components/DetailsContent.js | 13 +++-- .../quality-gates/components/DetailsHeader.js | 19 ++++--- .../components/QualityGatesApp.js | 25 ++++------ .../containers/DetailsContainer.js | 11 +++-- 10 files changed, 104 insertions(+), 96 deletions(-) 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 325f181c7bb..b7eb009e7f1 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -20,32 +20,37 @@ import { getJSON, post, postJSON, RequestData } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -export function fetchQualityGatesAppDetails(): Promise { - return getJSON('/api/qualitygates/app').catch(throwGlobalError); +interface Condition { + error?: string; + id: number; + metric: string; + op: string; + period?: number; + warning?: string; } export interface QualityGate { + actions?: { + associateProjects: boolean; + copy: boolean; + edit: boolean; + setAsDefault: boolean; + }; + conditions?: Condition[]; + id: number; isBuiltIn?: boolean; isDefault?: boolean; - id: number; name: string; } -export function fetchQualityGates(): Promise { - return getJSON('/api/qualitygates/list').then( - r => - r.qualitygates.map((qualityGate: any) => { - return { - ...qualityGate, - id: qualityGate.id, - isDefault: qualityGate.id === r.default - }; - }), - throwGlobalError - ); +export function fetchQualityGates(): Promise<{ + actions: { create: boolean }; + qualitygates: QualityGate[]; +}> { + return getJSON('/api/qualitygates/list').catch(throwGlobalError); } -export function fetchQualityGate(id: string): Promise { +export function fetchQualityGate(id: string): Promise { return getJSON('/api/qualitygates/show', { id }).catch(throwGlobalError); } @@ -87,11 +92,10 @@ export function deleteCondition(id: string): Promise { export function getGateForProject(project: string): Promise { return getJSON('/api/qualitygates/get_by_project', { project }).then( - r => - r.qualityGate && { - id: r.qualityGate.id, - isDefault: r.qualityGate.default, - name: r.qualityGate.name + ({ qualityGate }) => + qualityGate && { + ...qualityGate, + isDefault: qualityGate.default } ); } diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx index 0a4f20f304b..95b899a9aa3 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx @@ -69,7 +69,7 @@ export default class App extends React.PureComponent { fetchQualityGates() { this.setState({ loading: true }); Promise.all([fetchQualityGates(), getGateForProject(this.props.component.key)]).then( - ([allGates, gate]) => { + ([{ qualitygates: allGates }, gate]) => { if (this.mounted) { this.setState({ allGates, gate, loading: false }); } diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx index 4fa2bc575af..d04a0ee2f6e 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx @@ -21,8 +21,8 @@ jest.mock('../../../api/quality-gates', () => ({ associateGateWithProject: jest.fn(() => Promise.resolve()), dissociateGateWithProject: jest.fn(() => Promise.resolve()), - fetchQualityGates: jest.fn(), - getGateForProject: jest.fn() + fetchQualityGates: jest.fn(() => Promise.resolve({})), + getGateForProject: jest.fn(() => Promise.resolve()) })); jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js index 866af392525..471d9d0f132 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { sortBy } from 'lodash'; +import { omitBy, map, sortBy } from 'lodash'; import Select from '../../../components/controls/Select'; import { translate, getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n'; @@ -30,15 +30,15 @@ export default function AddConditionForm({ metrics, onSelect }) { onSelect(metric); } - const metricsToDisplay = metrics.filter(metric => !metric.hidden); - const sortedMetrics = sortBy(metricsToDisplay, 'domain'); - const options = sortedMetrics.map(metric => { - return { + const metricsToDisplay = omitBy(metrics, metric => metric.hidden); + const options = sortBy( + map(metricsToDisplay, metric => ({ value: metric.key, label: getLocalizedMetricName(metric), domain: metric.domain - }; - }); + })), + 'domain' + ); // use "disabled" property to emulate optgroups const optionsWithDomains = []; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js index f46cba13af4..7986f5a1816 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js @@ -65,13 +65,11 @@ export default class Conditions extends React.PureComponent { onDeleteCondition } = this.props; - const existingConditions = conditions.filter(condition => - metrics.find(metric => metric.key === condition.metric) - ); + const existingConditions = conditions.filter(condition => metrics[condition.metric]); const sortedConditions = sortBy( existingConditions, - condition => metrics.find(metric => metric.key === condition.metric).name + condition => metrics[condition.metric] && metrics[condition.metric].name ); const duplicates = []; @@ -85,11 +83,10 @@ export default class Conditions extends React.PureComponent { } }); - const uniqDuplicates = uniqBy(duplicates, d => d.metric).map(condition => { - const metric = metrics.find(metric => metric.key === condition.metric); - return { ...condition, metric }; - }); - + const uniqDuplicates = uniqBy(duplicates, d => d.metric).map(condition => ({ + ...condition, + metric: metrics[condition.metric] + })); return (

{translate('quality_gates.conditions')}

@@ -127,7 +124,7 @@ export default class Conditions extends React.PureComponent { key={getKey(condition, index)} qualityGate={qualityGate} condition={condition} - metric={metrics.find(metric => metric.key === condition.metric)} + metric={metrics[condition.metric]} edit={edit} onSaveCondition={onSaveCondition} onDeleteCondition={onDeleteCondition} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js index 751ae51ede3..97249bf6532 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js @@ -33,7 +33,12 @@ import DeleteView from '../views/delete-view'; import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls'; export default class Details extends React.PureComponent { + static contextTypes = { + router: PropTypes.object.isRequired + }; + componentDidMount() { + this.props.fetchMetrics(); this.fetchDetails(); } @@ -43,26 +48,25 @@ export default class Details extends React.PureComponent { } } - fetchDetails() { - const { id } = this.props.params; - fetchQualityGate(id).then(qualityGate => this.props.onShow(qualityGate)); - } + fetchDetails = () => + fetchQualityGate(this.props.params.id).then( + qualityGate => this.props.onShow(qualityGate), + () => {} + ); - handleRenameClick() { + handleRenameClick = () => { const { qualityGate, onRename } = this.props; - new RenameView({ qualityGate, onRename: (qualityGate, newName) => { onRename(qualityGate, newName); } }).render(); - } + }; - handleCopyClick() { + handleCopyClick = () => { const { qualityGate, onCopy, organization } = this.props; const { router } = this.context; - new CopyView({ qualityGate, onCopy: newQualityGate => { @@ -70,19 +74,18 @@ export default class Details extends React.PureComponent { router.push(getQualityGateUrl(newQualityGate.id, organization && organization.key)); } }).render(); - } + }; - handleSetAsDefaultClick() { + handleSetAsDefaultClick = () => { const { qualityGate, onSetAsDefault, onUnsetAsDefault } = this.props; - if (qualityGate.isDefault) { unsetQualityGateAsDefault(qualityGate.id).then(() => onUnsetAsDefault(qualityGate)); } else { setQualityGateAsDefault(qualityGate.id).then(() => onSetAsDefault(qualityGate)); } - } + }; - handleDeleteClick() { + handleDeleteClick = () => { const { qualityGate, onDelete, organization } = this.props; const { router } = this.context; new DeleteView({ @@ -92,10 +95,10 @@ export default class Details extends React.PureComponent { router.replace(getQualityGatesUrl(organization && organization.key)); } }).render(); - } + }; render() { - const { qualityGate, edit, metrics } = this.props; + const { qualityGate, metrics } = this.props; const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props; if (!qualityGate) { @@ -107,17 +110,15 @@ export default class Details extends React.PureComponent {

{translate('quality_gates.projects')}

- {gate.isDefault ? defaultMessage : } + {gate.isDefault ? ( + defaultMessage + ) : ( + + )}
); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js index db4c2bb5fcd..dc878e83e39 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js @@ -43,8 +43,8 @@ export default class DetailsHeader extends React.PureComponent { }; render() { - const { qualityGate, edit } = this.props; - + const { qualityGate } = this.props; + const actions = qualityGate.actions || {}; return (
@@ -53,17 +53,22 @@ export default class DetailsHeader extends React.PureComponent { {qualityGate.name} {qualityGate.isBuiltIn && } - {edit && ( -
+ +
+ {actions.edit && ( + )} + {actions.copy && ( + )} + {actions.setAsDefault && ( -
- )} + )} +
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js index 018715db8a5..a8b0b97d490 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js @@ -23,10 +23,7 @@ import Helmet from 'react-helmet'; import ListHeader from './ListHeader'; import List from './List'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import { - fetchQualityGatesAppDetails, - fetchQualityGates as fetchQualityGatesAPI -} from '../../../api/quality-gates'; +import { fetchQualityGates } from '../../../api/quality-gates'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; import '../styles.css'; @@ -53,31 +50,27 @@ export default class QualityGatesApp extends Component { } } - fetchQualityGates() { - Promise.all([ - fetchQualityGatesAppDetails(), - fetchQualityGatesAPI() - ]).then(([details, qualityGates]) => { + fetchQualityGates = () => + fetchQualityGates().then(({ actions, qualitygates: qualityGates }) => { const { organization, updateStore } = this.props; - updateStore({ ...details, qualityGates }); - if (qualityGates && qualityGates.length === 1 && !details.edit) { + updateStore({ actions, qualityGates }); + if (qualityGates && qualityGates.length === 1 && !actions.create) { this.context.router.replace( getQualityGateUrl(qualityGates[0].id, organization && organization.key) ); } }); - } - handleAdd(qualityGate) { + handleAdd = qualityGate => { const { addQualityGate, organization } = this.props; const { router } = this.context; addQualityGate(qualityGate); router.push(getQualityGateUrl(qualityGate.id, organization && organization.key)); - } + }; render() { - const { children, qualityGates, edit, organization } = this.props; + const { children, qualityGates, actions, organization } = this.props; const defaultTitle = translate('quality_gates.page'); return (
@@ -88,7 +81,7 @@ export default class QualityGatesApp extends Component {
- + {qualityGates && }
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js b/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js index 85083752c90..c5eddfe37f9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js @@ -30,9 +30,13 @@ import { saveCondition } from '../store/actions'; import Details from '../components/Details'; -import { getQualityGatesAppState } from '../../../store/rootReducer'; +import { getMetrics, getQualityGatesAppState } from '../../../store/rootReducer'; +import { fetchMetrics } from '../../../store/rootActions'; -const mapStateToProps = state => getQualityGatesAppState(state); +const mapStateToProps = state => ({ + ...getQualityGatesAppState(state), + metrics: getMetrics(state) +}); const mapDispatchToProps = dispatch => ({ onShow: qualityGate => dispatch(showQualityGate(qualityGate)), @@ -44,7 +48,8 @@ const mapDispatchToProps = dispatch => ({ onAddCondition: metric => dispatch(addCondition(metric)), onSaveCondition: (oldCondition, newCondition) => dispatch(saveCondition(oldCondition, newCondition)), - onDeleteCondition: condition => dispatch(deleteCondition(condition)) + onDeleteCondition: condition => dispatch(deleteCondition(condition)), + fetchMetrics: () => dispatch(fetchMetrics()) }); export default connect(mapStateToProps, mapDispatchToProps)(Details); -- 2.39.5