aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2023-01-11 14:02:45 +0100
committersonartech <sonartech@sonarsource.com>2023-01-12 20:02:52 +0000
commit105204fa4727f1c77e0f293cc2a9dfa984493e7b (patch)
treeef5bdc22cd455d1225f4c153ceab97b2b1a46e6f /server/sonar-web
parente3100a5030d09dc7df418d58c1ca5946fa4b965f (diff)
downloadsonarqube-105204fa4727f1c77e0f293cc2a9dfa984493e7b.tar.gz
sonarqube-105204fa4727f1c77e0f293cc2a9dfa984493e7b.zip
SONAR-17815 New UI changes for CAYC quality gates
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx132
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx144
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx107
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx198
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx63
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/styles.css41
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/utils.ts92
19 files changed, 444 insertions, 539 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
index 1e7ef5d75fc..bec51cabac4 100644
--- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
@@ -125,6 +125,18 @@ export class QualityGatesServiceMock {
isBuiltIn: true,
isCaycCompliant: true,
}),
+ mockQualityGate({
+ id: 'AWBWEMe4qGAMGEYPjJlruit',
+ name: 'Non Cayc QG',
+ conditions: [
+ { id: 'AXJMbIUHPAOIsUIE3eNs', metric: 'new_security_rating', op: 'GT', error: '1' },
+ { id: 'AXJMbIUHPAOIsUIE3eOD', metric: 'new_reliability_rating', op: 'GT', error: '1' },
+ { id: 'AXJMbIUHPAOIsUIE3eOF', metric: 'new_coverage', op: 'LT', error: '80' },
+ ],
+ isDefault: false,
+ isBuiltIn: false,
+ isCaycCompliant: false,
+ }),
];
this.list = cloneDeep(this.readOnlyList);
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
index 156ce62c0c8..6bd2c2f0788 100644
--- 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
@@ -20,18 +20,12 @@
import * as React from 'react';
import DocLink from '../../../components/common/DocLink';
import { translate } from '../../../helpers/l10n';
-import { BadgeTarget, QGBadgeType } from '../../../types/quality-gates';
-interface Props {
- badgeType: QGBadgeType;
- target: BadgeTarget;
-}
-
-export default function CaycBadgeTooltip({ badgeType, target }: Props) {
+export default function CaycBadgeTooltip() {
return (
<div>
<p className="spacer-bottom padded-bottom bordered-bottom-cayc">
- {translate('quality_gates.cayc.tooltip', badgeType, target)}
+ {translate('quality_gates.cayc.tooltip.message')}
</p>
<DocLink to="/user-guide/clean-as-you-code/">
{translate('quality_gates.cayc.badge.tooltip.learn_more')}
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
deleted file mode 100644
index 546c79d27fa..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx
+++ /dev/null
@@ -1,132 +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 * 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
deleted file mode 100644
index c1c9ea4e857..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx
+++ /dev/null
@@ -1,59 +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 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 { BadgeTarget, QGBadgeType } from '../../../types/quality-gates';
-import CaycBadgeTooltip from './CaycBadgeTooltip';
-
-interface Props {
- className?: string;
- target?: BadgeTarget;
- type: QGBadgeType;
-}
-
-const iconForType = {
- [QGBadgeType.Ok]: CheckIcon,
- [QGBadgeType.Missing]: AlertErrorIcon,
- [QGBadgeType.Weak]: AlertWarnIcon,
-};
-
-const getIcon = (type: QGBadgeType) => iconForType[type];
-
-export default function CaycStatusBadge({
- className,
- target = BadgeTarget.Condition,
- type,
-}: Props) {
- const Icon = getIcon(type);
-
- return (
- <Tooltip overlay={<CaycBadgeTooltip badgeType={type} target={target} />}>
- <div className={classNames(`badge qg-cayc-${type}-badge display-flex-center`, className)}>
- <Icon className="spacer-right" />
- <span>{translate('quality_gates.cayc_condition', type)}</span>
- </div>
- </Tooltip>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
index ff8106788fe..7bba57a6236 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
@@ -23,12 +23,9 @@ 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 { QGBadgeType } from '../../../types/quality-gates';
import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
-import { getLocalizedMetricNameNoDiffMetric, isCaycCondition, isCaycWeakCondition } from '../utils';
-import CaycStatusBadge from './CaycStatusBadge';
+import { CAYC_CONDITIONS_WITHOUT_FIXED_VALUE, getLocalizedMetricNameNoDiffMetric } from '../utils';
import ConditionModal from './ConditionModal';
import ConditionValue from './ConditionValue';
@@ -42,7 +39,6 @@ interface Props {
updated?: boolean;
metrics: Dict<Metric>;
showEdit?: boolean;
- isMissingCondition?: boolean;
isCaycModal?: boolean;
}
@@ -87,20 +83,6 @@ export class ConditionComponent extends React.PureComponent<Props, State> {
);
};
- getBadgeType = () => {
- const { condition, isCaycModal, isMissingCondition } = this.props;
-
- if (!isCaycModal) {
- if (isMissingCondition) {
- return QGBadgeType.Missing;
- } else if (isCaycWeakCondition(condition)) {
- return QGBadgeType.Weak;
- }
- }
-
- return QGBadgeType.Ok;
- };
-
renderOperator() {
// TODO can operator be missing?
const { op = 'GT' } = this.props.condition;
@@ -118,88 +100,82 @@ export class ConditionComponent extends React.PureComponent<Props, State> {
updated,
metrics,
showEdit = true,
- isMissingCondition = false,
isCaycModal = false,
} = this.props;
return (
<tr className={classNames({ highlighted: updated })}>
- <td
- className={classNames('text-middle', {
- 'text-through': isMissingCondition && !isCaycModal,
- 'green-text': isMissingCondition && isCaycModal,
- })}
- >
+ <td className="text-middle">
{getLocalizedMetricNameNoDiffMetric(metric, metrics)}
{metric.hidden && (
<span className="text-danger little-spacer-left">{translate('deprecated')}</span>
)}
</td>
- <td
- className={classNames('text-middle nowrap', {
- 'text-through': isMissingCondition && !isCaycModal,
- 'green-text': isMissingCondition && isCaycModal,
- })}
- >
- {this.renderOperator()}
- </td>
+ <td className="text-middle nowrap">{this.renderOperator()}</td>
- <td
- className={classNames('text-middle nowrap', {
- 'text-through': isMissingCondition && !isCaycModal,
- 'green-text': isMissingCondition && isCaycModal,
- })}
- >
- <ConditionValue metric={metric} isCaycModal={isCaycModal} condition={condition} />
+ <td className="text-middle nowrap">
+ <ConditionValue
+ metric={metric}
+ isCaycModal={isCaycModal}
+ condition={condition}
+ isCaycCompliant={qualityGate.isCaycCompliant}
+ />
</td>
-
<td className="text-middle nowrap display-flex-justify-end">
- {isCaycCondition(condition) && <CaycStatusBadge type={this.getBadgeType()} />}
- {canEdit && showEdit && !isMissingCondition && (
+ {!isCaycModal && canEdit && (
<>
- <EditButton
- aria-label={translateWithParameters('quality_gates.condition.edit', metric.name)}
- data-test="quality-gates__condition-update"
- onClick={this.handleOpenUpdate}
- className="spacer-right"
- />
- <DeleteButton
- aria-label={translateWithParameters('quality_gates.condition.delete', metric.name)}
- data-test="quality-gates__condition-delete"
- onClick={this.handleDeleteClick}
- />
- {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)
+ {(!qualityGate.isCaycCompliant ||
+ CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric) ||
+ (qualityGate.isCaycCompliant && showEdit)) && (
+ <>
+ <EditButton
+ aria-label={translateWithParameters(
+ 'quality_gates.condition.edit',
+ metric.name
+ )}
+ data-test="quality-gates__condition-update"
+ onClick={this.handleOpenUpdate}
+ className="spacer-right"
+ />
+ {this.state.modal && (
+ <ConditionModal
+ condition={condition}
+ header={translate('quality_gates.update_condition')}
+ metric={metric}
+ onAddCondition={this.handleUpdateCondition}
+ onClose={this.handleUpdateClose}
+ qualityGate={qualityGate}
+ />
)}
- {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>
+ </>
+ )}
+ {(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && showEdit)) && (
+ <>
+ <DeleteButton
+ aria-label={translateWithParameters(
+ 'quality_gates.condition.delete',
+ metric.name
+ )}
+ data-test="quality-gates__condition-delete"
+ onClick={this.handleDeleteClick}
+ />
+ {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>
)}
- </ConfirmModal>
+ </>
)}
</>
)}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
index 5a5392db369..b542d00ea24 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
@@ -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, isCaycCondition, isCaycWeakCondition } from '../utils';
+import { getPossibleOperators } 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, condition } = this.props;
+ const { header, metrics, onClose } = this.props;
const { op, error, scope, metric } = this.state;
return (
<ConfirmModal
@@ -138,14 +138,6 @@ 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
index 2d93b71d6ee..d587a5a298f 100644
--- 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
@@ -17,12 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { sortBy } from 'lodash';
import * as React from 'react';
-import { createCondition, updateCondition } from '../../../api/quality-gates';
+import { createCondition, deleteCondition, 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 {
+ getCaycConditionsWithCorrectValue,
+ getCorrectCaycCondition,
+ getWeakMissingAndNonCaycConditions,
+} from '../utils';
import ConditionsTable from './ConditionsTable';
interface Props {
@@ -33,23 +38,32 @@ interface Props {
scope: 'new' | 'overall' | 'new-cayc';
onClose: () => void;
onAddCondition: (condition: Condition) => void;
- onRemoveCondition: (Condition: Condition) => void;
+ onRemoveCondition: (condition: Condition) => void;
onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void;
+ lockEditing: () => 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);
+ const promiseArr: Promise<Condition | undefined | void>[] = [];
+ const { weakConditions, missingConditions, nonCaycConditions } =
+ getWeakMissingAndNonCaycConditions(conditions);
weakConditions.forEach((condition) => {
promiseArr.push(
updateCondition({
...getCorrectCaycCondition(condition),
id: condition.id,
- }).catch(() => undefined)
+ })
+ .then((resultCondition) => {
+ const currentCondition = conditions.find((con) => con.metric === condition.metric);
+ if (currentCondition) {
+ this.props.onSaveCondition(resultCondition, currentCondition);
+ }
+ })
+ .catch(() => undefined)
);
});
@@ -58,29 +72,32 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent
createCondition({
...getCorrectCaycCondition(condition),
gateId: qualityGate.id,
- }).catch(() => undefined)
+ })
+ .then((resultCondition) => this.props.onAddCondition(resultCondition))
+ .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);
- }
- });
+ nonCaycConditions.forEach((condition) => {
+ promiseArr.push(
+ deleteCondition({ id: condition.id })
+ .then(() => this.props.onRemoveCondition(condition))
+ .catch(() => undefined)
+ );
+ });
+
+ return Promise.all(promiseArr).then(() => {
+ this.props.lockEditing();
});
};
render() {
- const { conditions, qualityGate } = this.props;
- const { weakConditions, missingConditions } = getWeakAndMissingConditions(conditions);
-
+ const { conditions, qualityGate, metrics } = this.props;
+ const caycConditionsWithCorrectValue = getCaycConditionsWithCorrectValue(conditions);
+ const sortedConditions = sortBy(
+ caycConditionsWithCorrectValue,
+ (condition) => metrics[condition.metric] && metrics[condition.metric].name
+ );
return (
<ConfirmModal
header={translateWithParameters(
@@ -92,45 +109,19 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent
onConfirm={this.updateCaycQualityGate}
size="medium"
>
- <div className="quality-gate-section">
+ <div className="quality-gate-section huge-spacer-bottom">
<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}
- />
- </>
- )}
+ <h3 className="medium text-normal spacer-top spacer-bottom">
+ {translate('quality_gates.conditions.new_code', 'long')}
+ </h3>
+ <ConditionsTable
+ {...this.props}
+ conditions={sortedConditions}
+ showEdit={false}
+ 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
index df286fa490f..4c3a4961c51 100644
--- 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
@@ -20,42 +20,24 @@
import * as React from 'react';
import { formatMeasure } from '../../../helpers/measures';
import { Condition, Metric } from '../../../types/types';
-import { getCorrectCaycCondition, isCaycCondition, isCaycWeakCondition } from '../utils';
+import { getCorrectCaycCondition, isCaycCondition } from '../utils';
import ConditionValueDescription from './ConditionValueDescription';
interface Props {
condition: Condition;
isCaycModal?: boolean;
metric: Metric;
+ isCaycCompliant?: boolean;
}
-function ConditionValue({ condition, isCaycModal, metric }: Props) {
+function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: 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">
+ <span className="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} />
+ <ConditionValueDescription condition={getCorrectCaycCondition(condition)} metric={metric} />
</>
);
}
@@ -63,7 +45,7 @@ function ConditionValue({ condition, isCaycModal, metric }: Props) {
return (
<>
<span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span>
- {isCaycCondition(condition) && (
+ {isCaycCompliant && isCaycCondition(condition) && (
<ConditionValueDescription condition={condition} metric={metric} />
)}
</>
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
index 908ecc040d7..6829c71fe9c 100644
--- 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
@@ -31,6 +31,12 @@ import { GlobalSettingKeys } from '../../../types/settings';
import { Condition, Metric } from '../../../types/types';
import { isCaycCondition } from '../utils';
+const NO_DESCRIPTION_CONDITION = [
+ 'new_security_hotspots_reviewed',
+ 'new_coverage',
+ 'new_duplicated_lines_density',
+];
+
interface Props {
appState: AppState;
condition: Condition;
@@ -72,7 +78,7 @@ function ConditionValueDescription({
return (
<span className={className}>
- {isCaycCondition(condition) && condition.metric !== 'new_security_hotspots_reviewed' && (
+ {isCaycCondition(condition) && !NO_DESCRIPTION_CONDITION.includes(condition.metric) && (
<>
(
{translate(
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
index 76360b49de9..f0615f4406b 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
@@ -19,10 +19,12 @@
*/
import { differenceWith, map, sortBy, uniqBy } from 'lodash';
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
+import DocLink from '../../../components/common/DocLink';
import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
import { Button } from '../../../components/controls/buttons';
import ModalButton, { ModalProps } from '../../../components/controls/ModalButton';
@@ -32,14 +34,11 @@ 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 { getCaycConditions, getOthersConditions } from '../utils';
-import CaycConditions from './CaycConditions';
import ConditionModal from './ConditionModal';
+import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
import ConditionsTable from './ConditionsTable';
interface Props extends WithAvailableFeaturesProps {
- canEdit: boolean;
- conditions: ConditionType[];
metrics: Dict<Metric>;
onAddCondition: (condition: ConditionType) => void;
onRemoveCondition: (Condition: ConditionType) => void;
@@ -48,6 +47,10 @@ interface Props extends WithAvailableFeaturesProps {
updatedConditionId?: string;
}
+interface State {
+ unlockEditing: boolean;
+}
+
const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL'];
const FORBIDDEN_METRICS: string[] = [
MetricKey.alert_status,
@@ -56,9 +59,32 @@ const FORBIDDEN_METRICS: string[] = [
MetricKey.new_security_hotspots,
];
-export class Conditions extends React.PureComponent<Props> {
+export class Conditions extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ unlockEditing: !props.qualityGate.isCaycCompliant,
+ };
+ }
+
+ componentDidUpdate(prevProps: Readonly<Props>): void {
+ const { qualityGate } = this.props;
+ if (prevProps.qualityGate.name !== qualityGate.name) {
+ this.setState({ unlockEditing: !qualityGate.isCaycCompliant });
+ }
+ }
+
+ unlockEditing = () => {
+ this.setState({ unlockEditing: true });
+ };
+
+ lockEditing = () => {
+ this.setState({ unlockEditing: false });
+ };
+
renderConditionModal = ({ onClose }: ModalProps) => {
- const { metrics, qualityGate, conditions } = this.props;
+ const { metrics, qualityGate } = this.props;
+ const { conditions = [] } = qualityGate;
const availableMetrics = differenceWith(
map(metrics, (metric) => metric).filter(
(metric) =>
@@ -80,18 +106,33 @@ export class Conditions extends React.PureComponent<Props> {
);
};
- render() {
- const {
- qualityGate,
- metrics,
- canEdit,
- onAddCondition,
- onRemoveCondition,
- onSaveCondition,
- updatedConditionId,
- conditions,
- } = this.props;
+ renderCaycModal = ({ onClose }: ModalProps) => {
+ const { qualityGate, metrics } = this.props;
+ const { conditions = [] } = qualityGate;
+ const canEdit = Boolean(qualityGate.actions?.manageConditions);
+ return (
+ <CaycReviewUpdateConditionsModal
+ qualityGate={qualityGate}
+ metrics={metrics}
+ canEdit={canEdit}
+ onRemoveCondition={this.props.onRemoveCondition}
+ onSaveCondition={this.props.onSaveCondition}
+ onAddCondition={this.props.onAddCondition}
+ lockEditing={this.lockEditing}
+ updatedConditionId={this.props.updatedConditionId}
+ conditions={conditions}
+ scope="new-cayc"
+ onClose={onClose}
+ />
+ );
+ };
+ render() {
+ const { qualityGate, metrics, onRemoveCondition, onSaveCondition, updatedConditionId } =
+ this.props;
+ const canEdit = Boolean(qualityGate.actions?.manageConditions);
+ const { unlockEditing } = this.state;
+ const { conditions = [] } = qualityGate;
const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
const sortedConditions = sortBy(
existingConditions,
@@ -123,18 +164,82 @@ export class Conditions extends React.PureComponent<Props> {
return (
<div className="quality-gate-section">
- {canEdit && (
- <div className="pull-right">
- <ModalButton modal={this.renderConditionModal}>
- {({ onClick }) => (
- <Button data-test="quality-gates__add-condition" onClick={onClick}>
- {translate('quality_gates.add_condition')}
- </Button>
- )}
- </ModalButton>
- </div>
+ {qualityGate.isCaycCompliant && (
+ <Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="success">
+ <h4 className="spacer-bottom cayc-success-header">
+ {translate('quality_gates.cayc.banner.title')}
+ </h4>
+ <div className="cayc-warning-description">
+ <FormattedMessage
+ id="quality_gates.cayc.banner.description"
+ defaultMessage={translate('quality_gates.cayc.banner.description')}
+ values={{
+ cayc_link: (
+ <DocLink to="/user-guide/clean-as-you-code/">
+ {translate('quality_gates.cayc')}
+ </DocLink>
+ ),
+ new_code_link: (
+ <DocLink to="/project-administration/defining-new-code//">
+ {translate('quality_gates.cayc.new_code')}
+ </DocLink>
+ ),
+ }}
+ />
+ </div>
+ <ul className="big-spacer-top big-spacer-left spacer-bottom cayc-warning-description">
+ <li>{translate('quality_gates.cayc.banner.list_item1')}</li>
+ <li>{translate('quality_gates.cayc.banner.list_item2')}</li>
+ <li>{translate('quality_gates.cayc.banner.list_item3')}</li>
+ <li>{translate('quality_gates.cayc.banner.list_item4')}</li>
+ <li>{translate('quality_gates.cayc.banner.list_item5')}</li>
+ </ul>
+ </Alert>
+ )}
+
+ {!qualityGate.isCaycCompliant && (
+ <Alert className="big-spacer-top big-spacer-bottom" variant="warning">
+ <h4 className="spacer-bottom cayc-warning-header">
+ {translate('quality_gates.cayc_missing.banner.title')}
+ </h4>
+ <div className="cayc-warning-description spacer-bottom">
+ <FormattedMessage
+ id="quality_gates.cayc_missing.banner.description"
+ defaultMessage={translate('quality_gates.cayc_missing.banner.description')}
+ values={{
+ cayc_link: (
+ <DocLink to="/user-guide/clean-as-you-code/">
+ {translate('quality_gates.cayc')}
+ </DocLink>
+ ),
+ }}
+ />
+ </div>
+ {canEdit && (
+ <ModalButton modal={this.renderCaycModal}>
+ {({ onClick }) => (
+ <Button className="big-spacer-top spacer-bottom" onClick={onClick}>
+ {translate('quality_gates.cayc_condition.review_update')}
+ </Button>
+ )}
+ </ModalButton>
+ )}
+ </Alert>
)}
+ {(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && unlockEditing)) &&
+ canEdit && (
+ <div className="pull-right">
+ <ModalButton modal={this.renderConditionModal}>
+ {({ onClick }) => (
+ <Button data-test="quality-gates__add-condition" onClick={onClick}>
+ {translate('quality_gates.add_condition')}
+ </Button>
+ )}
+ </ModalButton>
+ </div>
+ )}
+
<header className="display-flex-center">
<h2 className="big">{translate('quality_gates.conditions')}</h2>
<DocumentationTooltip
@@ -170,20 +275,6 @@ export class Conditions extends React.PureComponent<Props> {
{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}
@@ -191,7 +282,8 @@ export class Conditions extends React.PureComponent<Props> {
onRemoveCondition={onRemoveCondition}
onSaveCondition={onSaveCondition}
updatedConditionId={updatedConditionId}
- conditions={getOthersConditions(sortedConditionsOnNewMetrics)}
+ conditions={sortedConditionsOnNewMetrics}
+ showEdit={this.state.unlockEditing}
scope="new"
/>
</div>
@@ -222,6 +314,28 @@ export class Conditions extends React.PureComponent<Props> {
</div>
)}
+ {qualityGate.isCaycCompliant && !unlockEditing && canEdit && (
+ <div className="huge-spacer-top big-spacer-bottom qg-unfollow-cayc big-padded">
+ <h4 className="spacer-bottom">{translate('quality_gates.cayc_unfollow.title')}</h4>
+ <div className="cayc-warning-description">
+ <FormattedMessage
+ id="quality_gates.cayc_unfollow.description"
+ defaultMessage={translate('quality_gates.cayc_unfollow.description')}
+ values={{
+ cayc_link: (
+ <DocLink to="/user-guide/clean-as-you-code/">
+ {translate('quality_gates.cayc')}
+ </DocLink>
+ ),
+ }}
+ />
+ </div>
+ <Button className="big-spacer-top spacer-bottom" onClick={this.unlockEditing}>
+ {translate('quality_gates.cayc.unlock_edit')}
+ </Button>
+ </div>
+ )}
+
{existingConditions.length === 0 && (
<div className="big-spacer-top">{translate('quality_gates.no_conditions')}</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
index bdacfd41ffe..024619c6a36 100644
--- 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
@@ -30,10 +30,9 @@ interface Props {
qualityGate: QualityGate;
updatedConditionId?: string;
conditions: ConditionType[];
- missingConditions?: ConditionType[];
scope: 'new' | 'overall' | 'new-cayc';
- showEdit?: boolean;
isCaycModal?: boolean;
+ showEdit?: boolean;
}
export default class ConditionsTable extends React.PureComponent<Props> {
@@ -47,9 +46,8 @@ export default class ConditionsTable extends React.PureComponent<Props> {
updatedConditionId,
scope,
conditions,
- missingConditions,
- showEdit,
isCaycModal,
+ showEdit,
} = this.props;
return (
@@ -77,27 +75,10 @@ export default class ConditionsTable extends React.PureComponent<Props> {
onSaveCondition={onSaveCondition}
qualityGate={qualityGate}
updated={condition.id === updatedConditionId}
- showEdit={showEdit}
isCaycModal={isCaycModal}
+ showEdit={showEdit}
/>
))}
-
- {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>
);
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
index dbeac43e8ed..48e22abfe16 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { clone } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { fetchQualityGate } from '../../../api/quality-gates';
@@ -83,8 +84,14 @@ export default class Details extends React.PureComponent<Props, State> {
return null;
}
addGlobalSuccessMessage(translate('quality_gates.condition_added'));
+
+ const updatedQualityGate = addCondition(clone(qualityGate), condition);
+ if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) {
+ this.props.refreshQualityGates();
+ }
+
return {
- qualityGate: addCondition(qualityGate, condition),
+ qualityGate: updatedQualityGate,
updatedConditionId: condition.id,
};
});
@@ -96,8 +103,12 @@ export default class Details extends React.PureComponent<Props, State> {
return null;
}
addGlobalSuccessMessage(translate('quality_gates.condition_updated'));
+ const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition);
+ if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) {
+ this.props.refreshQualityGates();
+ }
return {
- qualityGate: replaceCondition(qualityGate, newCondition, oldCondition),
+ qualityGate: updatedQualityGate,
updatedConditionId: newCondition.id,
};
});
@@ -109,8 +120,12 @@ export default class Details extends React.PureComponent<Props, State> {
return null;
}
addGlobalSuccessMessage(translate('quality_gates.condition_deleted'));
+ const updatedQualityGate = deleteCondition(clone(qualityGate), condition);
+ if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) {
+ this.props.refreshQualityGates();
+ }
return {
- qualityGate: deleteCondition(qualityGate, condition),
+ qualityGate: updatedQualityGate,
updatedConditionId: undefined,
};
});
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
index be07765c087..3d9b55f52e5 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
@@ -37,7 +37,6 @@ export interface DetailsContentProps {
export function DetailsContent(props: DetailsContentProps) {
const { isDefault, qualityGate, updatedConditionId } = props;
- const conditions = qualityGate.conditions || [];
const actions = qualityGate.actions || {};
return (
@@ -50,8 +49,6 @@ export function DetailsContent(props: DetailsContentProps) {
)}
<Conditions
- canEdit={Boolean(actions.manageConditions)}
- conditions={conditions}
onAddCondition={props.onAddCondition}
onRemoveCondition={props.onRemoveCondition}
onSaveCondition={props.onSaveCondition}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
index 3565f5c0a8d..49b1b5213fd 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
@@ -22,11 +22,11 @@ import { setQualityGateAsDefault } from '../../../api/quality-gates';
import { Button } from '../../../components/controls/buttons';
import ModalButton from '../../../components/controls/ModalButton';
import Tooltip from '../../../components/controls/Tooltip';
+import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
import { translate } from '../../../helpers/l10n';
-import { BadgeTarget, QGBadgeType } from '../../../types/quality-gates';
import { QualityGate } from '../../../types/types';
import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
-import CaycStatusBadge from './CaycStatusBadge';
+import CaycBadgeTooltip from './CaycBadgeTooltip';
import CopyQualityGateForm from './CopyQualityGateForm';
import DeleteQualityGateForm from './DeleteQualityGateForm';
import RenameQualityGateForm from './RenameQualityGateForm';
@@ -72,11 +72,9 @@ export default class DetailsHeader extends React.PureComponent<Props> {
<h2>{qualityGate.name}</h2>
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />}
{!qualityGate.isCaycCompliant && (
- <CaycStatusBadge
- className="spacer-left"
- target={BadgeTarget.QualityGate}
- type={QGBadgeType.Weak}
- />
+ <Tooltip overlay={<CaycBadgeTooltip />}>
+ <AlertWarnIcon className="spacer-left" />
+ </Tooltip>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
index 15062dded7a..4de88601e08 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
@@ -19,12 +19,12 @@
*/
import * as React from 'react';
import { NavLink } from 'react-router-dom';
+import Tooltip from '../../../components/controls/Tooltip';
+import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
-import { BadgeTarget, QGBadgeType } from '../../../types/quality-gates';
import { QualityGate } from '../../../types/types';
import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
-import CaycStatusBadge from './CaycStatusBadge';
interface Props {
qualityGates: QualityGate[];
@@ -49,11 +49,9 @@ export default function List({ qualityGates }: Props) {
)}
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />}
{!qualityGate.isCaycCompliant && (
- <CaycStatusBadge
- className="little-spacer-left"
- target={BadgeTarget.QualityGate}
- type={QGBadgeType.Weak}
- />
+ <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
+ <AlertWarnIcon className="spacer-left" />
+ </Tooltip>
)}
</NavLink>
))}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx
index df26623354e..8e110cf87af 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx
@@ -50,7 +50,7 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss
props;
return (
- <div className="quality-gate-permissions">
+ <div className="quality-gate-permissions" data-testid="quality-gate-permissions">
<h3 className="spacer-bottom">{translate('quality_gates.permissions')}</h3>
<p className="spacer-bottom">{translate('quality_gates.permissions.help')}</p>
<div>
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
index 5b58db2852f..b011eed4e71 100644
--- 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
@@ -177,6 +177,8 @@ it('should be able to add a condition', async () => {
handler.setIsAdmin(true);
renderQualityGateApp();
+ await user.click(await screen.findByText('SonarSource way - CFamily'));
+
// On new code
await user.click(await screen.findByText('quality_gates.add_condition'));
@@ -246,6 +248,7 @@ it('should be able to handle duplicate or deprecated condition', async () => {
const user = userEvent.setup();
handler.setIsAdmin(true);
renderQualityGateApp();
+
await user.click(
// make it a regexp to ignore badges:
await screen.findByRole('menuitem', { name: new RegExp(handler.getCorruptedQualityGateName()) })
@@ -262,6 +265,7 @@ it('should be able to handle delete condition', async () => {
handler.setIsAdmin(true);
renderQualityGateApp();
+ await user.click(await screen.findByText('Non Cayc QG'));
const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
await user.click(
@@ -287,7 +291,7 @@ it('should explain condition on branch', async () => {
).toBeInTheDocument();
});
-it('should be able to see warning when CAYC condition is not properly set and update them', async () => {
+it('should show warning banner when CAYC condition is not properly set and should be able to update them', async () => {
const user = userEvent.setup();
handler.setIsAdmin(true);
renderQualityGateApp();
@@ -296,14 +300,11 @@ it('should be able to see warning when CAYC condition is not properly set and up
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.getByText('quality_gates.cayc_missing.banner.title')).toBeInTheDocument();
+ expect(screen.getByText('quality_gates.cayc_missing.banner.description')).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' })
@@ -317,10 +318,6 @@ it('should be able to see warning when CAYC condition is not properly set and up
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();
@@ -328,11 +325,18 @@ it('should be able to see warning when CAYC condition is not properly set and up
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);
+ const conditionsWrapper = within(await screen.findByTestId('quality-gates__conditions-new'));
+ expect(conditionsWrapper.getByText('Maintainability Rating')).toBeInTheDocument();
+ expect(conditionsWrapper.getByText('Reliability Rating')).toBeInTheDocument();
+ expect(conditionsWrapper.getByText('Security Hotspots Reviewed')).toBeInTheDocument();
+ expect(conditionsWrapper.getByText('Security Rating')).toBeInTheDocument();
+ expect(conditionsWrapper.getAllByText('Coverage')).toHaveLength(2); // This quality gate has duplicate condition
+ expect(conditionsWrapper.getByText('Duplicated Lines (%)')).toBeInTheDocument();
+
+ expect(screen.queryByTestId('quality-gates__conditions-overall')).not.toBeInTheDocument();
});
-it('should not show any warning when CAYC condition are properly set', async () => {
+it('should show success banner when quality gate is CAYC compliant', async () => {
const user = userEvent.setup();
handler.setIsAdmin(true);
renderQualityGateApp();
@@ -341,28 +345,23 @@ it('should not show any warning when CAYC condition are properly set', async ()
await user.click(qualityGate);
+ expect(screen.getByText('quality_gates.cayc.banner.title')).toBeInTheDocument();
+ expect(screen.getByText('quality_gates.cayc.banner.description')).toBeInTheDocument();
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 conditionsWrapper = within(await screen.findByTestId('quality-gates__conditions-new'));
- 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();
+ expect(await conditionsWrapper.findByText('Maintainability Rating')).toBeInTheDocument();
+ expect(await conditionsWrapper.findByText('Reliability Rating')).toBeInTheDocument();
+ expect(await conditionsWrapper.findByText('Security Hotspots Reviewed')).toBeInTheDocument();
+ expect(await conditionsWrapper.findByText('Security Rating')).toBeInTheDocument();
+ expect(await conditionsWrapper.findByText('Coverage')).toBeInTheDocument();
+ expect(await conditionsWrapper.findByText('Duplicated Lines (%)')).toBeInTheDocument();
});
it('should unlock editing option for CAYC conditions', async () => {
@@ -544,9 +543,8 @@ describe('The Permissions section', () => {
});
await user.click(cancelButton);
- // FP
- // eslint-disable-next-line jest-dom/prefer-in-document
- expect(screen.getAllByRole('listitem')).toHaveLength(1);
+ const permissionList = within(await screen.findByTestId('quality-gate-permissions'));
+ expect(permissionList.getByRole('listitem')).toBeInTheDocument();
// Delete the user permission
const deleteButton = screen.getByTestId('permission-delete-button');
@@ -554,7 +552,7 @@ describe('The Permissions section', () => {
const deletePopup = screen.getByRole('dialog');
const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
await user.click(dialogDeleteButton);
- expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ expect(permissionList.queryByRole('listitem')).not.toBeInTheDocument();
});
it('should assign permission to a group and delete it later', async () => {
@@ -586,7 +584,8 @@ describe('The Permissions section', () => {
const deletePopup = screen.getByRole('dialog');
const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
await user.click(dialogDeleteButton);
- expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ const permissionList = within(await screen.findByTestId('quality-gate-permissions'));
+ expect(permissionList.queryByRole('listitem')).not.toBeInTheDocument();
});
it('should handle searchUser service failure', async () => {
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/styles.css b/server/sonar-web/src/main/js/apps/quality-gates/styles.css
index 6591d52354f..f4703e59887 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/styles.css
+++ b/server/sonar-web/src/main/js/apps/quality-gates/styles.css
@@ -30,10 +30,6 @@
background-color: var(--rowHoverHighlight);
}
-.cayc-conditions-wrapper {
- background-color: var(--barBackgroundColor);
-}
-
.quality-gate-section tbody {
border: 1px solid var(--disableGrayBorder);
}
@@ -43,6 +39,10 @@
border-bottom: 1px solid var(--disableGrayBorder);
}
+.quality-gate-section td {
+ height: 24px;
+}
+
.quality-gate-section tr th {
font-weight: 400 !important;
font-size: 11px;
@@ -57,24 +57,8 @@
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);
+.cayc-success-banner ul {
+ list-style: disc;
}
.bordered-bottom-cayc {
@@ -85,14 +69,15 @@
color: var(--alertTextWarning);
}
-.cayc-warning-description {
- line-height: 18px;
+.cayc-success-header {
+ color: var(--alertTextSuccess);
}
-.green-text {
- color: var(--success500);
+.cayc-warning-description {
+ line-height: 18px;
}
-.red-text {
- color: var(--red);
+.qg-unfollow-cayc {
+ border: 1px solid var(--neutral200);
+ background-color: var(--neutral50);
}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts
index c532e9e5ed3..be68153f64b 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts
+++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts
@@ -21,7 +21,7 @@ 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 } = {
+const CAYC_CONDITIONS: { [key: string]: Condition } = {
new_reliability_rating: {
error: '1',
id: 'new_reliability_rating',
@@ -46,48 +46,71 @@ const CAYC_CONDITIONS_WITH_EXPECTED_VALUE: { [key: string]: Condition } = {
metric: 'new_security_hotspots_reviewed',
op: 'LT',
},
+ new_coverage: {
+ id: 'AXJMbIUHPAOIsUIE3eOF',
+ metric: 'new_coverage',
+ op: 'LT',
+ error: '80',
+ },
+ new_duplicated_lines_density: {
+ id: 'AXJMbIUHPAOIsUIE3eOG',
+ metric: 'new_duplicated_lines_density',
+ op: 'GT',
+ error: '3',
+ },
};
-export function getCaycConditions(conditions: Condition[]) {
- return conditions.filter((condition) => isCaycCondition(condition));
-}
+export const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE = ['new_duplicated_lines_density', 'new_coverage'];
export function isCaycCondition(condition: Condition) {
- return condition.metric in CAYC_CONDITIONS_WITH_EXPECTED_VALUE;
-}
-
-export function isCaycWeakCondition(condition: Condition) {
- return (
- isCaycCondition(condition) &&
- CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric].error !== condition.error
- );
+ return condition.metric in CAYC_CONDITIONS;
}
-export function getWeakAndMissingConditions(conditions: Condition[]) {
+export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) {
const result: {
weakConditions: Condition[];
missingConditions: Condition[];
+ nonCaycConditions: Condition[];
} = {
weakConditions: [],
missingConditions: [],
+ nonCaycConditions: [],
};
- Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).forEach((key) => {
+ Object.keys(CAYC_CONDITIONS).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.missingConditions.push(CAYC_CONDITIONS[key]);
+ } else if (
+ !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) &&
+ CAYC_CONDITIONS[key].error !== selectedCondition.error
+ ) {
result.weakConditions.push(selectedCondition);
}
});
+
+ result.nonCaycConditions = getNonCaycConditions(conditions);
return result;
}
-export function getOthersConditions(conditions: Condition[]) {
+export function getCaycConditionsWithCorrectValue(conditions: Condition[]) {
+ return Object.keys(CAYC_CONDITIONS).map((key) => {
+ const selectedCondition = conditions.find((condition) => condition.metric === key);
+ if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && selectedCondition) {
+ return selectedCondition;
+ }
+ return CAYC_CONDITIONS[key];
+ });
+}
+
+export function getNonCaycConditions(conditions: Condition[]) {
return conditions.filter((condition) => !isCaycCondition(condition));
}
export function getCorrectCaycCondition(condition: Condition) {
- return CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric];
+ if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric)) {
+ return condition;
+ }
+ return CAYC_CONDITIONS[condition.metric];
}
export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): boolean {
@@ -98,12 +121,18 @@ export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): b
export function addCondition(qualityGate: QualityGate, condition: Condition): QualityGate {
const oldConditions = qualityGate.conditions || [];
const conditions = [...oldConditions, condition];
+ if (conditions) {
+ qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions);
+ }
return { ...qualityGate, conditions };
}
export function deleteCondition(qualityGate: QualityGate, condition: Condition): QualityGate {
const conditions =
qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition);
+ if (conditions) {
+ qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions);
+ }
return { ...qualityGate, conditions };
}
@@ -117,9 +146,36 @@ export function replaceCondition(
qualityGate.conditions.map((candidate) => {
return candidate === oldCondition ? newCondition : candidate;
});
+ if (conditions) {
+ qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions);
+ }
+
return { ...qualityGate, conditions };
}
+export function updateCaycComplaintStatus(conditions: Condition[]) {
+ if (conditions.length !== Object.keys(CAYC_CONDITIONS).length) {
+ return false;
+ }
+
+ for (const key of Object.keys(CAYC_CONDITIONS)) {
+ const selectedCondition = conditions.find((condition) => condition.metric === key);
+ if (!selectedCondition) {
+ return false;
+ }
+
+ if (
+ !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) &&
+ selectedCondition &&
+ selectedCondition.error !== CAYC_CONDITIONS[key].error
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
export function getPossibleOperators(metric: Metric) {
if (metric.direction === 1) {
return 'LT';