diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-11-24 11:41:05 +0100 |
---|---|---|
committer | Eric Hartmann <hartmann.eric@gmail.Com> | 2017-12-04 13:44:55 +0100 |
commit | 2907c50b0aaf6b323ab70b91a3aa4436ef323006 (patch) | |
tree | 241ec8fa257b9d55cabece0488d99f00fe72c948 | |
parent | 7674a1465d06a513879ff09e66e0eb8746dcc837 (diff) | |
download | sonarqube-2907c50b0aaf6b323ab70b91a3aa4436ef323006.tar.gz sonarqube-2907c50b0aaf6b323ab70b91a3aa4436ef323006.zip |
SONAR-10088 SONAR-10114 Allow/prevent QG actions based on list of authorized actions
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<any> { - 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<QualityGate[]> { - 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<any> { +export function fetchQualityGate(id: string): Promise<QualityGate> { return getJSON('/api/qualitygates/show', { id }).catch(throwGlobalError); } @@ -87,11 +92,10 @@ export function deleteCondition(id: string): Promise<void> { export function getGateForProject(project: string): Promise<QualityGate | undefined> { 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<Props> { 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 ( <div id="quality-gate-conditions" className="quality-gate-section"> <h3 className="spacer-bottom">{translate('quality_gates.conditions')}</h3> @@ -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 { <Helmet title={qualityGate.name} /> <DetailsHeader qualityGate={qualityGate} - edit={edit} - onRename={this.handleRenameClick.bind(this)} - onCopy={this.handleCopyClick.bind(this)} - onSetAsDefault={this.handleSetAsDefaultClick.bind(this)} - onDelete={this.handleDeleteClick.bind(this)} + onRename={this.handleRenameClick} + onCopy={this.handleCopyClick} + onSetAsDefault={this.handleSetAsDefaultClick} + onDelete={this.handleDeleteClick} organization={this.props.organization} /> <DetailsContent gate={qualityGate} - canEdit={edit} metrics={metrics} onAddCondition={onAddCondition} onSaveCondition={onSaveCondition} @@ -127,7 +128,3 @@ export default class Details extends React.PureComponent { ); } } - -Details.contextTypes = { - router: PropTypes.object.isRequired -}; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js index e5418420447..e7021d78f0c 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js @@ -24,11 +24,12 @@ import { translate } from '../../../helpers/l10n'; export default class DetailsContent extends React.PureComponent { render() { - const { gate, canEdit, metrics } = this.props; + const { gate, metrics } = this.props; const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props; const conditions = gate.conditions || []; + const actions = gate.actions || {}; - const defaultMessage = canEdit + const defaultMessage = actions.associateProjects ? translate('quality_gates.projects_for_default.edit') : translate('quality_gates.projects_for_default'); @@ -38,7 +39,7 @@ export default class DetailsContent extends React.PureComponent { qualityGate={gate} conditions={conditions} metrics={metrics} - edit={canEdit} + edit={actions.edit} onAddCondition={onAddCondition} onSaveCondition={onSaveCondition} onDeleteCondition={onDeleteCondition} @@ -46,7 +47,11 @@ export default class DetailsContent extends React.PureComponent { <div id="quality-gate-projects" className="quality-gate-section"> <h3 className="spacer-bottom">{translate('quality_gates.projects')}</h3> - {gate.isDefault ? defaultMessage : <Projects qualityGate={gate} edit={canEdit} />} + {gate.isDefault ? ( + defaultMessage + ) : ( + <Projects qualityGate={gate} edit={actions.associateProjects} /> + )} </div> </div> ); 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 ( <div className="layout-page-header-panel layout-page-main-header issues-main-header"> <div className="layout-page-header-panel-inner layout-page-main-header-inner"> @@ -53,17 +53,22 @@ export default class DetailsHeader extends React.PureComponent { {qualityGate.name} {qualityGate.isBuiltIn && <BuiltInBadge className="spacer-left" tooltip={true} />} </h2> - {edit && ( - <div className="pull-right"> + + <div className="pull-right"> + {actions.edit && ( <button id="quality-gate-rename" onClick={this.handleRenameClick}> {translate('rename')} </button> + )} + {actions.copy && ( <button className="little-spacer-left" id="quality-gate-copy" onClick={this.handleCopyClick}> {translate('copy')} </button> + )} + {actions.setAsDefault && ( <button className="little-spacer-left" id="quality-gate-toggle-default" @@ -72,14 +77,16 @@ export default class DetailsHeader extends React.PureComponent { ? translate('unset_as_default') : translate('set_as_default')} </button> + )} + {actions.edit && ( <button id="quality-gate-delete" className="little-spacer-left button-red" onClick={this.handleDeleteClick}> {translate('delete')} </button> - </div> - )} + )} + </div> </div> </div> </div> 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 ( <div id="quality-gates-page" className="layout-page"> @@ -88,7 +81,7 @@ export default class QualityGatesApp extends Component { <div className="layout-page-side" style={{ top }}> <div className="layout-page-side-inner"> <div className="layout-page-filters"> - <ListHeader canEdit={edit} onAdd={this.handleAdd.bind(this)} /> + <ListHeader canEdit={actions && actions.create} onAdd={this.handleAdd} /> {qualityGates && <List organization={organization} qualityGates={qualityGates} />} </div> </div> 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); |