diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-01-11 14:02:45 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-01-12 20:02:52 +0000 |
commit | 105204fa4727f1c77e0f293cc2a9dfa984493e7b (patch) | |
tree | ef5bdc22cd455d1225f4c153ceab97b2b1a46e6f /server/sonar-web | |
parent | e3100a5030d09dc7df418d58c1ca5946fa4b965f (diff) | |
download | sonarqube-105204fa4727f1c77e0f293cc2a9dfa984493e7b.tar.gz sonarqube-105204fa4727f1c77e0f293cc2a9dfa984493e7b.zip |
SONAR-17815 New UI changes for CAYC quality gates
Diffstat (limited to 'server/sonar-web')
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'; |