]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21178 Improve quality gates page accessiblity
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 26 Jan 2024 10:59:54 +0000 (11:59 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 30 Jan 2024 15:02:03 +0000 (15:02 +0000)
server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionOperator.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/helpers/qualityGates.ts

index 765b6cd79dad9d09beb9c2ca51cc272b1ba12e6b..dda75f630848498f5bc64bbede9db7aa363ca975 100644 (file)
@@ -30,6 +30,7 @@ import {
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
+import { getOperatorLabel } from '../../../helpers/qualityGates';
 import {
   getComponentDrilldownUrl,
   getComponentIssuesUrl,
@@ -155,11 +156,7 @@ export default class QualityGateCondition extends React.PureComponent<Props> {
     const threshold = (condition.level === 'ERROR' ? condition.error : condition.warning) as string;
     const actual = (condition.period ? measure.period?.value : measure.value) as string;
 
-    let operator = translate('quality_gates.operator', condition.op);
-
-    if (metric.type === MetricType.Rating) {
-      operator = translate('quality_gates.operator', condition.op, 'rating');
-    }
+    const operator = getOperatorLabel(condition.op, metric);
 
     return this.wrapWithLink(
       <div className="sw-flex sw-items-center sw-p-2">
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx
new file mode 100644 (file)
index 0000000..61f7ae7
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { ButtonPrimary, FormField, Modal, RadioButton } from 'design-system';
+import * as React from 'react';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { isDiffMetric } from '../../../helpers/measures';
+import { useCreateConditionMutation } from '../../../queries/quality-gates';
+import { Condition, Metric, QualityGate } from '../../../types/types';
+import { getPossibleOperators } from '../utils';
+import ConditionOperator from './ConditionOperator';
+import MetricSelect from './MetricSelect';
+import ThresholdInput from './ThresholdInput';
+
+interface Props {
+  metrics: Metric[];
+  onClose: () => void;
+  qualityGate: QualityGate;
+}
+
+const ADD_CONDITION_MODAL_ID = 'add-condition-modal';
+
+export default function AddConditionModal({ metrics, onClose, qualityGate }: Readonly<Props>) {
+  const [errorThreshold, setErrorThreshold] = React.useState('');
+  const [scope, setScope] = React.useState<'new' | 'overall'>('new');
+  const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>();
+  const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>();
+  const { mutateAsync: createCondition } = useCreateConditionMutation(qualityGate.name);
+
+  const getSinglePossibleOperator = (metric: Metric) => {
+    const operators = getPossibleOperators(metric);
+    return Array.isArray(operators) ? undefined : operators;
+  };
+
+  const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    if (selectedMetric) {
+      const newCondition: Omit<Condition, 'id'> = {
+        metric: selectedMetric.key,
+        op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator,
+        error: errorThreshold,
+      };
+      await createCondition(newCondition);
+      onClose();
+    }
+  };
+
+  const handleScopeChange = (scope: 'new' | 'overall') => {
+    let correspondingMetric;
+
+    if (selectedMetric) {
+      const correspondingMetricKey =
+        scope === 'new' ? `new_${selectedMetric.key}` : selectedMetric.key.replace(/^new_/, '');
+      correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey);
+    }
+
+    setScope(scope);
+    setSelectedMetric(correspondingMetric);
+  };
+
+  const handleMetricChange = (metric: Metric) => {
+    setSelectedMetric(metric);
+    setSelectedOperator(undefined);
+    setErrorThreshold('');
+  };
+
+  const handleOperatorChange = (op: string) => {
+    setSelectedOperator(op);
+  };
+
+  const handleErrorChange = (error: string) => {
+    setErrorThreshold(error);
+  };
+
+  const renderBody = () => {
+    return (
+      <form onSubmit={handleFormSubmit} id={ADD_CONDITION_MODAL_ID}>
+        <FormField label={translate('quality_gates.conditions.where')}>
+          <div className="sw-flex sw-gap-4">
+            <RadioButton checked={scope === 'new'} onCheck={handleScopeChange} value="new">
+              <span data-test="quality-gates__condition-scope-new">
+                {translate('quality_gates.conditions.new_code')}
+              </span>
+            </RadioButton>
+            <RadioButton checked={scope === 'overall'} onCheck={handleScopeChange} value="overall">
+              <span data-test="quality-gates__condition-scope-overall">
+                {translate('quality_gates.conditions.overall_code')}
+              </span>
+            </RadioButton>
+          </div>
+        </FormField>
+
+        <FormField
+          description={selectedMetric && getLocalizedMetricName(selectedMetric)}
+          htmlFor="condition-metric"
+          label={translate('quality_gates.conditions.fails_when')}
+        >
+          <MetricSelect
+            metric={selectedMetric}
+            metricsArray={metrics.filter((m) =>
+              scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key),
+            )}
+            onMetricChange={handleMetricChange}
+          />
+        </FormField>
+
+        {selectedMetric && (
+          <div className="sw-flex sw-gap-2">
+            <FormField
+              className="sw-mb-0"
+              htmlFor="condition-operator"
+              label={translate('quality_gates.conditions.operator')}
+            >
+              <ConditionOperator
+                metric={selectedMetric}
+                onOperatorChange={handleOperatorChange}
+                op={selectedOperator}
+              />
+            </FormField>
+            <FormField
+              htmlFor="condition-threshold"
+              label={translate('quality_gates.conditions.value')}
+            >
+              <ThresholdInput
+                metric={selectedMetric}
+                name="error"
+                onChange={handleErrorChange}
+                value={errorThreshold}
+              />
+            </FormField>
+          </div>
+        )}
+      </form>
+    );
+  };
+
+  return (
+    <Modal
+      isScrollable={false}
+      isOverflowVisible
+      headerTitle={translate('quality_gates.add_condition')}
+      onClose={onClose}
+      body={renderBody()}
+      primaryButton={
+        <ButtonPrimary
+          autoFocus
+          disabled={selectedMetric === undefined}
+          id="add-condition-button"
+          form={ADD_CONDITION_MODAL_ID}
+          type="submit"
+        >
+          {translate('quality_gates.add_condition')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('close')}
+    />
+  );
+}
index 3d388f8afbc2bd77852d6e1d6ccb89fc96cafc21..e15de9c0759a2b436daaae7c9903354785ebf7e6 100644 (file)
@@ -33,12 +33,12 @@ import {
 import * as React from 'react';
 import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
 import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
+import { getOperatorLabel } from '../../../helpers/qualityGates';
 import { useDeleteConditionMutation } from '../../../queries/quality-gates';
-import { MetricType } from '../../../types/metrics';
 import { CaycStatus, Condition as ConditionType, Metric, QualityGate } from '../../../types/types';
 import { getLocalizedMetricNameNoDiffMetric, isConditionWithFixedValue } from '../utils';
-import ConditionModal from './ConditionModal';
 import ConditionValue from './ConditionValue';
+import EditConditionModal from './EditConditionModal';
 
 export enum ConditionChange {
   Added = 'added',
@@ -67,6 +67,7 @@ export default function ConditionComponent({
   const [modal, setModal] = React.useState(false);
   const { mutateAsync: deleteCondition } = useDeleteConditionMutation(qualityGate.name);
   const metrics = useMetrics();
+  const { op = 'GT' } = condition;
 
   const handleOpenUpdate = () => {
     setModal(true);
@@ -84,13 +85,6 @@ export default function ConditionComponent({
     setDeleteFormOpen(false);
   };
 
-  const renderOperator = () => {
-    const { op = 'GT' } = condition;
-    return metric.type === MetricType.Rating
-      ? translate('quality_gates.operator', op, 'rating')
-      : translate('quality_gates.operator', op);
-  };
-
   const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant;
 
   return (
@@ -100,7 +94,7 @@ export default function ConditionComponent({
         {metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />}
       </ContentCell>
 
-      <ContentCell className="sw-whitespace-nowrap">{renderOperator()}</ContentCell>
+      <ContentCell className="sw-whitespace-nowrap">{getOperatorLabel(op, metric)}</ContentCell>
 
       <NumericalCell className="sw-whitespace-nowrap">
         <ConditionValue
@@ -126,7 +120,7 @@ export default function ConditionComponent({
                   size="small"
                 />
                 {modal && (
-                  <ConditionModal
+                  <EditConditionModal
                     condition={condition}
                     header={translate('quality_gates.update_condition')}
                     metric={metric}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
deleted file mode 100644 (file)
index 777a2d3..0000000
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { ButtonPrimary, FormField, Modal, RadioButton } from 'design-system';
-import * as React from 'react';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { isDiffMetric } from '../../../helpers/measures';
-import {
-  useCreateConditionMutation,
-  useUpdateConditionMutation,
-} from '../../../queries/quality-gates';
-import { Condition, Metric, QualityGate } from '../../../types/types';
-import { getPossibleOperators } from '../utils';
-import ConditionOperator from './ConditionOperator';
-import MetricSelect from './MetricSelect';
-import ThresholdInput from './ThresholdInput';
-
-interface Props {
-  condition?: Condition;
-  metric?: Metric;
-  metrics?: Metric[];
-  header: string;
-  onClose: () => void;
-  qualityGate: QualityGate;
-}
-
-const ADD_CONDITION_MODAL_ID = 'add-condition-modal';
-
-export default function ConditionModal({
-  condition,
-  metric,
-  metrics,
-  header,
-  onClose,
-  qualityGate,
-}: Readonly<Props>) {
-  const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : '');
-  const [scope, setScope] = React.useState<'new' | 'overall'>('new');
-  const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>(metric);
-  const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>(
-    condition ? condition.op : undefined,
-  );
-  const { mutateAsync: createCondition } = useCreateConditionMutation(qualityGate.name);
-  const { mutateAsync: updateCondition } = useUpdateConditionMutation(qualityGate.name);
-
-  const getSinglePossibleOperator = (metric: Metric) => {
-    const operators = getPossibleOperators(metric);
-    return Array.isArray(operators) ? undefined : operators;
-  };
-
-  const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
-    event.preventDefault();
-
-    if (selectedMetric) {
-      const newCondition: Omit<Condition, 'id'> = {
-        metric: selectedMetric.key,
-        op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator,
-        error: errorThreshold,
-      };
-      const submitPromise = condition
-        ? updateCondition({ id: condition.id, ...newCondition })
-        : createCondition(newCondition);
-      await submitPromise;
-      onClose();
-    }
-  };
-
-  const handleScopeChange = (scope: 'new' | 'overall') => {
-    let correspondingMetric;
-
-    if (selectedMetric && metrics) {
-      const correspondingMetricKey =
-        scope === 'new' ? `new_${selectedMetric.key}` : selectedMetric.key.replace(/^new_/, '');
-      correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey);
-    }
-
-    setScope(scope);
-    setSelectedMetric(correspondingMetric);
-  };
-
-  const handleMetricChange = (metric: Metric) => {
-    setSelectedMetric(metric);
-    setSelectedOperator(undefined);
-    setErrorThreshold('');
-  };
-
-  const handleOperatorChange = (op: string) => {
-    setSelectedOperator(op);
-  };
-
-  const handleErrorChange = (error: string) => {
-    setErrorThreshold(error);
-  };
-
-  const renderBody = () => {
-    return (
-      <form id={ADD_CONDITION_MODAL_ID} onSubmit={handleFormSubmit}>
-        {metric === undefined && (
-          <FormField label={translate('quality_gates.conditions.where')}>
-            <div className="sw-flex sw-gap-4">
-              <RadioButton checked={scope === 'new'} onCheck={handleScopeChange} value="new">
-                <span data-test="quality-gates__condition-scope-new">
-                  {translate('quality_gates.conditions.new_code')}
-                </span>
-              </RadioButton>
-              <RadioButton
-                checked={scope === 'overall'}
-                onCheck={handleScopeChange}
-                value="overall"
-              >
-                <span data-test="quality-gates__condition-scope-overall">
-                  {translate('quality_gates.conditions.overall_code')}
-                </span>
-              </RadioButton>
-            </div>
-          </FormField>
-        )}
-
-        <FormField
-          description={metric && getLocalizedMetricName(metric)}
-          htmlFor="condition-metric"
-          label={translate('quality_gates.conditions.fails_when')}
-        >
-          {metrics && (
-            <MetricSelect
-              metric={selectedMetric}
-              metricsArray={metrics.filter((m) =>
-                scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key),
-              )}
-              onMetricChange={handleMetricChange}
-            />
-          )}
-        </FormField>
-
-        {selectedMetric && (
-          <div className="sw-flex sw-gap-2">
-            <FormField
-              className="sw-mb-0"
-              htmlFor="condition-operator"
-              label={translate('quality_gates.conditions.operator')}
-            >
-              <ConditionOperator
-                metric={selectedMetric}
-                onOperatorChange={handleOperatorChange}
-                op={selectedOperator}
-              />
-            </FormField>
-            <FormField
-              htmlFor="condition-threshold"
-              label={translate('quality_gates.conditions.value')}
-            >
-              <ThresholdInput
-                metric={selectedMetric}
-                name="error"
-                onChange={handleErrorChange}
-                value={errorThreshold}
-              />
-            </FormField>
-          </div>
-        )}
-      </form>
-    );
-  };
-
-  return (
-    <Modal
-      isScrollable={false}
-      isOverflowVisible
-      headerTitle={header}
-      onClose={onClose}
-      body={renderBody()}
-      primaryButton={
-        <ButtonPrimary
-          autoFocus
-          disabled={selectedMetric === undefined}
-          id="add-condition-button"
-          form={ADD_CONDITION_MODAL_ID}
-          type="submit"
-        >
-          {header}
-        </ButtonPrimary>
-      }
-      secondaryButtonLabel={translate('close')}
-    />
-  );
-}
index 0278ccf11d31b15de0154d44fda801c288162b9e..4382b85ea3a180fd49021076a440f6ca8b5fdf57 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { InputSelect, Note } from 'design-system';
 import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
+import { getOperatorLabel } from '../../../helpers/qualityGates';
 import { Metric } from '../../../types/types';
 import { getPossibleOperators } from '../utils';
 
@@ -34,18 +34,12 @@ export default class ConditionOperator extends React.PureComponent<Props> {
     this.props.onOperatorChange(value);
   };
 
-  getLabel(op: string, metric: Metric) {
-    return metric.type === 'RATING'
-      ? translate('quality_gates.operator', op, 'rating')
-      : translate('quality_gates.operator', op);
-  }
-
   render() {
     const operators = getPossibleOperators(this.props.metric);
 
     if (Array.isArray(operators)) {
       const operatorOptions = operators.map((op) => {
-        const label = this.getLabel(op, this.props.metric);
+        const label = getOperatorLabel(op, this.props.metric);
         return { label, value: op };
       });
 
@@ -64,6 +58,6 @@ export default class ConditionOperator extends React.PureComponent<Props> {
       );
     }
 
-    return <Note className="sw-w-abs-150">{this.getLabel(operators, this.props.metric)}</Note>;
+    return <Note className="sw-w-abs-150">{getOperatorLabel(operators, this.props.metric)}</Note>;
   }
 }
index 9267a9eff2c6d96cc9f452e21249e8ac4df2020e..aeba605928cd30df2e5634a935580476e5900914 100644 (file)
@@ -42,11 +42,11 @@ import { Feature } from '../../../types/features';
 import { MetricKey } from '../../../types/metrics';
 import { CaycStatus, Condition as ConditionType, QualityGate } from '../../../types/types';
 import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils';
+import AddConditionModal from './AddConditionModal';
 import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide';
 import CaycCompliantBanner from './CaycCompliantBanner';
 import CaycCondition from './CaycCondition';
 import CaycFixOptimizeBanner from './CaycFixOptimizeBanner';
-import ConditionModal from './ConditionModal';
 import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
 import ConditionsTable from './ConditionsTable';
 
@@ -110,12 +110,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
         (metric, condition) => metric.key === condition.metric,
       );
       return (
-        <ConditionModal
-          header={translate('quality_gates.add_condition')}
-          metrics={availableMetrics}
-          onClose={onClose}
-          qualityGate={qualityGate}
-        />
+        <AddConditionModal metrics={availableMetrics} onClose={onClose} qualityGate={qualityGate} />
       );
     },
     [metrics, qualityGate],
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx
new file mode 100644 (file)
index 0000000..24faa55
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { ButtonPrimary, FormField, Highlight, Modal, Note } from 'design-system';
+import { isArray } from 'lodash';
+import * as React from 'react';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { useUpdateConditionMutation } from '../../../queries/quality-gates';
+import { Condition, Metric, QualityGate } from '../../../types/types';
+import { getPossibleOperators } from '../utils';
+import ConditionOperator from './ConditionOperator';
+import ThresholdInput from './ThresholdInput';
+
+interface Props {
+  condition: Condition;
+  metric: Metric;
+  header: string;
+  onClose: () => void;
+  qualityGate: QualityGate;
+}
+
+const EDIT_CONDITION_MODAL_ID = 'edit-condition-modal';
+
+export default function EditConditionModal({
+  condition,
+  metric,
+  onClose,
+  qualityGate,
+}: Readonly<Props>) {
+  const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : '');
+
+  const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>(
+    condition ? condition.op : undefined,
+  );
+  const { mutateAsync: updateCondition } = useUpdateConditionMutation(qualityGate.name);
+
+  const getSinglePossibleOperator = (metric: Metric) => {
+    const operators = getPossibleOperators(metric);
+    return isArray(operators) ? selectedOperator : operators;
+  };
+
+  const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    const newCondition: Omit<Condition, 'id'> = {
+      metric: metric.key,
+      op: getSinglePossibleOperator(metric),
+      error: errorThreshold,
+    };
+    await updateCondition({ id: condition.id, ...newCondition });
+    onClose();
+  };
+
+  const handleErrorChange = (error: string) => {
+    setErrorThreshold(error);
+  };
+
+  const handleOperatorChange = (op: string) => {
+    setSelectedOperator(op);
+  };
+
+  const renderBody = () => {
+    return (
+      <form onSubmit={handleFormSubmit} id={EDIT_CONDITION_MODAL_ID}>
+        <span className="sw-flex sw-flex-col sw-w-full sw-mb-6" aria-hidden="true">
+          <Highlight className="sw-mb-2 sw-flex sw-items-center sw-gap-2">
+            <span>{translate('quality_gates.conditions.fails_when')}</span>
+          </Highlight>
+          <Note className="sw-mt-2">{getLocalizedMetricName(metric)}</Note>
+        </span>
+
+        <div className="sw-flex sw-gap-2">
+          <FormField
+            className="sw-mb-0"
+            htmlFor="condition-operator"
+            label={translate('quality_gates.conditions.operator')}
+          >
+            <ConditionOperator
+              metric={metric}
+              onOperatorChange={handleOperatorChange}
+              op={selectedOperator}
+            />
+          </FormField>
+          <FormField
+            htmlFor="condition-threshold"
+            label={translate('quality_gates.conditions.value')}
+          >
+            <ThresholdInput
+              metric={metric}
+              name="error"
+              onChange={handleErrorChange}
+              value={errorThreshold}
+            />
+          </FormField>
+        </div>
+      </form>
+    );
+  };
+
+  return (
+    <Modal
+      isScrollable={false}
+      isOverflowVisible
+      headerTitle={translate('quality_gates.update_condition')}
+      onClose={onClose}
+      body={renderBody()}
+      primaryButton={
+        <ButtonPrimary form={EDIT_CONDITION_MODAL_ID} type="submit">
+          {translate('quality_gates.update_condition')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('close')}
+    />
+  );
+}
index eb9a67bd80c2fbf2727d22df3353e7ecb790bc97..f12283529bcf022645d7a524d0d1a2ec4977f3bd 100644 (file)
@@ -26,7 +26,7 @@ import { searchProjects, searchUsers } from '../../../../api/quality-gates';
 import { dismissNotice } from '../../../../api/users';
 import { mockLoggedInUser } from '../../../../helpers/testMocks';
 import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
-import { byRole } from '../../../../helpers/testSelector';
+import { byRole, byTestId } from '../../../../helpers/testSelector';
 import { Feature } from '../../../../types/features';
 import { CaycStatus } from '../../../../types/types';
 import { NoticeType } from '../../../../types/users';
@@ -218,49 +218,52 @@ it('should be able to add a condition', async () => {
   // On new code
   await user.click(await screen.findByText('quality_gates.add_condition'));
 
-  let dialog = within(screen.getByRole('dialog'));
+  const dialog = byRole('dialog');
 
-  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' }));
-  await selectEvent.select(dialog.getByRole('combobox'), ['Issues']);
-  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+  await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.new_code' }).get());
+
+  await selectEvent.select(dialog.byRole('combobox').get(), 'Issues');
+  await user.click(
+    await dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).find(),
+  );
   await user.keyboard('12');
-  await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
-  const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
-  expect(await newConditions.findByRole('cell', { name: 'Issues' })).toBeInTheDocument();
-  expect(await newConditions.findByRole('cell', { name: '12' })).toBeInTheDocument();
+  await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get());
+  const newConditions = byTestId('quality-gates__conditions-new');
+  expect(await newConditions.byRole('cell', { name: 'Issues' }).find()).toBeInTheDocument();
+  expect(await newConditions.byRole('cell', { name: '12' }).find()).toBeInTheDocument();
 
   // On overall code
   await user.click(await screen.findByText('quality_gates.add_condition'));
 
-  dialog = within(screen.getByRole('dialog'));
-  await selectEvent.select(dialog.getByRole('combobox'), ['Info Issues']);
-  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
-  await user.click(dialog.getByLabelText('quality_gates.conditions.operator'));
+  await selectEvent.select(dialog.byRole('combobox').get(), ['Info Issues']);
+  await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get());
+  await user.click(dialog.byLabelText('quality_gates.conditions.operator').get());
 
-  await user.click(dialog.getByText('quality_gates.operator.LT'));
-  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+  await user.click(dialog.byText('quality_gates.operator.LT').get());
+  await user.click(dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).get());
   await user.keyboard('42');
-  await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
+  await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get());
 
-  const overallConditions = within(await screen.findByTestId('quality-gates__conditions-overall'));
+  const overallConditions = byTestId('quality-gates__conditions-overall');
 
-  expect(await overallConditions.findByRole('cell', { name: 'Info Issues' })).toBeInTheDocument();
-  expect(await overallConditions.findByRole('cell', { name: '42' })).toBeInTheDocument();
+  expect(
+    await overallConditions.byRole('cell', { name: 'Info Issues' }).find(),
+  ).toBeInTheDocument();
+  expect(await overallConditions.byRole('cell', { name: '42' }).find()).toBeInTheDocument();
 
   // Select a rating
   await user.click(await screen.findByText('quality_gates.add_condition'));
 
-  dialog = within(screen.getByRole('dialog'));
-  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
-  await selectEvent.select(dialog.getByRole('combobox'), ['Maintainability Rating']);
-  await user.click(dialog.getByLabelText('quality_gates.conditions.value'));
-  await user.click(dialog.getByText('B'));
-  await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
+  await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get());
+  await selectEvent.select(dialog.byRole('combobox').get(), ['Maintainability Rating']);
+  await user.click(dialog.byLabelText('quality_gates.conditions.value').get());
+  await user.click(dialog.byText('B').get());
+  await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get());
 
   expect(
-    await overallConditions.findByRole('cell', { name: 'Maintainability Rating' }),
+    await overallConditions.byRole('cell', { name: 'Maintainability Rating' }).find(),
   ).toBeInTheDocument();
-  expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument();
+  expect(await overallConditions.byRole('cell', { name: 'B' }).find()).toBeInTheDocument();
 });
 
 it('should be able to edit a condition', async () => {
index abba1d81c7aab9493e14202c0d1ee357087ffdac..19c8e161ed7657fd21723257ddf71e2d159a56bd 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { MetricKey } from '../types/metrics';
+import { MetricKey, MetricType } from '../types/metrics';
 import {
   QualityGateApplicationStatusChildProject,
   QualityGateProjectStatus,
   QualityGateStatusCondition,
 } from '../types/quality-gates';
+import { Metric } from '../types/types';
+import { translate } from './l10n';
+
+export function getOperatorLabel(op: string, metric: Metric) {
+  return metric.type === MetricType.Rating
+    ? translate('quality_gates.operator', op, 'rating')
+    : translate('quality_gates.operator', op);
+}
 
 export function extractStatusConditionsFromProjectStatus(
   projectStatus: QualityGateProjectStatus,