]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11572 limit operators of quality gate conditions in UI
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 10 Dec 2018 16:47:01 +0000 (17:47 +0100)
committerSonarTech <sonartech@sonarsource.com>
Tue, 8 Jan 2019 19:21:06 +0000 (20:21 +0100)
server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionOperator.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/utils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.tsx
deleted file mode 100644 (file)
index 47c852c..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { sortBy } from 'lodash';
-import Select from '../../../components/controls/Select';
-import { translate, getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n';
-
-interface Props {
-  metrics: T.Metric[];
-  onAddCondition: (metric: T.Metric) => void;
-}
-
-interface State {
-  value: number;
-}
-
-interface Option {
-  disabled?: boolean;
-  domain?: string;
-  label: string;
-  value: number;
-}
-
-export default class AddConditionSelect extends React.PureComponent<Props, State> {
-  state = { value: -1 };
-
-  handleChange = ({ value }: Option) => {
-    this.setState({ value });
-    this.props.onAddCondition(this.props.metrics[value]);
-  };
-
-  render() {
-    const { metrics } = this.props;
-
-    const options: Option[] = sortBy(
-      metrics.map((metric, index) => ({
-        value: index,
-        label: getLocalizedMetricName(metric),
-        domain: metric.domain
-      })),
-      'domain'
-    );
-
-    // use "disabled" property to emulate optgroups
-    const optionsWithDomains: Option[] = [];
-    options.forEach((option, index, options) => {
-      const previous = index > 0 ? options[index - 1] : null;
-      if (option.domain && (!previous || previous.domain !== option.domain)) {
-        optionsWithDomains.push({
-          value: 0,
-          label: getLocalizedMetricDomain(option.domain),
-          disabled: true
-        });
-      }
-      optionsWithDomains.push(option);
-    });
-
-    return (
-      <Select
-        className="text-middle input-large"
-        onChange={this.handleChange}
-        options={optionsWithDomains}
-        placeholder={translate('quality_gates.add_condition')}
-        value={this.state.value}
-      />
-    );
-  }
-}
index 6b775c7271230fcddd42c1e985290ce95b519355..9d7762977d54e28f45519ffd48ffa4b3544091e0 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import ConditionOperator from './ConditionOperator';
 import ConditionModal from './ConditionModal';
 import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
 import { translate, getLocalizedMetricName, translateWithParameters } from '../../../helpers/l10n';
@@ -77,6 +76,18 @@ export default class Condition extends React.PureComponent<Props, State> {
     );
   };
 
+  renderOperator() {
+    // TODO can operator be missing?
+    const { op = 'GT' } = this.props.condition;
+    return (
+      <span className="note">
+        {this.props.metric.type === 'RATING'
+          ? translate('quality_gates.operator', op, 'rating')
+          : translate('quality_gates.operator', op)}
+      </span>
+    );
+  }
+
   render() {
     const { condition, canEdit, metric, organization, qualityGate } = this.props;
     return (
@@ -88,9 +99,7 @@ export default class Condition extends React.PureComponent<Props, State> {
           )}
         </td>
 
-        <td className="thin text-middle nowrap">
-          <ConditionOperator canEdit={false} metric={metric} op={condition.op} />
-        </td>
+        <td className="thin text-middle nowrap">{this.renderOperator()}</td>
 
         <td className="thin text-middle nowrap">{formatMeasure(condition.error, metric.type)}</td>
 
index e480bd53c91b70e5bc2592128d917743ca406d43..e2778fe943f424c4cb7bd0995df231071e7ecadd 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import AddConditionSelect from './AddConditionSelect';
+import MetricSelect from './MetricSelect';
 import ConditionOperator from './ConditionOperator';
 import ThresholdInput from './ThresholdInput';
 import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
 import { createCondition, updateCondition } from '../../../api/quality-gates';
 import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { Alert } from '../../../components/ui/Alert';
+import { getPossibleOperators } from '../utils';
 
 interface Props {
   condition?: T.Condition;
@@ -64,31 +65,29 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
     this.mounted = false;
   }
 
-  getUpdatedCondition = (metric: T.Metric) => {
-    return {
-      metric: metric.key,
-      op: metric.type === 'RATING' ? 'GT' : this.state.op,
-      error: this.state.error
-    };
-  };
+  getSinglePossibleOperator(metric: T.Metric) {
+    const operators = getPossibleOperators(metric);
+    return Array.isArray(operators) ? undefined : operators;
+  }
 
   handleFormSubmit = () => {
     if (this.state.metric) {
       const { condition, qualityGate, organization } = this.props;
-      const newCondition = this.getUpdatedCondition(this.state.metric);
-      let submitPromise: Promise<T.Condition>;
-      if (condition) {
-        submitPromise = updateCondition({ organization, id: condition.id, ...newCondition });
-      } else {
-        submitPromise = createCondition({ gateId: qualityGate.id, organization, ...newCondition });
-      }
+      const newCondition: T.Omit<T.Condition, 'id'> = {
+        metric: this.state.metric.key,
+        op: this.getSinglePossibleOperator(this.state.metric) || this.state.op,
+        error: this.state.error
+      };
+      const submitPromise = condition
+        ? updateCondition({ organization, id: condition.id, ...newCondition })
+        : createCondition({ gateId: qualityGate.id, organization, ...newCondition });
       return submitPromise.then(this.props.onAddCondition);
     }
     return Promise.reject();
   };
 
-  handleChooseType = (metric: T.Metric) => {
-    this.setState({ metric });
+  handleMetricChange = (metric: T.Metric) => {
+    this.setState({ metric, op: undefined, error: '' });
   };
 
   handleOperatorChange = (op: string) => {
@@ -112,9 +111,7 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
         {this.state.errorMessage && <Alert variant="error">{this.state.errorMessage}</Alert>}
         <div className="modal-field">
           <label htmlFor="create-user-login">{translate('quality_gates.conditions.metric')}</label>
-          {metrics && (
-            <AddConditionSelect metrics={metrics} onAddCondition={this.handleChooseType} />
-          )}
+          {metrics && <MetricSelect metrics={metrics} onMetricChange={this.handleMetricChange} />}
           {this.props.metric && (
             <span className="note">{getLocalizedMetricName(this.props.metric)}</span>
           )}
@@ -124,7 +121,6 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
             <div className="modal-field">
               <label>{translate('quality_gates.conditions.operator')}</label>
               <ConditionOperator
-                canEdit={true}
                 metric={metric}
                 onOperatorChange={this.handleOperatorChange}
                 op={op}
index 13ef8d24e469883015f3ded3233e9163a4c24aa9..ba7b8aca6c46af1bc18d8ffed94c0c07a1fe660e 100644 (file)
 import * as React from 'react';
 import Select from '../../../components/controls/Select';
 import { translate } from '../../../helpers/l10n';
+import { getPossibleOperators } from '../utils';
 
 interface Props {
   op?: string;
-  canEdit: boolean;
   metric: T.Metric;
-  onOperatorChange?: (op: string) => void;
+  onOperatorChange: (op: string) => void;
 }
 
 export default class ConditionOperator extends React.PureComponent<Props> {
   handleChange = ({ value }: { label: string; value: string }) => {
-    if (this.props.onOperatorChange) {
-      this.props.onOperatorChange(value);
-    }
+    this.props.onOperatorChange(value);
   };
 
-  render() {
-    const { canEdit, metric, op } = this.props;
-    if (!canEdit && op) {
-      return metric.type === 'RATING' ? (
-        <span className="note">{translate('quality_gates.operator', op, 'rating')}</span>
-      ) : (
-        <span className="note">{translate('quality_gates.operator', op)}</span>
-      );
-    }
+  getLabel(op: string, metric: T.Metric) {
+    return metric.type === 'RATING'
+      ? translate('quality_gates.operator', op, 'rating')
+      : translate('quality_gates.operator', op);
+  }
 
-    if (metric.type === 'RATING') {
-      return <span className="note">{translate('quality_gates.operator.GT.rating')}</span>;
-    }
+  render() {
+    const operators = getPossibleOperators(this.props.metric);
 
-    const operators = ['LT', 'GT', 'EQ', 'NE'];
-    const operatorOptions = operators.map(op => {
-      const label = translate('quality_gates.operator', op);
-      return { label, value: op };
-    });
+    if (Array.isArray(operators)) {
+      const operatorOptions = operators.map(op => {
+        const label = this.getLabel(op, this.props.metric);
+        return { label, value: op };
+      });
 
-    return (
-      <Select
-        autoFocus={true}
-        className="input-medium"
-        clearable={false}
-        name="operator"
-        onChange={this.handleChange}
-        options={operatorOptions}
-        searchable={false}
-        value={op}
-      />
-    );
+      return (
+        <Select
+          autoFocus={true}
+          className="input-medium"
+          clearable={false}
+          name="operator"
+          onChange={this.handleChange}
+          options={operatorOptions}
+          searchable={false}
+          value={this.props.op}
+        />
+      );
+    } else {
+      return <span className="note">{this.getLabel(operators, this.props.metric)}</span>;
+    }
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx
new file mode 100644 (file)
index 0000000..60331a8
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { sortBy } from 'lodash';
+import Select from '../../../components/controls/Select';
+import { translate, getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n';
+
+interface Props {
+  metrics: T.Metric[];
+  onMetricChange: (metric: T.Metric) => void;
+}
+
+interface State {
+  value: number;
+}
+
+interface Option {
+  disabled?: boolean;
+  domain?: string;
+  label: string;
+  value: number;
+}
+
+export default class MetricSelect extends React.PureComponent<Props, State> {
+  state = { value: -1 };
+
+  handleChange = ({ value }: Option) => {
+    this.setState({ value });
+    this.props.onMetricChange(this.props.metrics[value]);
+  };
+
+  render() {
+    const { metrics } = this.props;
+
+    const options: Option[] = sortBy(
+      metrics.map((metric, index) => ({
+        value: index,
+        label: getLocalizedMetricName(metric),
+        domain: metric.domain
+      })),
+      'domain'
+    );
+
+    // use "disabled" property to emulate optgroups
+    const optionsWithDomains: Option[] = [];
+    options.forEach((option, index, options) => {
+      const previous = index > 0 ? options[index - 1] : null;
+      if (option.domain && (!previous || previous.domain !== option.domain)) {
+        optionsWithDomains.push({
+          value: 0,
+          label: getLocalizedMetricDomain(option.domain),
+          disabled: true
+        });
+      }
+      optionsWithDomains.push(option);
+    });
+
+    return (
+      <Select
+        className="text-middle input-large"
+        onChange={this.handleChange}
+        options={optionsWithDomains}
+        placeholder={translate('search.search_for_metrics')}
+        value={this.state.value}
+      />
+    );
+  }
+}
index 08a3a01ff8c7bf2ac563d70bf2969c59ced7eed7..8c76e632ee8c8c05a57ee705b00881bb9f5182c5 100644 (file)
@@ -46,3 +46,13 @@ export function replaceCondition(
     });
   return { ...qualityGate, conditions };
 }
+
+export function getPossibleOperators(metric: T.Metric) {
+  if (metric.direction === 1) {
+    return 'LT';
+  } else if (metric.direction === -1) {
+    return 'GT';
+  } else {
+    return ['LT', 'GT'];
+  }
+}
index d13743b6434e99037a70d4e1471e57ec728a58f4..9dd6544c165cd1590062847cd41938d18f25eaca 100644 (file)
@@ -951,6 +951,7 @@ search.search_for_authors=Search for authors...
 search.search_for_directories=Search for directories...
 search.search_for_files=Search for files...
 search.search_for_modules=Search for modules...
+search.search_for_metrics=Search for metrics...
 
 
 #------------------------------------------------------------------------------