]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17815 Adding CAYC section QG conditions
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Mon, 2 Jan 2023 09:02:39 +0000 (10:02 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 5 Jan 2023 20:02:57 +0000 (20:02 +0000)
21 files changed:
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.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
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/styles.css
server/sonar-web/src/main/js/apps/quality-gates/utils.ts
server/sonar-web/src/main/js/components/controls/SelectList.tsx
server/sonar-web/src/main/js/components/measure/RatingTooltipContent.tsx
server/sonar-web/src/main/js/helpers/ratings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 09a01a3906cca45167a9f48e084d401641b39518..5167a9c05d797e323b860e48e114602b43f62daf 100644 (file)
@@ -139,6 +139,10 @@ module.exports = {
     darkBackgroundSeparator: '#413b3b',
     darkBackgroundFontColor: '#f6f8fa',
 
+    //quality gate badges
+    badgeGreenBackground: '#f0faec',
+    badgeRedBackground: '#ffeaea',
+
     // new color palette
     // Some of these colors duplicate what we have above, but this will make it
     // easier to align with the UX designers on what colors to use where.
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx
new file mode 100644 (file)
index 0000000..7c98337
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 DocLink from '../../../components/common/DocLink';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  badgeType: 'missing' | 'weak' | 'ok';
+}
+
+export default function CaycBadgeTooltip({ badgeType }: Props) {
+  return (
+    <div>
+      <p className="spacer-bottom padded-bottom bordered-bottom-cayc">
+        {translate('quality_gates.cayc.tooltip', badgeType)}
+      </p>
+      <DocLink to="/user-guide/clean-as-you-code/">
+        {translate('quality_gates.cayc.badge.tooltip.learn_more')}
+      </DocLink>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx
new file mode 100644 (file)
index 0000000..546c79d
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { FormattedMessage } from 'react-intl';
+import DocLink from '../../../components/common/DocLink';
+import { Button, ButtonLink } from '../../../components/controls/buttons';
+import ModalButton, { ModalProps } from '../../../components/controls/ModalButton';
+import { Alert } from '../../../components/ui/Alert';
+import { translate } from '../../../helpers/l10n';
+import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
+import { getWeakAndMissingConditions } from '../utils';
+import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
+import ConditionsTable from './ConditionsTable';
+
+interface Props {
+  canEdit: boolean;
+  metrics: Dict<Metric>;
+  onAddCondition: (condition: ConditionType) => void;
+  onRemoveCondition: (Condition: ConditionType) => void;
+  onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
+  qualityGate: QualityGate;
+  updatedConditionId?: string;
+  conditions: ConditionType[];
+  scope: 'new' | 'overall' | 'new-cayc';
+}
+
+interface State {
+  showEdit: boolean;
+}
+
+export default class CaycConditions extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = { showEdit: false };
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (this.props.qualityGate.id !== prevProps.qualityGate.id) {
+      this.setState({ showEdit: false });
+    }
+  }
+
+  toggleEditing = () => {
+    const { showEdit } = this.state;
+    this.setState({ showEdit: !showEdit });
+  };
+
+  renderConfirmModal = ({ onClose }: ModalProps) => (
+    <CaycReviewUpdateConditionsModal {...this.props} onClose={onClose} />
+  );
+
+  render() {
+    const { conditions, canEdit } = this.props;
+    const { showEdit } = this.state;
+    const { weakConditions, missingConditions } = getWeakAndMissingConditions(conditions);
+    const caycDescription = canEdit
+      ? `${translate('quality_gates.cayc.description')} ${translate(
+          'quality_gates.cayc.description.extended'
+        )}`
+      : translate('quality_gates.cayc.description');
+
+    return (
+      <div className="cayc-conditions-wrapper big-padded big-spacer-top big-spacer-bottom">
+        <h4>{translate('quality_gates.cayc')}</h4>
+        <div className="big-padded-top big-padded-bottom">
+          <FormattedMessage
+            id="quality_gates.cayc.description"
+            defaultMessage={caycDescription}
+            values={{
+              link: (
+                <DocLink to="/user-guide/clean-as-you-code/">
+                  {translate('quality_gates.cayc')}
+                </DocLink>
+              ),
+            }}
+          />
+        </div>
+        {(weakConditions.length > 0 || missingConditions.length > 0) && (
+          <Alert className="big-spacer-bottom" variant="warning">
+            <h4 className="spacer-bottom cayc-warning-header">
+              {translate('quality_gates.cayc_condition.missing_warning.title')}
+            </h4>
+            <p className="cayc-warning-description">
+              {translate('quality_gates.cayc_condition.missing_warning.description')}
+            </p>
+            {canEdit && (
+              <ModalButton modal={this.renderConfirmModal}>
+                {({ onClick }) => (
+                  <Button className="big-spacer-top spacer-bottom" onClick={onClick}>
+                    {translate('quality_gates.cayc_condition.review_update')}
+                  </Button>
+                )}
+              </ModalButton>
+            )}
+          </Alert>
+        )}
+        {canEdit && (
+          <ButtonLink className="pull-right spacer-right" onClick={this.toggleEditing}>
+            {showEdit
+              ? translate('quality_gates.cayc.lock_edit')
+              : translate('quality_gates.cayc.unlock_edit')}
+          </ButtonLink>
+        )}
+
+        <div className="big-padded-top">
+          <ConditionsTable
+            {...this.props}
+            showEdit={showEdit}
+            missingConditions={missingConditions}
+          />
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx
new file mode 100644 (file)
index 0000000..3193145
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 classNames from 'classnames';
+import * as React from 'react';
+import Tooltip from '../../../components/controls/Tooltip';
+import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
+import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
+import CheckIcon from '../../../components/icons/CheckIcon';
+import { translate } from '../../../helpers/l10n';
+import { Condition } from '../../../types/types';
+import { isCaycWeakCondition } from '../utils';
+import CaycBadgeTooltip from './CaycBadgeTooltip';
+
+interface Props {
+  className?: string;
+  isMissingCondition?: boolean;
+  condition: Condition;
+  isCaycModal?: boolean;
+}
+
+export default function CaycStatusBadge({
+  className,
+  isMissingCondition,
+  condition,
+  isCaycModal,
+}: Props) {
+  if (isMissingCondition && !isCaycModal) {
+    return (
+      <Tooltip overlay={<CaycBadgeTooltip badgeType="missing" />}>
+        <div className={classNames('badge qg-cayc-missing-badge display-flex-center', className)}>
+          <AlertErrorIcon className="spacer-right" />
+          <span>{translate('quality_gates.cayc_condition.missing')}</span>
+        </div>
+      </Tooltip>
+    );
+  } else if (isCaycWeakCondition(condition) && !isCaycModal) {
+    return (
+      <Tooltip overlay={<CaycBadgeTooltip badgeType="weak" />}>
+        <div className={classNames('badge qg-cayc-weak-badge display-flex-center', className)}>
+          <AlertWarnIcon className="spacer-right" />
+          <span>{translate('quality_gates.cayc_condition.weak')}</span>
+        </div>
+      </Tooltip>
+    );
+  }
+  return (
+    <Tooltip overlay={<CaycBadgeTooltip badgeType="ok" />}>
+      <div className={classNames('badge qg-cayc-ok-badge display-flex-center', className)}>
+        <CheckIcon className="spacer-right" />
+        <span>{translate('quality_gates.cayc_condition.ok')}</span>
+      </div>
+    </Tooltip>
+  );
+}
index d9c328072d82253cbdb21c9e672e5f18690c860c..3458febd88ce9014d6bbf89c4768003d4e1eb44d 100644 (file)
@@ -23,11 +23,13 @@ import { deleteCondition } from '../../../api/quality-gates';
 import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
 import { DeleteButton, EditButton } from '../../../components/controls/buttons';
 import ConfirmModal from '../../../components/controls/ConfirmModal';
+import { Alert } from '../../../components/ui/Alert';
 import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
 import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
-import { getLocalizedMetricNameNoDiffMetric } from '../utils';
+import { getLocalizedMetricNameNoDiffMetric, isCaycCondition } from '../utils';
+import CaycStatusBadge from './CaycStatusBadge';
 import ConditionModal from './ConditionModal';
+import ConditionValue from './ConditionValue';
 
 interface Props {
   condition: ConditionType;
@@ -38,6 +40,9 @@ interface Props {
   qualityGate: QualityGate;
   updated?: boolean;
   metrics: Dict<Metric>;
+  showEdit?: boolean;
+  isMissingCondition?: boolean;
+  isCaycModal?: boolean;
 }
 
 interface State {
@@ -90,63 +95,106 @@ export class ConditionComponent extends React.PureComponent<Props, State> {
   }
 
   render() {
-    const { condition, canEdit, metric, qualityGate, updated, metrics } = this.props;
+    const {
+      condition,
+      canEdit,
+      metric,
+      qualityGate,
+      updated,
+      metrics,
+      showEdit = true,
+      isMissingCondition = false,
+      isCaycModal = false,
+    } = this.props;
+
     return (
       <tr className={classNames({ highlighted: updated })}>
-        <td className="text-middle">
+        <td
+          className={classNames('text-middle', {
+            'text-through': isMissingCondition && !isCaycModal,
+            'green-text': isMissingCondition && isCaycModal,
+          })}
+        >
           {getLocalizedMetricNameNoDiffMetric(metric, metrics)}
           {metric.hidden && (
             <span className="text-danger little-spacer-left">{translate('deprecated')}</span>
           )}
         </td>
 
-        <td className="text-middle nowrap">{this.renderOperator()}</td>
-
-        <td className="text-middle nowrap">{formatMeasure(condition.error, metric.type)}</td>
+        <td
+          className={classNames('text-middle nowrap', {
+            'text-through': isMissingCondition && !isCaycModal,
+            'green-text': isMissingCondition && isCaycModal,
+          })}
+        >
+          {this.renderOperator()}
+        </td>
 
-        {canEdit && (
-          <>
-            <td className="text-center thin">
+        <td
+          className={classNames('text-middle nowrap', {
+            'text-through': isMissingCondition && !isCaycModal,
+            'green-text': isMissingCondition && isCaycModal,
+          })}
+        >
+          <ConditionValue metric={metric} isCaycModal={isCaycModal} condition={condition} />
+        </td>
+        <td className="text-middle nowrap display-flex-justify-end">
+          {isCaycCondition(condition) && (
+            <CaycStatusBadge
+              isMissingCondition={isMissingCondition}
+              condition={condition}
+              isCaycModal={isCaycModal}
+              className="spacer-right"
+            />
+          )}
+          {canEdit && showEdit && !isMissingCondition && (
+            <>
               <EditButton
                 aria-label={translateWithParameters('quality_gates.condition.edit', metric.name)}
                 data-test="quality-gates__condition-update"
                 onClick={this.handleOpenUpdate}
+                className="spacer-right"
               />
-            </td>
-            <td className="text-center thin">
               <DeleteButton
                 aria-label={translateWithParameters('quality_gates.condition.delete', metric.name)}
                 data-test="quality-gates__condition-delete"
                 onClick={this.handleDeleteClick}
               />
-            </td>
-            {this.state.modal && (
-              <ConditionModal
-                condition={condition}
-                header={translate('quality_gates.update_condition')}
-                metric={metric}
-                onAddCondition={this.handleUpdateCondition}
-                onClose={this.handleUpdateClose}
-                qualityGate={qualityGate}
-              />
-            )}
-            {this.state.deleteFormOpen && (
-              <ConfirmModal
-                confirmButtonText={translate('delete')}
-                confirmData={condition}
-                header={translate('quality_gates.delete_condition')}
-                isDestructive={true}
-                onClose={this.closeDeleteForm}
-                onConfirm={this.removeCondition}
-              >
-                {translateWithParameters(
-                  'quality_gates.delete_condition.confirm.message',
-                  getLocalizedMetricName(this.props.metric)
-                )}
-              </ConfirmModal>
-            )}
-          </>
-        )}
+              {this.state.modal && (
+                <ConditionModal
+                  condition={condition}
+                  header={translate('quality_gates.update_condition')}
+                  metric={metric}
+                  onAddCondition={this.handleUpdateCondition}
+                  onClose={this.handleUpdateClose}
+                  qualityGate={qualityGate}
+                />
+              )}
+              {this.state.deleteFormOpen && (
+                <ConfirmModal
+                  confirmButtonText={translate('delete')}
+                  confirmData={condition}
+                  header={translate('quality_gates.delete_condition')}
+                  isDestructive={true}
+                  onClose={this.closeDeleteForm}
+                  onConfirm={this.removeCondition}
+                >
+                  {translateWithParameters(
+                    'quality_gates.delete_condition.confirm.message',
+                    getLocalizedMetricName(this.props.metric)
+                  )}
+                  {isCaycCondition(condition) && (
+                    <Alert className="big-spacer-bottom big-spacer-top" variant="warning">
+                      <p className="cayc-warning-description">
+                        {translate('quality_gates.cayc_condition.delete_warning')}
+                      </p>
+                    </Alert>
+                  )}
+                </ConfirmModal>
+              )}
+            </>
+          )}
+        </td>
       </tr>
     );
   }
index b542d00ea24e61c97495eb0bda7a28f2381f4bc7..5a5392db369d852ee95ca3a46706a79226259471 100644 (file)
@@ -25,7 +25,7 @@ import { Alert } from '../../../components/ui/Alert';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { isDiffMetric } from '../../../helpers/measures';
 import { Condition, Metric, QualityGate } from '../../../types/types';
-import { getPossibleOperators } from '../utils';
+import { getPossibleOperators, isCaycCondition, isCaycWeakCondition } from '../utils';
 import ConditionOperator from './ConditionOperator';
 import MetricSelect from './MetricSelect';
 import ThresholdInput from './ThresholdInput';
@@ -105,7 +105,7 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { header, metrics, onClose } = this.props;
+    const { header, metrics, onClose, condition } = this.props;
     const { op, error, scope, metric } = this.state;
     return (
       <ConfirmModal
@@ -138,6 +138,14 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
           </div>
         )}
 
+        {condition && isCaycCondition(condition) && !isCaycWeakCondition(condition) && (
+          <Alert className="big-spacer-bottom big-spacer-top" variant="warning">
+            <p className="cayc-warning-description">
+              {translate('quality_gates.cayc_condition.edit_warning')}
+            </p>
+          </Alert>
+        )}
+
         <div className="modal-field">
           <label htmlFor="condition-metric">
             {translate('quality_gates.conditions.fails_when')}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx
new file mode 100644 (file)
index 0000000..2d93b71
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { createCondition, updateCondition } from '../../../api/quality-gates';
+import ConfirmModal from '../../../components/controls/ConfirmModal';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Condition, Dict, Metric, QualityGate } from '../../../types/types';
+import { getCorrectCaycCondition, getWeakAndMissingConditions } from '../utils';
+import ConditionsTable from './ConditionsTable';
+
+interface Props {
+  canEdit: boolean;
+  metrics: Dict<Metric>;
+  updatedConditionId?: string;
+  conditions: Condition[];
+  scope: 'new' | 'overall' | 'new-cayc';
+  onClose: () => void;
+  onAddCondition: (condition: Condition) => void;
+  onRemoveCondition: (Condition: Condition) => void;
+  onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void;
+  qualityGate: QualityGate;
+}
+
+export default class CaycReviewUpdateConditionsModal extends React.PureComponent<Props> {
+  updateCaycQualityGate = () => {
+    const { conditions, qualityGate } = this.props;
+    const promiseArr: Promise<Condition | undefined>[] = [];
+    const { weakConditions, missingConditions } = getWeakAndMissingConditions(conditions);
+
+    weakConditions.forEach((condition) => {
+      promiseArr.push(
+        updateCondition({
+          ...getCorrectCaycCondition(condition),
+          id: condition.id,
+        }).catch(() => undefined)
+      );
+    });
+
+    missingConditions.forEach((condition) => {
+      promiseArr.push(
+        createCondition({
+          ...getCorrectCaycCondition(condition),
+          gateId: qualityGate.id,
+        }).catch(() => undefined)
+      );
+    });
+
+    return Promise.all(promiseArr).then((data) => {
+      data.forEach((condition) => {
+        if (condition === undefined) {
+          return;
+        }
+        const currentCondition = conditions.find((con) => con.metric === condition.metric);
+        if (currentCondition) {
+          this.props.onSaveCondition(condition, currentCondition);
+        } else {
+          this.props.onAddCondition(condition);
+        }
+      });
+    });
+  };
+
+  render() {
+    const { conditions, qualityGate } = this.props;
+    const { weakConditions, missingConditions } = getWeakAndMissingConditions(conditions);
+
+    return (
+      <ConfirmModal
+        header={translateWithParameters(
+          'quality_gates.cayc.review_update_modal.header',
+          qualityGate.name
+        )}
+        confirmButtonText={translate('quality_gates.cayc.review_update_modal.confirm_text')}
+        onClose={this.props.onClose}
+        onConfirm={this.updateCaycQualityGate}
+        size="medium"
+      >
+        <div className="quality-gate-section">
+          <p className="big-spacer-bottom">
+            {translate('quality_gates.cayc.review_update_modal.description')}
+          </p>
+
+          {weakConditions.length > 0 && (
+            <>
+              <h4 className="spacer-top spacer-bottom">
+                {translateWithParameters(
+                  'quality_gates.cayc.review_update_modal.modify_condition.header',
+                  weakConditions.length
+                )}
+              </h4>
+              <ConditionsTable
+                {...this.props}
+                conditions={weakConditions}
+                showEdit={false}
+                isCaycModal={true}
+              />
+            </>
+          )}
+
+          {missingConditions.length > 0 && (
+            <>
+              <h4 className="spacer-top spacer-bottom">
+                {translateWithParameters(
+                  'quality_gates.cayc.review_update_modal.add_condition.header',
+                  missingConditions.length
+                )}
+              </h4>
+              <ConditionsTable
+                {...this.props}
+                conditions={[]}
+                showEdit={false}
+                missingConditions={missingConditions}
+                isCaycModal={true}
+              />
+            </>
+          )}
+        </div>
+      </ConfirmModal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx
new file mode 100644 (file)
index 0000000..df286fa
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { formatMeasure } from '../../../helpers/measures';
+import { Condition, Metric } from '../../../types/types';
+import { getCorrectCaycCondition, isCaycCondition, isCaycWeakCondition } from '../utils';
+import ConditionValueDescription from './ConditionValueDescription';
+
+interface Props {
+  condition: Condition;
+  isCaycModal?: boolean;
+  metric: Metric;
+}
+
+function ConditionValue({ condition, isCaycModal, metric }: Props) {
+  if (isCaycModal) {
+    return (
+      <>
+        {isCaycWeakCondition(condition) && (
+          <span className="spacer-right text-through red-text">
+            {formatMeasure(condition.error, metric.type)}
+          </span>
+        )}
+        <span className="green-text spacer-right">
+          {formatMeasure(getCorrectCaycCondition(condition).error, metric.type)}
+        </span>
+
+        <ConditionValueDescription
+          condition={getCorrectCaycCondition(condition)}
+          metric={metric}
+          className="green-text"
+        />
+      </>
+    );
+  }
+
+  if (isCaycWeakCondition(condition)) {
+    return (
+      <>
+        <span className="spacer-right red-text">{formatMeasure(condition.error, metric.type)}</span>
+        <ConditionValueDescription condition={condition} metric={metric} />
+      </>
+    );
+  }
+
+  return (
+    <>
+      <span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span>
+      {isCaycCondition(condition) && (
+        <ConditionValueDescription condition={condition} metric={metric} />
+      )}
+    </>
+  );
+}
+
+export default ConditionValue;
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx
new file mode 100644 (file)
index 0000000..908ecc0
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import {
+  getMaintainabilityGrid,
+  GRID_INDEX_OFFSET,
+  PERCENT_MULTIPLIER,
+} from '../../../helpers/ratings';
+import { AppState } from '../../../types/appstate';
+import { GlobalSettingKeys } from '../../../types/settings';
+import { Condition, Metric } from '../../../types/types';
+import { isCaycCondition } from '../utils';
+
+interface Props {
+  appState: AppState;
+  condition: Condition;
+  metric: Metric;
+  className?: string;
+}
+
+function ConditionValueDescription({
+  condition,
+  appState: { settings },
+  metric,
+  className = '',
+}: Props) {
+  if (condition.metric === 'new_maintainability_rating') {
+    const maintainabilityGrid = getMaintainabilityGrid(
+      settings[GlobalSettingKeys.RatingGrid] ?? ''
+    );
+    const maintainabilityRatingThreshold =
+      maintainabilityGrid[Math.floor(Number(condition.error)) - GRID_INDEX_OFFSET];
+    const ratingLetter = formatMeasure(condition.error, 'RATING');
+
+    return (
+      <span className={className}>
+        (
+        {condition.error === '1'
+          ? translateWithParameters(
+              'quality_gates.cayc.new_maintainability_rating.A',
+              formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, 'PERCENT')
+            )
+          : translateWithParameters(
+              'quality_gates.cayc.new_maintainability_rating',
+              ratingLetter,
+              formatMeasure(maintainabilityRatingThreshold * PERCENT_MULTIPLIER, 'PERCENT')
+            )}
+        )
+      </span>
+    );
+  }
+
+  return (
+    <span className={className}>
+      {isCaycCondition(condition) && condition.metric !== 'new_security_hotspots_reviewed' && (
+        <>
+          (
+          {translate(
+            `quality_gates.cayc.${condition.metric}.${formatMeasure(condition.error, metric.type)}`
+          )}
+          )
+        </>
+      )}
+    </span>
+  );
+}
+
+export default withAppStateContext(ConditionValueDescription);
index 69bed8bb37380684ecd810b7960d5b372dad81bf..7b119a0ee175bfdeb4819bbf41b9f46182edb458 100644 (file)
@@ -32,8 +32,10 @@ import { isDiffMetric } from '../../../helpers/measures';
 import { Feature } from '../../../types/features';
 import { MetricKey } from '../../../types/metrics';
 import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
-import Condition from './Condition';
+import { getCaycConditions, getOthersConditions } from '../utils';
+import CaycConditions from './CaycConditions';
 import ConditionModal from './ConditionModal';
+import ConditionsTable from './ConditionsTable';
 
 interface Props extends WithAvailableFeaturesProps {
   canEdit: boolean;
@@ -65,56 +67,31 @@ export class Conditions extends React.PureComponent<Props> {
       updatedConditionId,
     } = this.props;
 
-    const captionTranslationId =
-      scope === 'new'
-        ? 'quality_gates.conditions.new_code'
-        : 'quality_gates.conditions.overall_code';
     return (
-      <table className="data zebra" data-test={`quality-gates__conditions-${scope}`}>
-        <caption>
-          <h4>{translate(captionTranslationId, 'long')}</h4>
-
-          {this.props.hasFeature(Feature.BranchSupport) && (
-            <p className="spacer-top spacer-bottom">
-              {translate(captionTranslationId, 'description')}
-            </p>
-          )}
-        </caption>
-        <thead>
-          <tr>
-            <th className="nowrap" style={{ width: 300 }}>
-              {translate('quality_gates.conditions.metric')}
-            </th>
-            <th className="nowrap">{translate('quality_gates.conditions.operator')}</th>
-            <th className="nowrap">{translate('quality_gates.conditions.value')}</th>
-            {canEdit && (
-              <>
-                <th className="thin">{translate('edit')}</th>
-                <th className="thin">{translate('delete')}</th>
-              </>
-            )}
-          </tr>
-        </thead>
-        <tbody>
-          {conditions.map((condition) => (
-            <Condition
-              canEdit={canEdit}
-              condition={condition}
-              key={condition.id}
-              metric={metrics[condition.metric]}
-              onRemoveCondition={onRemoveCondition}
-              onSaveCondition={onSaveCondition}
-              qualityGate={qualityGate}
-              updated={condition.id === updatedConditionId}
-            />
-          ))}
-        </tbody>
-      </table>
+      <ConditionsTable
+        qualityGate={qualityGate}
+        metrics={metrics}
+        canEdit={canEdit}
+        onRemoveCondition={onRemoveCondition}
+        onSaveCondition={onSaveCondition}
+        updatedConditionId={updatedConditionId}
+        conditions={getOthersConditions(conditions)}
+        scope={scope}
+      />
     );
   };
 
   render() {
-    const { conditions, metrics, canEdit } = this.props;
+    const {
+      qualityGate,
+      metrics,
+      canEdit,
+      onAddCondition,
+      onRemoveCondition,
+      onSaveCondition,
+      updatedConditionId,
+      conditions,
+    } = this.props;
 
     const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
     const sortedConditions = sortBy(
@@ -180,8 +157,8 @@ export class Conditions extends React.PureComponent<Props> {
           </div>
         )}
 
-        <header className="display-flex-center spacer-bottom">
-          <h3>{translate('quality_gates.conditions')}</h3>
+        <header className="display-flex-center">
+          <h2 className="big">{translate('quality_gates.conditions')}</h2>
           <DocumentationTooltip
             className="spacer-left"
             content={translate('quality_gates.conditions.help')}
@@ -207,13 +184,63 @@ export class Conditions extends React.PureComponent<Props> {
 
         {sortedConditionsOnNewMetrics.length > 0 && (
           <div className="big-spacer-top">
-            {this.renderConditionsTable(sortedConditionsOnNewMetrics, 'new')}
+            <h3 className="medium text-normal">
+              {translate('quality_gates.conditions.new_code', 'long')}
+            </h3>
+            {this.props.hasFeature(Feature.BranchSupport) && (
+              <p className="spacer-top spacer-bottom">
+                {translate('quality_gates.conditions.new_code', 'description')}
+              </p>
+            )}
+
+            <CaycConditions
+              qualityGate={qualityGate}
+              metrics={metrics}
+              canEdit={canEdit}
+              onRemoveCondition={onRemoveCondition}
+              onAddCondition={onAddCondition}
+              onSaveCondition={onSaveCondition}
+              updatedConditionId={updatedConditionId}
+              conditions={getCaycConditions(sortedConditionsOnNewMetrics)}
+              scope="new-cayc"
+            />
+
+            <h4>{translate('quality_gates.other_conditions')}</h4>
+            <ConditionsTable
+              qualityGate={qualityGate}
+              metrics={metrics}
+              canEdit={canEdit}
+              onRemoveCondition={onRemoveCondition}
+              onSaveCondition={onSaveCondition}
+              updatedConditionId={updatedConditionId}
+              conditions={getOthersConditions(sortedConditionsOnNewMetrics)}
+              scope="new"
+            />
           </div>
         )}
 
         {sortedConditionsOnOverallMetrics.length > 0 && (
           <div className="big-spacer-top">
-            {this.renderConditionsTable(sortedConditionsOnOverallMetrics, 'overall')}
+            <h3 className="medium text-normal">
+              {translate('quality_gates.conditions.overall_code', 'long')}
+            </h3>
+
+            {this.props.hasFeature(Feature.BranchSupport) && (
+              <p className="spacer-top spacer-bottom">
+                {translate('quality_gates.conditions.overall_code', 'description')}
+              </p>
+            )}
+
+            <ConditionsTable
+              qualityGate={qualityGate}
+              metrics={metrics}
+              canEdit={canEdit}
+              onRemoveCondition={onRemoveCondition}
+              onSaveCondition={onSaveCondition}
+              updatedConditionId={updatedConditionId}
+              conditions={sortedConditionsOnOverallMetrics}
+              scope="overall"
+            />
           </div>
         )}
 
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx
new file mode 100644 (file)
index 0000000..bdacfd4
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { translate } from '../../../helpers/l10n';
+import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
+import Condition from './Condition';
+
+interface Props {
+  canEdit: boolean;
+  metrics: Dict<Metric>;
+  onRemoveCondition: (Condition: ConditionType) => void;
+  onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
+  qualityGate: QualityGate;
+  updatedConditionId?: string;
+  conditions: ConditionType[];
+  missingConditions?: ConditionType[];
+  scope: 'new' | 'overall' | 'new-cayc';
+  showEdit?: boolean;
+  isCaycModal?: boolean;
+}
+
+export default class ConditionsTable extends React.PureComponent<Props> {
+  render() {
+    const {
+      qualityGate,
+      metrics,
+      canEdit,
+      onRemoveCondition,
+      onSaveCondition,
+      updatedConditionId,
+      scope,
+      conditions,
+      missingConditions,
+      showEdit,
+      isCaycModal,
+    } = this.props;
+
+    return (
+      <table
+        className="data zebra"
+        data-test={`quality-gates__conditions-${scope}`}
+        data-testid={`quality-gates__conditions-${scope}`}
+      >
+        <thead>
+          <tr>
+            <th className="nowrap abs-width-300">{translate('quality_gates.conditions.metric')}</th>
+            <th className="nowrap">{translate('quality_gates.conditions.operator')}</th>
+            <th className="nowrap">{translate('quality_gates.conditions.value')}</th>
+            <th className="thin">&nbsp;</th>
+          </tr>
+        </thead>
+        <tbody>
+          {conditions.map((condition) => (
+            <Condition
+              canEdit={canEdit}
+              condition={condition}
+              key={condition.id}
+              metric={metrics[condition.metric]}
+              onRemoveCondition={onRemoveCondition}
+              onSaveCondition={onSaveCondition}
+              qualityGate={qualityGate}
+              updated={condition.id === updatedConditionId}
+              showEdit={showEdit}
+              isCaycModal={isCaycModal}
+            />
+          ))}
+
+          {missingConditions &&
+            missingConditions.map((condition) => (
+              <Condition
+                canEdit={canEdit}
+                condition={condition}
+                key={condition.id}
+                metric={metrics[condition.metric]}
+                onRemoveCondition={onRemoveCondition}
+                onSaveCondition={onSaveCondition}
+                qualityGate={qualityGate}
+                updated={condition.id === updatedConditionId}
+                showEdit={showEdit}
+                isMissingCondition={true}
+                isCaycModal={isCaycModal}
+              />
+            ))}
+        </tbody>
+      </table>
+    );
+  }
+}
index 3816836633932b43b978afa67ca2f1f1cf7de806..82a8b06001b1b4d35a0e738e5ea25e607e42d40e 100644 (file)
@@ -39,7 +39,9 @@ export default function List({ qualityGates }: Props) {
           key={qualityGate.id}
           to={getQualityGateUrl(String(qualityGate.id))}
         >
-          <span className="flex-1">{qualityGate.name}</span>
+          <span className="flex-1 text-ellipsis" title={qualityGate.name}>
+            {qualityGate.name}
+          </span>
           {qualityGate.isDefault && (
             <span className="badge little-spacer-left">{translate('default')}</span>
           )}
index 1675f373d5eae4139d2396a73f66eff6453a4ff7..8a5ff197b015e7b7f3cf5f3b4c945d9103bc2d4f 100644 (file)
@@ -174,6 +174,7 @@ export default class Projects extends React.PureComponent<Props, State> {
         renderElement={this.renderElement}
         selectedElements={this.state.selectedProjects}
         withPaging={true}
+        autoFocusSearch={false}
       />
     );
   }
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx
deleted file mode 100644 (file)
index da96467..0000000
+++ /dev/null
@@ -1,493 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { screen, waitFor, within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import selectEvent from 'react-select-event';
-import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
-import { searchProjects, searchUsers } from '../../../../api/quality-gates';
-import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils';
-import { Feature } from '../../../../types/features';
-import routes from '../../routes';
-
-jest.mock('../../../../api/quality-gates');
-
-let handler: QualityGatesServiceMock;
-
-beforeAll(() => {
-  handler = new QualityGatesServiceMock();
-});
-
-afterEach(() => handler.reset());
-
-it('should open the default quality gates', async () => {
-  renderQualityGateApp();
-
-  expect(await screen.findAllByRole('menuitem')).toHaveLength(handler.list.length);
-
-  const defaultQualityGate = handler.getDefaultQualityGate();
-  expect(await screen.findAllByText(defaultQualityGate.name)).toHaveLength(2);
-});
-
-it('should list all quality gates', async () => {
-  renderQualityGateApp();
-
-  expect(
-    await screen.findByRole('menuitem', {
-      name: `${handler.getDefaultQualityGate().name} default`,
-    })
-  ).toBeInTheDocument();
-  expect(
-    await screen.findByRole('menuitem', {
-      name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
-    })
-  ).toBeInTheDocument();
-});
-
-it('should be able to create a quality gate then delete it', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-  let createButton = await screen.findByRole('button', { name: 'create' });
-
-  // Using keyboard
-  await user.click(createButton);
-  let nameInput = screen.getByRole('textbox', { name: /name.*/ });
-  expect(nameInput).toBeInTheDocument();
-  await user.click(nameInput);
-  await user.keyboard('testone{Enter}');
-  expect(await screen.findByRole('menuitem', { name: 'testone' })).toBeInTheDocument();
-
-  // Using modal button
-  createButton = await screen.findByRole('button', { name: 'create' });
-  await user.click(createButton);
-  nameInput = screen.getByRole('textbox', { name: /name.*/ });
-  const saveButton = screen.getByRole('button', { name: 'save' });
-
-  expect(saveButton).toBeDisabled();
-  await user.click(nameInput);
-  await user.keyboard('testtwo');
-  await user.click(saveButton);
-
-  const newQG = await screen.findByRole('menuitem', { name: 'testtwo' });
-  expect(newQG).toBeInTheDocument();
-
-  // Delete the quality gate
-  await user.click(newQG);
-  const deleteButton = await screen.findByRole('button', { name: 'delete' });
-  await user.click(deleteButton);
-  const popup = screen.getByRole('dialog');
-  const dialogDeleteButton = within(popup).getByRole('button', { name: 'delete' });
-  await user.click(dialogDeleteButton);
-
-  await waitFor(() => {
-    expect(screen.queryByRole('menuitem', { name: 'testtwo' })).not.toBeInTheDocument();
-  });
-});
-
-it('should be able to copy a quality gate', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-
-  const copyButton = await screen.findByRole('button', { name: 'copy' });
-
-  await user.click(copyButton);
-  const nameInput = screen.getByRole('textbox', { name: /name.*/ });
-  expect(nameInput).toBeInTheDocument();
-  await user.click(nameInput);
-  await user.keyboard(' bis{Enter}');
-
-  expect(await screen.findByRole('menuitem', { name: /.* bis/ })).toBeInTheDocument();
-});
-
-it('should be able to rename a quality gate', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-
-  const renameButton = await screen.findByRole('button', { name: 'rename' });
-
-  await user.click(renameButton);
-  const nameInput = screen.getByRole('textbox', { name: /name.*/ });
-  expect(nameInput).toBeInTheDocument();
-  await user.click(nameInput);
-  await user.keyboard('{Control>}a{/Control}New Name{Enter}');
-
-  expect(await screen.findByRole('menuitem', { name: /New Name.*/ })).toBeInTheDocument();
-});
-
-it('should be able to set as default a quality gate', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-
-  const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-  await user.click(notDefaultQualityGate);
-  const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
-  await user.click(setAsDefaultButton);
-  expect(screen.getAllByRole('menuitem')[1]).toHaveTextContent('default');
-});
-
-it('should be able to add a condition', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-
-  // On new code
-  await user.click(await screen.findByText('quality_gates.add_condition'));
-
-  let dialog = within(screen.getByRole('dialog'));
-
-  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' }));
-  await selectEvent.select(dialog.getByRole('textbox'), ['Issues']);
-  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
-  await user.keyboard('12{Enter}');
-
-  const newConditions = within(
-    await screen.findByRole('table', { name: 'quality_gates.conditions.new_code.long' })
-  );
-  expect(await newConditions.findByRole('cell', { name: 'Issues' })).toBeInTheDocument();
-  expect(await newConditions.findByRole('cell', { name: '12' })).toBeInTheDocument();
-
-  // On overall code
-  await user.click(await screen.findByText('quality_gates.add_condition'));
-
-  dialog = within(screen.getByRole('dialog'));
-  await selectEvent.select(dialog.getByRole('textbox'), ['Info Issues']);
-  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
-  await user.click(dialog.getByLabelText('quality_gates.conditions.operator'));
-
-  await user.click(dialog.getByText('quality_gates.operator.LT'));
-  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
-  await user.keyboard('42{Enter}');
-
-  let overallConditions = within(
-    await screen.findByRole('table', { name: 'quality_gates.conditions.overall_code.long' })
-  );
-
-  expect(await overallConditions.findByRole('cell', { name: 'Info Issues' })).toBeInTheDocument();
-  expect(await overallConditions.findByRole('cell', { name: '42' })).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('textbox'), ['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' }));
-
-  overallConditions = within(
-    await screen.findByRole('table', { name: 'quality_gates.conditions.overall_code.long' })
-  );
-
-  expect(
-    await overallConditions.findByRole('cell', { name: 'Maintainability Rating' })
-  ).toBeInTheDocument();
-  expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument();
-});
-
-it('should be able to edit a condition', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-
-  const newConditions = within(
-    await screen.findByRole('table', {
-      name: 'quality_gates.conditions.new_code.long',
-    })
-  );
-
-  await user.click(
-    newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code')
-  );
-  const dialog = within(screen.getByRole('dialog'));
-  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
-  await user.keyboard('{Backspace}{Backspace}23{Enter}');
-
-  expect(await newConditions.findByText('Coverage')).toBeInTheDocument();
-  expect(await newConditions.findByText('23.0%')).toBeInTheDocument();
-});
-
-it('should be able to handle duplicate or deprecated condition', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-  await user.click(
-    await screen.findByRole('menuitem', { name: handler.getCorruptedQualityGateName() })
-  );
-
-  expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
-  expect(
-    await screen.findByRole('cell', { name: 'Complexity / Function deprecated' })
-  ).toBeInTheDocument();
-});
-
-it('should be able to handle delete condition', async () => {
-  const user = userEvent.setup();
-  handler.setIsAdmin(true);
-  renderQualityGateApp();
-
-  const newConditions = within(
-    await screen.findByRole('table', {
-      name: 'quality_gates.conditions.new_code.long',
-    })
-  );
-
-  await user.click(
-    newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code')
-  );
-
-  const dialog = within(screen.getByRole('dialog'));
-  await user.click(dialog.getByRole('button', { name: 'delete' }));
-
-  await waitFor(() => {
-    expect(newConditions.queryByRole('cell', { name: 'Coverage' })).not.toBeInTheDocument();
-  });
-});
-
-it('should explain condition on branch', async () => {
-  renderQualityGateApp({ featureList: [Feature.BranchSupport] });
-
-  expect(
-    await screen.findByText('quality_gates.conditions.new_code.description')
-  ).toBeInTheDocument();
-  expect(
-    await screen.findByText('quality_gates.conditions.overall_code.description')
-  ).toBeInTheDocument();
-});
-
-describe('The Project section', () => {
-  it('should render list of projects correctly in different tabs', async () => {
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-
-    await user.click(notDefaultQualityGate);
-
-    // by default it shows "selected" values
-    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
-
-    // change tabs to show deselected projects
-    await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
-    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
-
-    // change tabs to show all projects
-    await user.click(screen.getByRole('button', { name: 'quality_gates.projects.all' }));
-    expect(screen.getAllByRole('checkbox')).toHaveLength(4);
-  });
-
-  it('should handle select and deselect correctly', async () => {
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-
-    await user.click(notDefaultQualityGate);
-
-    const checkedProjects = screen.getAllByRole('checkbox')[0];
-    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
-    await user.click(checkedProjects);
-    const reloadButton = screen.getByRole('button', { name: 'reload' });
-    expect(reloadButton).toBeInTheDocument();
-    await user.click(reloadButton);
-
-    // FP
-    // eslint-disable-next-line jest-dom/prefer-in-document
-    expect(screen.getAllByRole('checkbox')).toHaveLength(1);
-
-    // change tabs to show deselected projects
-    await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
-
-    const uncheckedProjects = screen.getAllByRole('checkbox')[0];
-    expect(screen.getAllByRole('checkbox')).toHaveLength(3);
-    await user.click(uncheckedProjects);
-    expect(reloadButton).toBeInTheDocument();
-    await user.click(reloadButton);
-    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
-  });
-
-  it('should handle the search of projects', async () => {
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-
-    await user.click(notDefaultQualityGate);
-
-    const searchInput = screen.getByRole('searchbox', { name: 'search_verb' });
-    expect(searchInput).toBeInTheDocument();
-    await user.click(searchInput);
-    await user.keyboard('test2{Enter}');
-
-    // FP
-    // eslint-disable-next-line jest-dom/prefer-in-document
-    expect(screen.getAllByRole('checkbox')).toHaveLength(1);
-  });
-
-  it('should display show more button if there are multiple pages of data', async () => {
-    (searchProjects as jest.Mock).mockResolvedValueOnce({
-      paging: { pageIndex: 2, pageSize: 3, total: 55 },
-      results: [],
-    });
-
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-    await user.click(notDefaultQualityGate);
-
-    expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument();
-  });
-});
-
-describe('The Permissions section', () => {
-  it('should not show button to grant permission when user is not admin', async () => {
-    renderQualityGateApp();
-
-    // await just to make sure we've loaded the page
-    expect(
-      await screen.findByRole('menuitem', {
-        name: `${handler.getDefaultQualityGate().name} default`,
-      })
-    ).toBeInTheDocument();
-
-    expect(screen.queryByText('quality_gates.permissions')).not.toBeInTheDocument();
-  });
-  it('should show button to grant permission when user is admin', async () => {
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    const grantPermissionButton = await screen.findByRole('button', {
-      name: 'quality_gates.permissions.grant',
-    });
-    expect(screen.getByText('quality_gates.permissions')).toBeInTheDocument();
-    expect(grantPermissionButton).toBeInTheDocument();
-  });
-
-  it('should assign permission to a user and delete it later', async () => {
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
-
-    // Granting permission to a user
-    const grantPermissionButton = await screen.findByRole('button', {
-      name: 'quality_gates.permissions.grant',
-    });
-    await user.click(grantPermissionButton);
-    const popup = screen.getByRole('dialog');
-    const searchUserInput = within(popup).getByRole('textbox');
-    expect(searchUserInput).toBeInTheDocument();
-    const addUserButton = screen.getByRole('button', {
-      name: 'add_verb',
-    });
-    expect(addUserButton).toBeDisabled();
-    await user.click(searchUserInput);
-    expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
-    await user.click(screen.getByText('userlogin'));
-    expect(addUserButton).toBeEnabled();
-    await user.click(addUserButton);
-    expect(screen.getByText('userlogin')).toBeInTheDocument();
-
-    // Cancel granting permission
-    await user.click(grantPermissionButton);
-    await user.click(searchUserInput);
-    await user.keyboard('test{Enter}');
-
-    const cancelButton = screen.getByRole('button', {
-      name: 'cancel',
-    });
-    await user.click(cancelButton);
-
-    // FP
-    // eslint-disable-next-line jest-dom/prefer-in-document
-    expect(screen.getAllByRole('listitem')).toHaveLength(1);
-
-    // Delete the user permission
-    const deleteButton = screen.getByTestId('permission-delete-button');
-    await user.click(deleteButton);
-    const deletePopup = screen.getByRole('dialog');
-    const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
-    await user.click(dialogDeleteButton);
-    expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
-  });
-
-  it('should assign permission to a group and delete it later', async () => {
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
-
-    // Granting permission to a group
-    const grantPermissionButton = await screen.findByRole('button', {
-      name: 'quality_gates.permissions.grant',
-    });
-    await user.click(grantPermissionButton);
-    const popup = screen.getByRole('dialog');
-    const searchUserInput = within(popup).getByRole('textbox');
-    const addUserButton = screen.getByRole('button', {
-      name: 'add_verb',
-    });
-    await user.click(searchUserInput);
-    expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
-    await user.click(screen.getAllByTestId('qg-add-permission-option')[1]);
-    await user.click(addUserButton);
-    expect(screen.getByText('Foo')).toBeInTheDocument();
-
-    // Delete the group permission
-    const deleteButton = screen.getByTestId('permission-delete-button');
-    await user.click(deleteButton);
-    const deletePopup = screen.getByRole('dialog');
-    const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
-    await user.click(dialogDeleteButton);
-    expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
-  });
-
-  it('should handle searchUser service failure', async () => {
-    (searchUsers as jest.Mock).mockRejectedValue('error');
-
-    const user = userEvent.setup();
-    handler.setIsAdmin(true);
-    renderQualityGateApp();
-
-    const grantPermissionButton = await screen.findByRole('button', {
-      name: 'quality_gates.permissions.grant',
-    });
-    await user.click(grantPermissionButton);
-    const popup = screen.getByRole('dialog');
-    const searchUserInput = within(popup).getByRole('textbox');
-    await user.click(searchUserInput);
-
-    expect(screen.getByText('no_results')).toBeInTheDocument();
-  });
-});
-
-function renderQualityGateApp(context?: RenderContext) {
-  renderAppRoutes('quality_gates', routes, context);
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
new file mode 100644 (file)
index 0000000..786f53c
--- /dev/null
@@ -0,0 +1,585 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import selectEvent from 'react-select-event';
+import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
+import { searchProjects, searchUsers } from '../../../../api/quality-gates';
+import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../../types/features';
+import routes from '../../routes';
+
+jest.mock('../../../../api/quality-gates');
+
+let handler: QualityGatesServiceMock;
+
+beforeAll(() => {
+  handler = new QualityGatesServiceMock();
+});
+
+afterEach(() => handler.reset());
+
+it('should open the default quality gates', async () => {
+  renderQualityGateApp();
+
+  expect(await screen.findAllByRole('menuitem')).toHaveLength(handler.list.length);
+
+  const defaultQualityGate = handler.getDefaultQualityGate();
+  expect(await screen.findAllByText(defaultQualityGate.name)).toHaveLength(2);
+});
+
+it('should list all quality gates', async () => {
+  renderQualityGateApp();
+
+  expect(
+    await screen.findByRole('menuitem', {
+      name: `${handler.getDefaultQualityGate().name} default`,
+    })
+  ).toBeInTheDocument();
+  expect(
+    await screen.findByRole('menuitem', {
+      name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
+    })
+  ).toBeInTheDocument();
+});
+
+it('should be able to create a quality gate then delete it', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+  let createButton = await screen.findByRole('button', { name: 'create' });
+
+  // Using keyboard
+  await user.click(createButton);
+  let nameInput = screen.getByRole('textbox', { name: /name.*/ });
+  expect(nameInput).toBeInTheDocument();
+  await user.click(nameInput);
+  await user.keyboard('testone{Enter}');
+  expect(await screen.findByRole('menuitem', { name: 'testone' })).toBeInTheDocument();
+
+  // Using modal button
+  createButton = await screen.findByRole('button', { name: 'create' });
+  await user.click(createButton);
+  nameInput = screen.getByRole('textbox', { name: /name.*/ });
+  const saveButton = screen.getByRole('button', { name: 'save' });
+
+  expect(saveButton).toBeDisabled();
+  await user.click(nameInput);
+  await user.keyboard('testtwo');
+  await user.click(saveButton);
+
+  const newQG = await screen.findByRole('menuitem', { name: 'testtwo' });
+  expect(newQG).toBeInTheDocument();
+
+  // Delete the quality gate
+  await user.click(newQG);
+  const deleteButton = await screen.findByRole('button', { name: 'delete' });
+  await user.click(deleteButton);
+  const popup = screen.getByRole('dialog');
+  const dialogDeleteButton = within(popup).getByRole('button', { name: 'delete' });
+  await user.click(dialogDeleteButton);
+
+  await waitFor(() => {
+    expect(screen.queryByRole('menuitem', { name: 'testtwo' })).not.toBeInTheDocument();
+  });
+});
+
+it('should be able to copy a quality gate', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const copyButton = await screen.findByRole('button', { name: 'copy' });
+
+  await user.click(copyButton);
+  const nameInput = screen.getByRole('textbox', { name: /name.*/ });
+  expect(nameInput).toBeInTheDocument();
+  await user.click(nameInput);
+  await user.keyboard(' bis{Enter}');
+
+  expect(await screen.findByRole('menuitem', { name: /.* bis/ })).toBeInTheDocument();
+});
+
+it('should be able to rename a quality gate', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const renameButton = await screen.findByRole('button', { name: 'rename' });
+
+  await user.click(renameButton);
+  const nameInput = screen.getByRole('textbox', { name: /name.*/ });
+  expect(nameInput).toBeInTheDocument();
+  await user.click(nameInput);
+  await user.keyboard('{Control>}a{/Control}New Name{Enter}');
+
+  expect(await screen.findByRole('menuitem', { name: /New Name.*/ })).toBeInTheDocument();
+});
+
+it('should be able to set as default a quality gate', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+  await user.click(notDefaultQualityGate);
+  const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
+  await user.click(setAsDefaultButton);
+  expect(screen.getAllByRole('menuitem')[1]).toHaveTextContent('default');
+});
+
+it('should be able to add a condition', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  // On new code
+  await user.click(await screen.findByText('quality_gates.add_condition'));
+
+  let dialog = within(screen.getByRole('dialog'));
+
+  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' }));
+  await selectEvent.select(dialog.getByRole('textbox'), ['Issues']);
+  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+  await user.keyboard('12{Enter}');
+
+  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();
+
+  // On overall code
+  await user.click(await screen.findByText('quality_gates.add_condition'));
+
+  dialog = within(screen.getByRole('dialog'));
+  await selectEvent.select(dialog.getByRole('textbox'), ['Info Issues']);
+  await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
+  await user.click(dialog.getByLabelText('quality_gates.conditions.operator'));
+
+  await user.click(dialog.getByText('quality_gates.operator.LT'));
+  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+  await user.keyboard('42{Enter}');
+
+  const overallConditions = within(await screen.findByTestId('quality-gates__conditions-overall'));
+
+  expect(await overallConditions.findByRole('cell', { name: 'Info Issues' })).toBeInTheDocument();
+  expect(await overallConditions.findByRole('cell', { name: '42' })).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('textbox'), ['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' }));
+
+  expect(
+    await overallConditions.findByRole('cell', { name: 'Maintainability Rating' })
+  ).toBeInTheDocument();
+  expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument();
+});
+
+it('should be able to edit a condition', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+
+  await user.click(
+    newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code')
+  );
+  const dialog = within(screen.getByRole('dialog'));
+  await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+  await user.keyboard('{Backspace}{Backspace}23{Enter}');
+
+  expect(await newConditions.findByText('Coverage')).toBeInTheDocument();
+  expect(await newConditions.findByText('23.0%')).toBeInTheDocument();
+});
+
+it('should be able to handle duplicate or deprecated condition', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+  await user.click(
+    await screen.findByRole('menuitem', { name: handler.getCorruptedQualityGateName() })
+  );
+
+  expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
+  expect(
+    await screen.findByRole('cell', { name: 'Complexity / Function deprecated' })
+  ).toBeInTheDocument();
+});
+
+it('should be able to handle delete condition', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+
+  await user.click(
+    newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code')
+  );
+
+  const dialog = within(screen.getByRole('dialog'));
+  await user.click(dialog.getByRole('button', { name: 'delete' }));
+
+  await waitFor(() => {
+    expect(newConditions.queryByRole('cell', { name: 'Coverage' })).not.toBeInTheDocument();
+  });
+});
+
+it('should explain condition on branch', async () => {
+  renderQualityGateApp({ featureList: [Feature.BranchSupport] });
+
+  expect(
+    await screen.findByText('quality_gates.conditions.new_code.description')
+  ).toBeInTheDocument();
+  expect(
+    await screen.findByText('quality_gates.conditions.overall_code.description')
+  ).toBeInTheDocument();
+});
+
+it('should be able to see warning when CAYC condition is not properly set and update them', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const qualityGate = await screen.findByText('SonarSource way - CFamily');
+
+  await user.click(qualityGate);
+
+  expect(
+    screen.getByText('quality_gates.cayc_condition.missing_warning.title')
+  ).toBeInTheDocument();
+  expect(screen.getByText('quality_gates.other_conditions')).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+  ).toBeInTheDocument();
+  expect(await screen.findAllByText('quality_gates.cayc_condition.missing')).toHaveLength(4);
+
+  await user.click(
+    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+  );
+  expect(
+    screen.getByRole('dialog', {
+      name: 'quality_gates.cayc.review_update_modal.header.SonarSource way - CFamily',
+    })
+  ).toBeInTheDocument();
+  expect(
+    screen.getByText('quality_gates.cayc.review_update_modal.description')
+  ).toBeInTheDocument();
+  expect(
+    screen.getByText('quality_gates.cayc.review_update_modal.add_condition.header.4')
+  ).toBeInTheDocument();
+  expect(await screen.findAllByText('quality_gates.cayc_condition.ok')).toHaveLength(4);
+  expect(
+    screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' })
+  ).toBeInTheDocument();
+
+  await user.click(
+    screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' })
+  );
+
+  const newCaycConditions = within(await screen.findByTestId('quality-gates__conditions-new-cayc'));
+  expect(await newCaycConditions.findAllByText('quality_gates.cayc_condition.ok')).toHaveLength(4);
+});
+
+it('should not show any warning when CAYC condition are properly set', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const qualityGate = await screen.findByText('SonarSource way');
+
+  await user.click(qualityGate);
+
+  expect(
+    screen.queryByText('quality_gates.cayc_condition.missing_warning.title')
+  ).not.toBeInTheDocument();
+  expect(screen.getByText('quality_gates.other_conditions')).toBeInTheDocument();
+  expect(
+    screen.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+  ).not.toBeInTheDocument();
+
+  const newCaycConditions = within(await screen.findByTestId('quality-gates__conditions-new-cayc'));
+
+  expect(await newCaycConditions.findByText('Maintainability Rating')).toBeInTheDocument();
+  expect(await newCaycConditions.findByText('Reliability Rating')).toBeInTheDocument();
+  expect(await newCaycConditions.findByText('Security Hotspots Reviewed')).toBeInTheDocument();
+  expect(await newCaycConditions.findByText('Security Rating')).toBeInTheDocument();
+  expect(await newCaycConditions.findAllByText('quality_gates.cayc_condition.ok')).toHaveLength(4);
+
+  const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+
+  expect(await newConditions.findByText('Coverage')).toBeInTheDocument();
+  expect(
+    newConditions.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+  ).not.toBeInTheDocument();
+});
+
+it('should unlock editing option for CAYC conditions', async () => {
+  const user = userEvent.setup();
+  handler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const qualityGate = await screen.findByText('SonarSource way');
+  await user.click(qualityGate);
+  expect(screen.getByText('quality_gates.cayc.unlock_edit')).toBeInTheDocument();
+  expect(
+    screen.queryByRole('button', {
+      name: 'quality_gates.condition.edit.Security Rating on New Code',
+    })
+  ).not.toBeInTheDocument();
+  expect(
+    screen.queryByRole('button', {
+      name: 'quality_gates.condition.delete.Security Rating on New Code',
+    })
+  ).not.toBeInTheDocument();
+
+  await user.click(screen.getByText('quality_gates.cayc.unlock_edit'));
+  expect(
+    screen.getByRole('button', { name: 'quality_gates.condition.edit.Security Rating on New Code' })
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', {
+      name: 'quality_gates.condition.delete.Security Rating on New Code',
+    })
+  ).toBeInTheDocument();
+});
+
+describe('The Project section', () => {
+  it('should render list of projects correctly in different tabs', async () => {
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+
+    await user.click(notDefaultQualityGate);
+
+    // by default it shows "selected" values
+    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+
+    // change tabs to show deselected projects
+    await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
+    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+
+    // change tabs to show all projects
+    await user.click(screen.getByRole('button', { name: 'quality_gates.projects.all' }));
+    expect(screen.getAllByRole('checkbox')).toHaveLength(4);
+  });
+
+  it('should handle select and deselect correctly', async () => {
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+
+    await user.click(notDefaultQualityGate);
+
+    const checkedProjects = screen.getAllByRole('checkbox')[0];
+    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+    await user.click(checkedProjects);
+    const reloadButton = screen.getByRole('button', { name: 'reload' });
+    expect(reloadButton).toBeInTheDocument();
+    await user.click(reloadButton);
+
+    // FP
+    // eslint-disable-next-line jest-dom/prefer-in-document
+    expect(screen.getAllByRole('checkbox')).toHaveLength(1);
+
+    // change tabs to show deselected projects
+    await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
+
+    const uncheckedProjects = screen.getAllByRole('checkbox')[0];
+    expect(screen.getAllByRole('checkbox')).toHaveLength(3);
+    await user.click(uncheckedProjects);
+    expect(reloadButton).toBeInTheDocument();
+    await user.click(reloadButton);
+    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+  });
+
+  it('should handle the search of projects', async () => {
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+
+    await user.click(notDefaultQualityGate);
+
+    const searchInput = screen.getByRole('searchbox', { name: 'search_verb' });
+    expect(searchInput).toBeInTheDocument();
+    await user.click(searchInput);
+    await user.keyboard('test2{Enter}');
+
+    // FP
+    // eslint-disable-next-line jest-dom/prefer-in-document
+    expect(screen.getAllByRole('checkbox')).toHaveLength(1);
+  });
+
+  it('should display show more button if there are multiple pages of data', async () => {
+    (searchProjects as jest.Mock).mockResolvedValueOnce({
+      paging: { pageIndex: 2, pageSize: 3, total: 55 },
+      results: [],
+    });
+
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+    await user.click(notDefaultQualityGate);
+
+    expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument();
+  });
+});
+
+describe('The Permissions section', () => {
+  it('should not show button to grant permission when user is not admin', async () => {
+    renderQualityGateApp();
+
+    // await just to make sure we've loaded the page
+    expect(
+      await screen.findByRole('menuitem', {
+        name: `${handler.getDefaultQualityGate().name} default`,
+      })
+    ).toBeInTheDocument();
+
+    expect(screen.queryByText('quality_gates.permissions')).not.toBeInTheDocument();
+  });
+  it('should show button to grant permission when user is admin', async () => {
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    const grantPermissionButton = await screen.findByRole('button', {
+      name: 'quality_gates.permissions.grant',
+    });
+    expect(screen.getByText('quality_gates.permissions')).toBeInTheDocument();
+    expect(grantPermissionButton).toBeInTheDocument();
+  });
+
+  it('should assign permission to a user and delete it later', async () => {
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
+
+    // Granting permission to a user
+    const grantPermissionButton = await screen.findByRole('button', {
+      name: 'quality_gates.permissions.grant',
+    });
+    await user.click(grantPermissionButton);
+    const popup = screen.getByRole('dialog');
+    const searchUserInput = within(popup).getByRole('textbox');
+    expect(searchUserInput).toBeInTheDocument();
+    const addUserButton = screen.getByRole('button', {
+      name: 'add_verb',
+    });
+    expect(addUserButton).toBeDisabled();
+    await user.click(searchUserInput);
+    expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
+    await user.click(screen.getByText('userlogin'));
+    expect(addUserButton).toBeEnabled();
+    await user.click(addUserButton);
+    expect(screen.getByText('userlogin')).toBeInTheDocument();
+
+    // Cancel granting permission
+    await user.click(grantPermissionButton);
+    await user.click(searchUserInput);
+    await user.keyboard('test{Enter}');
+
+    const cancelButton = screen.getByRole('button', {
+      name: 'cancel',
+    });
+    await user.click(cancelButton);
+
+    // FP
+    // eslint-disable-next-line jest-dom/prefer-in-document
+    expect(screen.getAllByRole('listitem')).toHaveLength(1);
+
+    // Delete the user permission
+    const deleteButton = screen.getByTestId('permission-delete-button');
+    await user.click(deleteButton);
+    const deletePopup = screen.getByRole('dialog');
+    const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
+    await user.click(dialogDeleteButton);
+    expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+  });
+
+  it('should assign permission to a group and delete it later', async () => {
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
+
+    // Granting permission to a group
+    const grantPermissionButton = await screen.findByRole('button', {
+      name: 'quality_gates.permissions.grant',
+    });
+    await user.click(grantPermissionButton);
+    const popup = screen.getByRole('dialog');
+    const searchUserInput = within(popup).getByRole('textbox');
+    const addUserButton = screen.getByRole('button', {
+      name: 'add_verb',
+    });
+    await user.click(searchUserInput);
+    expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
+    await user.click(screen.getAllByTestId('qg-add-permission-option')[1]);
+    await user.click(addUserButton);
+    expect(screen.getByText('Foo')).toBeInTheDocument();
+
+    // Delete the group permission
+    const deleteButton = screen.getByTestId('permission-delete-button');
+    await user.click(deleteButton);
+    const deletePopup = screen.getByRole('dialog');
+    const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
+    await user.click(dialogDeleteButton);
+    expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+  });
+
+  it('should handle searchUser service failure', async () => {
+    (searchUsers as jest.Mock).mockRejectedValue('error');
+
+    const user = userEvent.setup();
+    handler.setIsAdmin(true);
+    renderQualityGateApp();
+
+    const grantPermissionButton = await screen.findByRole('button', {
+      name: 'quality_gates.permissions.grant',
+    });
+    await user.click(grantPermissionButton);
+    const popup = screen.getByRole('dialog');
+    const searchUserInput = within(popup).getByRole('textbox');
+    await user.click(searchUserInput);
+
+    expect(screen.getByText('no_results')).toBeInTheDocument();
+  });
+});
+
+function renderQualityGateApp(context?: RenderContext) {
+  renderAppRoutes('quality_gates', routes, context);
+}
index d1b7a539417c5af8f6d8374f297e51aad36e4321..6591d52354f733ce22779e332afe4b16194639cf 100644 (file)
 .quality-gate-permissions .permission-list-item:hover {
   background-color: var(--rowHoverHighlight);
 }
+
+.cayc-conditions-wrapper {
+  background-color: var(--barBackgroundColor);
+}
+
+.quality-gate-section tbody {
+  border: 1px solid var(--disableGrayBorder);
+}
+
+.quality-gate-section tr {
+  background-color: white !important;
+  border-bottom: 1px solid var(--disableGrayBorder);
+}
+
+.quality-gate-section tr th {
+  font-weight: 400 !important;
+  font-size: 11px;
+}
+
+.quality-gate-section thead tr {
+  background-color: transparent !important;
+  border: none;
+}
+
+.quality-gate-section thead:after {
+  display: none !important;
+}
+
+.qg-cayc-ok-badge {
+  color: var(--success500);
+  background-color: var(--badgeGreenBackground);
+}
+
+.qg-cayc-weak-badge {
+  color: var(--alertIconWarning);
+  background-color: var(--alertBackgroundWarning);
+}
+
+.qg-cayc-missing-badge {
+  color: var(--red);
+  background-color: var(--badgeRedBackground);
+}
+
+.text-through {
+  text-decoration: line-through;
+  color: var(--blacka60);
+}
+
+.bordered-bottom-cayc {
+  border-bottom: 1px solid var(--whitea18);
+}
+
+.cayc-warning-header {
+  color: var(--alertTextWarning);
+}
+
+.cayc-warning-description {
+  line-height: 18px;
+}
+
+.green-text {
+  color: var(--success500);
+}
+
+.red-text {
+  color: var(--red);
+}
index 79b894c36096e0ed4de0ae6fd04214fd6c15e397..1548766e61ef5e5f467b0eee86ae751855816606 100644 (file)
@@ -21,6 +21,77 @@ import { getLocalizedMetricName } from '../../helpers/l10n';
 import { isDiffMetric } from '../../helpers/measures';
 import { Condition, Dict, Metric, QualityGate } from '../../types/types';
 
+const CAYC_CONDITIONS_WITH_EXPECTED_VALUE: { [key: string]: Condition } = {
+  new_reliability_rating: {
+    error: '1',
+    id: 'new_reliability_rating',
+    metric: 'new_reliability_rating',
+    op: 'GT',
+  },
+  new_security_rating: {
+    error: '1',
+    id: 'new_security_rating',
+    metric: 'new_security_rating',
+    op: 'GT',
+  },
+  new_maintainability_rating: {
+    error: '1',
+    id: 'new_maintainability_rating',
+    metric: 'new_maintainability_rating',
+    op: 'GT',
+  },
+  new_security_hotspots_reviewed: {
+    error: '100',
+    id: 'new_security_hotspots_reviewed',
+    metric: 'new_security_hotspots_reviewed',
+    op: 'LT',
+  },
+};
+
+export function getCaycConditions(conditions: Condition[]) {
+  return conditions.filter((condition) => isCaycCondition(condition));
+}
+
+export function isCaycCondition(condition: Condition) {
+  return Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).includes(condition.metric);
+}
+
+export function isCaycWeakCondition(condition: Condition) {
+  return (
+    isCaycCondition(condition) &&
+    CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric].error !== condition.error
+  );
+}
+
+export function getWeakAndMissingConditions(conditions: Condition[]) {
+  const result: {
+    weakConditions: Condition[];
+    missingConditions: Condition[];
+  } = {
+    weakConditions: [],
+    missingConditions: [],
+  };
+  Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).forEach((key) => {
+    const selectedCondition = conditions.find((condition) => condition.metric === key);
+    if (!selectedCondition) {
+      result.missingConditions.push(CAYC_CONDITIONS_WITH_EXPECTED_VALUE[key]);
+    } else if (CAYC_CONDITIONS_WITH_EXPECTED_VALUE[key].error !== selectedCondition.error) {
+      result.weakConditions.push(selectedCondition);
+    }
+  });
+  return result;
+}
+
+export function getOthersConditions(conditions: Condition[]) {
+  return conditions.filter(
+    (condition) => !Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).includes(condition.metric)
+  );
+}
+
+export function getCorrectCaycCondition(condition: Condition) {
+  return CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric];
+}
+
 export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): boolean {
   const finding = list.find((candidate) => candidate.id === qualityGate.id);
   return (finding && finding.isDefault) || false;
index 083a3716be77582c62e6e615fd23ee1f4fee6841..549a3a04e6e30de6f1c9aa068884eeb075df5bca 100644 (file)
@@ -48,6 +48,7 @@ interface Props {
   renderElement: (element: string) => React.ReactNode;
   selectedElements: string[];
   withPaging?: boolean;
+  autoFocusSearch?: boolean;
 }
 
 export interface SelectListSearchParams {
@@ -136,6 +137,7 @@ export default class SelectList extends React.PureComponent<Props, State> {
       labelSelected = translate('selected'),
       labelUnselected = translate('unselected'),
       labelAll = translate('all'),
+      autoFocusSearch = true,
     } = this.props;
     const { filter } = this.state.lastSearchParams;
 
@@ -157,7 +159,7 @@ export default class SelectList extends React.PureComponent<Props, State> {
             />
           </span>
           <SearchBox
-            autoFocus={true}
+            autoFocus={autoFocusSearch}
             loading={this.state.loading}
             onChange={this.handleQueryChange}
             placeholder={translate('search_verb')}
index 44087b5a82f53476de38fea6b448552d9111c3f5..5ba82ab2d30bf5748c7c06b60676959e8a79dc15 100644 (file)
@@ -21,31 +21,23 @@ import * as React from 'react';
 import withAppStateContext from '../../app/components/app-state/withAppStateContext';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { formatMeasure, isDiffMetric } from '../../helpers/measures';
+import {
+  DIFF_METRIC_PREFIX_LENGTH,
+  getMaintainabilityGrid,
+  GRID_INDEX_OFFSET,
+  PERCENT_MULTIPLIER,
+} from '../../helpers/ratings';
 import { AppState } from '../../types/appstate';
 import { MetricKey } from '../../types/metrics';
 import { GlobalSettingKeys } from '../../types/settings';
 import { KNOWN_RATINGS } from './utils';
 
-const RATING_GRID_SIZE = 4;
-const DIFF_METRIC_PREFIX_LENGTH = 4;
-const PERCENT_MULTIPLIER = 100;
-const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2)
-
 export interface RatingTooltipContentProps {
   appState: AppState;
   metricKey: MetricKey | string;
   value: number | string;
 }
 
-function getMaintainabilityGrid(ratingGridSetting: string) {
-  const numbers = ratingGridSetting
-    .split(',')
-    .map((s) => parseFloat(s))
-    .filter((n) => !isNaN(n));
-
-  return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0];
-}
-
 export function RatingTooltipContent(props: RatingTooltipContentProps) {
   const {
     appState: { settings },
index 60fa6b5b70c2c616496a214e5cff60df6022db8a..6b85b84c16248b8f1f4619256d86f64fa314c9b4 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.
  */
+
+const RATING_GRID_SIZE = 4;
+export const PERCENT_MULTIPLIER = 100;
+export const DIFF_METRIC_PREFIX_LENGTH = 4;
+export const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2)
+
 function checkNumberRating(coverageRating: number): void {
   if (!(typeof coverageRating === 'number' && coverageRating > 0 && coverageRating < 6)) {
     throw new Error(`Unknown number rating: "${coverageRating}"`);
@@ -58,3 +64,12 @@ export function getSizeRatingAverageValue(rating: number): number {
   const mapping = [500, 5000, 50000, 250000, 750000];
   return mapping[rating - 1];
 }
+
+export const getMaintainabilityGrid = (ratingGridSetting: string) => {
+  const numbers = ratingGridSetting
+    .split(',')
+    .map((s) => parseFloat(s))
+    .filter((n) => !isNaN(n));
+
+  return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0];
+};
index 95ff97d115ec69a31dfd600d9c70c57e8f1a041a..54cb7e6714e6a0561bf958a1ae5780d2f04efb4c 100644 (file)
@@ -1837,7 +1837,41 @@ quality_gates.permissions.remove.user=Remove permission from user
 quality_gates.permissions.remove.user.confirmation=Are you sure you want to remove permission on this quality gate from user {user}?
 quality_gates.permissions.remove.group=Remove permission from group
 quality_gates.permissions.remove.group.confirmation=Are you sure you want to remove permission on this quality gate from group {user}?
-
+quality_gates.cayc=Clean as You Code
+quality_gates.cayc.description=The following conditions ensure that your project remains compliant with {link}.
+quality_gates.cayc.description.extended=We strongly recommend that you keep them unchanged.
+quality_gates.other_conditions=Other conditions
+quality_gates.cayc_condition.missing_warning.title=Some Clean as You Code conditions are missing or are too permissive
+quality_gates.cayc_condition.missing_warning.description=Four conditions are included by default in any new Quality Gate. They represent the minimum level of quality needed to follow Clean as You Code. We recommend that you add the missing conditions and increase the value of the conditions that are too permissive to benefit from the power of Clean as You Code.
+quality_gates.cayc_condition.review_update=Review and update
+quality_gates.cayc.review_update_modal.header=Update "{0}" to follow Cleas as You Code
+quality_gates.cayc.review_update_modal.confirm_text=Update Quality Gate
+quality_gates.cayc.review_update_modal.description=Please review the changes before you update this Quality Gate.
+quality_gates.cayc.review_update_modal.modify_condition.header={0} condition(s) will be modified
+quality_gates.cayc.review_update_modal.add_condition.header={0} condition(s) will be added
+quality_gates.cayc_condition.ok=OK
+quality_gates.cayc_condition.weak=WEAK
+quality_gates.cayc_condition.missing=MISSING
+quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0}
+quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1}
+quality_gates.cayc.new_reliability_rating.A=No bugs
+quality_gates.cayc.new_reliability_rating.B=Minor bug
+quality_gates.cayc.new_reliability_rating.C=Major bug
+quality_gates.cayc.new_reliability_rating.D=Critical bug
+quality_gates.cayc.new_reliability_rating.E=Blocker bug
+quality_gates.cayc.new_security_rating.A=No vulnerabilities
+quality_gates.cayc.new_security_rating.B=Minor vulnerability
+quality_gates.cayc.new_security_rating.C=Major vulnerability
+quality_gates.cayc.new_security_rating.D=Critical vulnerability
+quality_gates.cayc.new_security_rating.E=Blocker vulnerability
+quality_gates.cayc.unlock_edit=Unlock to edit
+quality_gates.cayc.lock_edit=Lock for editing
+quality_gates.cayc.tooltip.ok=This condition is compliant with Clean as You Code.
+quality_gates.cayc.tooltip.weak=This condition is too permissive. Increase its value to follow Clean as You Code.
+quality_gates.cayc.tooltip.missing=This condition is missing. Add it to follow Clean as You Code.
+quality_gates.cayc.badge.tooltip.learn_more=Learn more: Clean as You Code
+quality_gates.cayc_condition.delete_warning=This condition is part of Clean as You Code. If you delete it you will not benefit from the power of Clean As You Code anymore.
+quality_gates.cayc_condition.edit_warning=This condition is part of Clean as You Code. If you decrease its value you will not benefit from the power of Clean As You Code anymore.
 #------------------------------------------------------------------------------
 #
 # RULES DOCUMENTATION PAGE