]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10088 SONAR-10114 Allow/prevent QG actions based on list of authorized actions
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 24 Nov 2017 10:41:05 +0000 (11:41 +0100)
committerEric Hartmann <hartmann.eric@gmail.Com>
Mon, 4 Dec 2017 12:44:55 +0000 (13:44 +0100)
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js
server/sonar-web/src/main/js/apps/quality-gates/components/Details.js
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js

index 325f181c7bb5a4e3f1256ca28e294e95881deec6..b7eb009e7f126c417f8bc5592f6c1d5050ea5e32 100644 (file)
 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
       }
   );
 }
index 0a4f20f304b083e5b92f3cc35c842f6a9309a4ea..95b899a9aa397877e6b408aef66d6768f48284b2 100644 (file)
@@ -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 });
         }
index 4fa2bc575af1b9514f408c70b82cc911feb14a6f..d04a0ee2f6e8d145196ea5fcc601684b710060b8 100644 (file)
@@ -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', () => ({
index 866af392525523e5621bafab436335317ef2c6a7..471d9d0f132af66497881dd2385ff793d157a357 100644 (file)
@@ -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 = [];
index f46cba13af44b58223ef8e6352751a7dc779e90f..7986f5a1816fbfd90cf7b032201f119a31254bb4 100644 (file)
@@ -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}
index 751ae51ede3dd57e11411295d0888d5410b6f460..97249bf6532db766c88de2a2d98caec66111b042 100644 (file)
@@ -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
-};
index e54184204477e6cf097c8e747c320914c7b9ed86..e7021d78f0c063ecc420f9ed1c09b34ffd017611 100644 (file)
@@ -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>
     );
index db4c2bb5fcd3c81a4976faed0c331c473a4d7d2b..dc878e83e39ba47816d65ab01eb2ddbfdddbe66f 100644 (file)
@@ -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>
index 018715db8a5bc9a871db2e4134e82e5097b62122..a8b0b97d490ed2b40431357c071bb8d7fd4e699d 100644 (file)
@@ -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>
index 85083752c90c17e62c142d3e0e1ec2dc40d1d073..c5eddfe37f9638d13f7046d8ec8d89b55b7ccc43 100644 (file)
@@ -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);