aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-11-24 11:41:05 +0100
committerEric Hartmann <hartmann.eric@gmail.Com>2017-12-04 13:44:55 +0100
commit2907c50b0aaf6b323ab70b91a3aa4436ef323006 (patch)
tree241ec8fa257b9d55cabece0488d99f00fe72c948
parent7674a1465d06a513879ff09e66e0eb8746dcc837 (diff)
downloadsonarqube-2907c50b0aaf6b323ab70b91a3aa4436ef323006.tar.gz
sonarqube-2907c50b0aaf6b323ab70b91a3aa4436ef323006.zip
SONAR-10088 SONAR-10114 Allow/prevent QG actions based on list of authorized actions
-rw-r--r--server/sonar-web/src/main/js/api/quality-gates.ts46
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js14
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js17
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Details.js49
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js13
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js19
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js25
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js11
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);