darkBackgroundSeparator: '#413b3b',
darkBackgroundFontColor: '#f6f8fa',
+ //quality gate badges
+ badgeGreenBackground: '#f0faec',
+ badgeRedBackground: '#ffeaea',
+
// new color palette
// Some of these colors duplicate what we have above, but this will make it
// easier to align with the UX designers on what colors to use where.
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DocLink from '../../../components/common/DocLink';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ badgeType: 'missing' | 'weak' | 'ok';
+}
+
+export default function CaycBadgeTooltip({ badgeType }: Props) {
+ return (
+ <div>
+ <p className="spacer-bottom padded-bottom bordered-bottom-cayc">
+ {translate('quality_gates.cayc.tooltip', badgeType)}
+ </p>
+ <DocLink to="/user-guide/clean-as-you-code/">
+ {translate('quality_gates.cayc.badge.tooltip.learn_more')}
+ </DocLink>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import classNames from 'classnames';
+import * as React from 'react';
+import Tooltip from '../../../components/controls/Tooltip';
+import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
+import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
+import CheckIcon from '../../../components/icons/CheckIcon';
+import { translate } from '../../../helpers/l10n';
+import { Condition } from '../../../types/types';
+import { isCaycWeakCondition } from '../utils';
+import CaycBadgeTooltip from './CaycBadgeTooltip';
+
+interface Props {
+ className?: string;
+ isMissingCondition?: boolean;
+ condition: Condition;
+ isCaycModal?: boolean;
+}
+
+export default function CaycStatusBadge({
+ className,
+ isMissingCondition,
+ condition,
+ isCaycModal,
+}: Props) {
+ if (isMissingCondition && !isCaycModal) {
+ return (
+ <Tooltip overlay={<CaycBadgeTooltip badgeType="missing" />}>
+ <div className={classNames('badge qg-cayc-missing-badge display-flex-center', className)}>
+ <AlertErrorIcon className="spacer-right" />
+ <span>{translate('quality_gates.cayc_condition.missing')}</span>
+ </div>
+ </Tooltip>
+ );
+ } else if (isCaycWeakCondition(condition) && !isCaycModal) {
+ return (
+ <Tooltip overlay={<CaycBadgeTooltip badgeType="weak" />}>
+ <div className={classNames('badge qg-cayc-weak-badge display-flex-center', className)}>
+ <AlertWarnIcon className="spacer-right" />
+ <span>{translate('quality_gates.cayc_condition.weak')}</span>
+ </div>
+ </Tooltip>
+ );
+ }
+ return (
+ <Tooltip overlay={<CaycBadgeTooltip badgeType="ok" />}>
+ <div className={classNames('badge qg-cayc-ok-badge display-flex-center', className)}>
+ <CheckIcon className="spacer-right" />
+ <span>{translate('quality_gates.cayc_condition.ok')}</span>
+ </div>
+ </Tooltip>
+ );
+}
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
import { DeleteButton, EditButton } from '../../../components/controls/buttons';
import ConfirmModal from '../../../components/controls/ConfirmModal';
+import { Alert } from '../../../components/ui/Alert';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
-import { getLocalizedMetricNameNoDiffMetric } from '../utils';
+import { getLocalizedMetricNameNoDiffMetric, isCaycCondition } from '../utils';
+import CaycStatusBadge from './CaycStatusBadge';
import ConditionModal from './ConditionModal';
+import ConditionValue from './ConditionValue';
interface Props {
condition: ConditionType;
qualityGate: QualityGate;
updated?: boolean;
metrics: Dict<Metric>;
+ showEdit?: boolean;
+ isMissingCondition?: boolean;
+ isCaycModal?: boolean;
}
interface State {
}
render() {
- const { condition, canEdit, metric, qualityGate, updated, metrics } = this.props;
+ const {
+ condition,
+ canEdit,
+ metric,
+ qualityGate,
+ updated,
+ metrics,
+ showEdit = true,
+ isMissingCondition = false,
+ isCaycModal = false,
+ } = this.props;
+
return (
<tr className={classNames({ highlighted: updated })}>
- <td className="text-middle">
+ <td
+ className={classNames('text-middle', {
+ 'text-through': isMissingCondition && !isCaycModal,
+ 'green-text': isMissingCondition && isCaycModal,
+ })}
+ >
{getLocalizedMetricNameNoDiffMetric(metric, metrics)}
{metric.hidden && (
<span className="text-danger little-spacer-left">{translate('deprecated')}</span>
)}
</td>
- <td className="text-middle nowrap">{this.renderOperator()}</td>
-
- <td className="text-middle nowrap">{formatMeasure(condition.error, metric.type)}</td>
+ <td
+ className={classNames('text-middle nowrap', {
+ 'text-through': isMissingCondition && !isCaycModal,
+ 'green-text': isMissingCondition && isCaycModal,
+ })}
+ >
+ {this.renderOperator()}
+ </td>
- {canEdit && (
- <>
- <td className="text-center thin">
+ <td
+ className={classNames('text-middle nowrap', {
+ 'text-through': isMissingCondition && !isCaycModal,
+ 'green-text': isMissingCondition && isCaycModal,
+ })}
+ >
+ <ConditionValue metric={metric} isCaycModal={isCaycModal} condition={condition} />
+ </td>
+ <td className="text-middle nowrap display-flex-justify-end">
+ {isCaycCondition(condition) && (
+ <CaycStatusBadge
+ isMissingCondition={isMissingCondition}
+ condition={condition}
+ isCaycModal={isCaycModal}
+ className="spacer-right"
+ />
+ )}
+ {canEdit && showEdit && !isMissingCondition && (
+ <>
<EditButton
aria-label={translateWithParameters('quality_gates.condition.edit', metric.name)}
data-test="quality-gates__condition-update"
onClick={this.handleOpenUpdate}
+ className="spacer-right"
/>
- </td>
- <td className="text-center thin">
<DeleteButton
aria-label={translateWithParameters('quality_gates.condition.delete', metric.name)}
data-test="quality-gates__condition-delete"
onClick={this.handleDeleteClick}
/>
- </td>
- {this.state.modal && (
- <ConditionModal
- condition={condition}
- header={translate('quality_gates.update_condition')}
- metric={metric}
- onAddCondition={this.handleUpdateCondition}
- onClose={this.handleUpdateClose}
- qualityGate={qualityGate}
- />
- )}
- {this.state.deleteFormOpen && (
- <ConfirmModal
- confirmButtonText={translate('delete')}
- confirmData={condition}
- header={translate('quality_gates.delete_condition')}
- isDestructive={true}
- onClose={this.closeDeleteForm}
- onConfirm={this.removeCondition}
- >
- {translateWithParameters(
- 'quality_gates.delete_condition.confirm.message',
- getLocalizedMetricName(this.props.metric)
- )}
- </ConfirmModal>
- )}
- </>
- )}
+ {this.state.modal && (
+ <ConditionModal
+ condition={condition}
+ header={translate('quality_gates.update_condition')}
+ metric={metric}
+ onAddCondition={this.handleUpdateCondition}
+ onClose={this.handleUpdateClose}
+ qualityGate={qualityGate}
+ />
+ )}
+ {this.state.deleteFormOpen && (
+ <ConfirmModal
+ confirmButtonText={translate('delete')}
+ confirmData={condition}
+ header={translate('quality_gates.delete_condition')}
+ isDestructive={true}
+ onClose={this.closeDeleteForm}
+ onConfirm={this.removeCondition}
+ >
+ {translateWithParameters(
+ 'quality_gates.delete_condition.confirm.message',
+ getLocalizedMetricName(this.props.metric)
+ )}
+ {isCaycCondition(condition) && (
+ <Alert className="big-spacer-bottom big-spacer-top" variant="warning">
+ <p className="cayc-warning-description">
+ {translate('quality_gates.cayc_condition.delete_warning')}
+ </p>
+ </Alert>
+ )}
+ </ConfirmModal>
+ )}
+ </>
+ )}
+ </td>
</tr>
);
}
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures';
import { Condition, Metric, QualityGate } from '../../../types/types';
-import { getPossibleOperators } from '../utils';
+import { getPossibleOperators, isCaycCondition, isCaycWeakCondition } from '../utils';
import ConditionOperator from './ConditionOperator';
import MetricSelect from './MetricSelect';
import ThresholdInput from './ThresholdInput';
};
render() {
- const { header, metrics, onClose } = this.props;
+ const { header, metrics, onClose, condition } = this.props;
const { op, error, scope, metric } = this.state;
return (
<ConfirmModal
</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')}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { createCondition, updateCondition } from '../../../api/quality-gates';
+import ConfirmModal from '../../../components/controls/ConfirmModal';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Condition, Dict, Metric, QualityGate } from '../../../types/types';
+import { getCorrectCaycCondition, getWeakAndMissingConditions } from '../utils';
+import ConditionsTable from './ConditionsTable';
+
+interface Props {
+ canEdit: boolean;
+ metrics: Dict<Metric>;
+ updatedConditionId?: string;
+ conditions: Condition[];
+ scope: 'new' | 'overall' | 'new-cayc';
+ onClose: () => void;
+ onAddCondition: (condition: Condition) => void;
+ onRemoveCondition: (Condition: Condition) => void;
+ onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void;
+ qualityGate: QualityGate;
+}
+
+export default class CaycReviewUpdateConditionsModal extends React.PureComponent<Props> {
+ updateCaycQualityGate = () => {
+ const { conditions, qualityGate } = this.props;
+ const promiseArr: Promise<Condition | undefined>[] = [];
+ const { weakConditions, missingConditions } = getWeakAndMissingConditions(conditions);
+
+ weakConditions.forEach((condition) => {
+ promiseArr.push(
+ updateCondition({
+ ...getCorrectCaycCondition(condition),
+ id: condition.id,
+ }).catch(() => undefined)
+ );
+ });
+
+ missingConditions.forEach((condition) => {
+ promiseArr.push(
+ createCondition({
+ ...getCorrectCaycCondition(condition),
+ gateId: qualityGate.id,
+ }).catch(() => undefined)
+ );
+ });
+
+ return Promise.all(promiseArr).then((data) => {
+ data.forEach((condition) => {
+ if (condition === undefined) {
+ return;
+ }
+ const currentCondition = conditions.find((con) => con.metric === condition.metric);
+ if (currentCondition) {
+ this.props.onSaveCondition(condition, currentCondition);
+ } else {
+ this.props.onAddCondition(condition);
+ }
+ });
+ });
+ };
+
+ render() {
+ const { conditions, qualityGate } = this.props;
+ const { weakConditions, missingConditions } = getWeakAndMissingConditions(conditions);
+
+ return (
+ <ConfirmModal
+ header={translateWithParameters(
+ 'quality_gates.cayc.review_update_modal.header',
+ qualityGate.name
+ )}
+ confirmButtonText={translate('quality_gates.cayc.review_update_modal.confirm_text')}
+ onClose={this.props.onClose}
+ onConfirm={this.updateCaycQualityGate}
+ size="medium"
+ >
+ <div className="quality-gate-section">
+ <p className="big-spacer-bottom">
+ {translate('quality_gates.cayc.review_update_modal.description')}
+ </p>
+
+ {weakConditions.length > 0 && (
+ <>
+ <h4 className="spacer-top spacer-bottom">
+ {translateWithParameters(
+ 'quality_gates.cayc.review_update_modal.modify_condition.header',
+ weakConditions.length
+ )}
+ </h4>
+ <ConditionsTable
+ {...this.props}
+ conditions={weakConditions}
+ showEdit={false}
+ isCaycModal={true}
+ />
+ </>
+ )}
+
+ {missingConditions.length > 0 && (
+ <>
+ <h4 className="spacer-top spacer-bottom">
+ {translateWithParameters(
+ 'quality_gates.cayc.review_update_modal.add_condition.header',
+ missingConditions.length
+ )}
+ </h4>
+ <ConditionsTable
+ {...this.props}
+ conditions={[]}
+ showEdit={false}
+ missingConditions={missingConditions}
+ isCaycModal={true}
+ />
+ </>
+ )}
+ </div>
+ </ConfirmModal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { formatMeasure } from '../../../helpers/measures';
+import { Condition, Metric } from '../../../types/types';
+import { getCorrectCaycCondition, isCaycCondition, isCaycWeakCondition } from '../utils';
+import ConditionValueDescription from './ConditionValueDescription';
+
+interface Props {
+ condition: Condition;
+ isCaycModal?: boolean;
+ metric: Metric;
+}
+
+function ConditionValue({ condition, isCaycModal, metric }: Props) {
+ if (isCaycModal) {
+ return (
+ <>
+ {isCaycWeakCondition(condition) && (
+ <span className="spacer-right text-through red-text">
+ {formatMeasure(condition.error, metric.type)}
+ </span>
+ )}
+ <span className="green-text spacer-right">
+ {formatMeasure(getCorrectCaycCondition(condition).error, metric.type)}
+ </span>
+
+ <ConditionValueDescription
+ condition={getCorrectCaycCondition(condition)}
+ metric={metric}
+ className="green-text"
+ />
+ </>
+ );
+ }
+
+ if (isCaycWeakCondition(condition)) {
+ return (
+ <>
+ <span className="spacer-right red-text">{formatMeasure(condition.error, metric.type)}</span>
+ <ConditionValueDescription condition={condition} metric={metric} />
+ </>
+ );
+ }
+
+ return (
+ <>
+ <span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span>
+ {isCaycCondition(condition) && (
+ <ConditionValueDescription condition={condition} metric={metric} />
+ )}
+ </>
+ );
+}
+
+export default ConditionValue;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import {
+ getMaintainabilityGrid,
+ GRID_INDEX_OFFSET,
+ PERCENT_MULTIPLIER,
+} from '../../../helpers/ratings';
+import { AppState } from '../../../types/appstate';
+import { GlobalSettingKeys } from '../../../types/settings';
+import { Condition, Metric } from '../../../types/types';
+import { isCaycCondition } from '../utils';
+
+interface Props {
+ appState: AppState;
+ condition: Condition;
+ metric: Metric;
+ className?: string;
+}
+
+function ConditionValueDescription({
+ condition,
+ appState: { settings },
+ metric,
+ className = '',
+}: Props) {
+ if (condition.metric === 'new_maintainability_rating') {
+ const maintainabilityGrid = getMaintainabilityGrid(
+ settings[GlobalSettingKeys.RatingGrid] ?? ''
+ );
+ const maintainabilityRatingThreshold =
+ maintainabilityGrid[Math.floor(Number(condition.error)) - GRID_INDEX_OFFSET];
+ const ratingLetter = formatMeasure(condition.error, 'RATING');
+
+ return (
+ <span className={className}>
+ (
+ {condition.error === '1'
+ ? translateWithParameters(
+ 'quality_gates.cayc.new_maintainability_rating.A',
+ formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, 'PERCENT')
+ )
+ : translateWithParameters(
+ 'quality_gates.cayc.new_maintainability_rating',
+ ratingLetter,
+ formatMeasure(maintainabilityRatingThreshold * PERCENT_MULTIPLIER, 'PERCENT')
+ )}
+ )
+ </span>
+ );
+ }
+
+ return (
+ <span className={className}>
+ {isCaycCondition(condition) && condition.metric !== 'new_security_hotspots_reviewed' && (
+ <>
+ (
+ {translate(
+ `quality_gates.cayc.${condition.metric}.${formatMeasure(condition.error, metric.type)}`
+ )}
+ )
+ </>
+ )}
+ </span>
+ );
+}
+
+export default withAppStateContext(ConditionValueDescription);
import { Feature } from '../../../types/features';
import { MetricKey } from '../../../types/metrics';
import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
-import Condition from './Condition';
+import { getCaycConditions, getOthersConditions } from '../utils';
+import CaycConditions from './CaycConditions';
import ConditionModal from './ConditionModal';
+import ConditionsTable from './ConditionsTable';
interface Props extends WithAvailableFeaturesProps {
canEdit: boolean;
updatedConditionId,
} = this.props;
- const captionTranslationId =
- scope === 'new'
- ? 'quality_gates.conditions.new_code'
- : 'quality_gates.conditions.overall_code';
return (
- <table className="data zebra" data-test={`quality-gates__conditions-${scope}`}>
- <caption>
- <h4>{translate(captionTranslationId, 'long')}</h4>
-
- {this.props.hasFeature(Feature.BranchSupport) && (
- <p className="spacer-top spacer-bottom">
- {translate(captionTranslationId, 'description')}
- </p>
- )}
- </caption>
- <thead>
- <tr>
- <th className="nowrap" style={{ width: 300 }}>
- {translate('quality_gates.conditions.metric')}
- </th>
- <th className="nowrap">{translate('quality_gates.conditions.operator')}</th>
- <th className="nowrap">{translate('quality_gates.conditions.value')}</th>
- {canEdit && (
- <>
- <th className="thin">{translate('edit')}</th>
- <th className="thin">{translate('delete')}</th>
- </>
- )}
- </tr>
- </thead>
- <tbody>
- {conditions.map((condition) => (
- <Condition
- canEdit={canEdit}
- condition={condition}
- key={condition.id}
- metric={metrics[condition.metric]}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- qualityGate={qualityGate}
- updated={condition.id === updatedConditionId}
- />
- ))}
- </tbody>
- </table>
+ <ConditionsTable
+ qualityGate={qualityGate}
+ metrics={metrics}
+ canEdit={canEdit}
+ onRemoveCondition={onRemoveCondition}
+ onSaveCondition={onSaveCondition}
+ updatedConditionId={updatedConditionId}
+ conditions={getOthersConditions(conditions)}
+ scope={scope}
+ />
);
};
render() {
- const { conditions, metrics, canEdit } = this.props;
+ const {
+ qualityGate,
+ metrics,
+ canEdit,
+ onAddCondition,
+ onRemoveCondition,
+ onSaveCondition,
+ updatedConditionId,
+ conditions,
+ } = this.props;
const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
const sortedConditions = sortBy(
</div>
)}
- <header className="display-flex-center spacer-bottom">
- <h3>{translate('quality_gates.conditions')}</h3>
+ <header className="display-flex-center">
+ <h2 className="big">{translate('quality_gates.conditions')}</h2>
<DocumentationTooltip
className="spacer-left"
content={translate('quality_gates.conditions.help')}
{sortedConditionsOnNewMetrics.length > 0 && (
<div className="big-spacer-top">
- {this.renderConditionsTable(sortedConditionsOnNewMetrics, 'new')}
+ <h3 className="medium text-normal">
+ {translate('quality_gates.conditions.new_code', 'long')}
+ </h3>
+ {this.props.hasFeature(Feature.BranchSupport) && (
+ <p className="spacer-top spacer-bottom">
+ {translate('quality_gates.conditions.new_code', 'description')}
+ </p>
+ )}
+
+ <CaycConditions
+ qualityGate={qualityGate}
+ metrics={metrics}
+ canEdit={canEdit}
+ onRemoveCondition={onRemoveCondition}
+ onAddCondition={onAddCondition}
+ onSaveCondition={onSaveCondition}
+ updatedConditionId={updatedConditionId}
+ conditions={getCaycConditions(sortedConditionsOnNewMetrics)}
+ scope="new-cayc"
+ />
+
+ <h4>{translate('quality_gates.other_conditions')}</h4>
+ <ConditionsTable
+ qualityGate={qualityGate}
+ metrics={metrics}
+ canEdit={canEdit}
+ onRemoveCondition={onRemoveCondition}
+ onSaveCondition={onSaveCondition}
+ updatedConditionId={updatedConditionId}
+ conditions={getOthersConditions(sortedConditionsOnNewMetrics)}
+ scope="new"
+ />
</div>
)}
{sortedConditionsOnOverallMetrics.length > 0 && (
<div className="big-spacer-top">
- {this.renderConditionsTable(sortedConditionsOnOverallMetrics, 'overall')}
+ <h3 className="medium text-normal">
+ {translate('quality_gates.conditions.overall_code', 'long')}
+ </h3>
+
+ {this.props.hasFeature(Feature.BranchSupport) && (
+ <p className="spacer-top spacer-bottom">
+ {translate('quality_gates.conditions.overall_code', 'description')}
+ </p>
+ )}
+
+ <ConditionsTable
+ qualityGate={qualityGate}
+ metrics={metrics}
+ canEdit={canEdit}
+ onRemoveCondition={onRemoveCondition}
+ onSaveCondition={onSaveCondition}
+ updatedConditionId={updatedConditionId}
+ conditions={sortedConditionsOnOverallMetrics}
+ scope="overall"
+ />
</div>
)}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
+import Condition from './Condition';
+
+interface Props {
+ canEdit: boolean;
+ metrics: Dict<Metric>;
+ onRemoveCondition: (Condition: ConditionType) => void;
+ onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
+ qualityGate: QualityGate;
+ updatedConditionId?: string;
+ conditions: ConditionType[];
+ missingConditions?: ConditionType[];
+ scope: 'new' | 'overall' | 'new-cayc';
+ showEdit?: boolean;
+ isCaycModal?: boolean;
+}
+
+export default class ConditionsTable extends React.PureComponent<Props> {
+ render() {
+ const {
+ qualityGate,
+ metrics,
+ canEdit,
+ onRemoveCondition,
+ onSaveCondition,
+ updatedConditionId,
+ scope,
+ conditions,
+ missingConditions,
+ showEdit,
+ isCaycModal,
+ } = this.props;
+
+ return (
+ <table
+ className="data zebra"
+ data-test={`quality-gates__conditions-${scope}`}
+ data-testid={`quality-gates__conditions-${scope}`}
+ >
+ <thead>
+ <tr>
+ <th className="nowrap abs-width-300">{translate('quality_gates.conditions.metric')}</th>
+ <th className="nowrap">{translate('quality_gates.conditions.operator')}</th>
+ <th className="nowrap">{translate('quality_gates.conditions.value')}</th>
+ <th className="thin"> </th>
+ </tr>
+ </thead>
+ <tbody>
+ {conditions.map((condition) => (
+ <Condition
+ canEdit={canEdit}
+ condition={condition}
+ key={condition.id}
+ metric={metrics[condition.metric]}
+ onRemoveCondition={onRemoveCondition}
+ onSaveCondition={onSaveCondition}
+ qualityGate={qualityGate}
+ updated={condition.id === updatedConditionId}
+ showEdit={showEdit}
+ isCaycModal={isCaycModal}
+ />
+ ))}
+
+ {missingConditions &&
+ missingConditions.map((condition) => (
+ <Condition
+ canEdit={canEdit}
+ condition={condition}
+ key={condition.id}
+ metric={metrics[condition.metric]}
+ onRemoveCondition={onRemoveCondition}
+ onSaveCondition={onSaveCondition}
+ qualityGate={qualityGate}
+ updated={condition.id === updatedConditionId}
+ showEdit={showEdit}
+ isMissingCondition={true}
+ isCaycModal={isCaycModal}
+ />
+ ))}
+ </tbody>
+ </table>
+ );
+ }
+}
key={qualityGate.id}
to={getQualityGateUrl(String(qualityGate.id))}
>
- <span className="flex-1">{qualityGate.name}</span>
+ <span className="flex-1 text-ellipsis" title={qualityGate.name}>
+ {qualityGate.name}
+ </span>
{qualityGate.isDefault && (
<span className="badge little-spacer-left">{translate('default')}</span>
)}
renderElement={this.renderElement}
selectedElements={this.state.selectedProjects}
withPaging={true}
+ autoFocusSearch={false}
/>
);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { screen, waitFor, within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import selectEvent from 'react-select-event';
-import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
-import { searchProjects, searchUsers } from '../../../../api/quality-gates';
-import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils';
-import { Feature } from '../../../../types/features';
-import routes from '../../routes';
-
-jest.mock('../../../../api/quality-gates');
-
-let handler: QualityGatesServiceMock;
-
-beforeAll(() => {
- handler = new QualityGatesServiceMock();
-});
-
-afterEach(() => handler.reset());
-
-it('should open the default quality gates', async () => {
- renderQualityGateApp();
-
- expect(await screen.findAllByRole('menuitem')).toHaveLength(handler.list.length);
-
- const defaultQualityGate = handler.getDefaultQualityGate();
- expect(await screen.findAllByText(defaultQualityGate.name)).toHaveLength(2);
-});
-
-it('should list all quality gates', async () => {
- renderQualityGateApp();
-
- expect(
- await screen.findByRole('menuitem', {
- name: `${handler.getDefaultQualityGate().name} default`,
- })
- ).toBeInTheDocument();
- expect(
- await screen.findByRole('menuitem', {
- name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
- })
- ).toBeInTheDocument();
-});
-
-it('should be able to create a quality gate then delete it', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
- let createButton = await screen.findByRole('button', { name: 'create' });
-
- // Using keyboard
- await user.click(createButton);
- let nameInput = screen.getByRole('textbox', { name: /name.*/ });
- expect(nameInput).toBeInTheDocument();
- await user.click(nameInput);
- await user.keyboard('testone{Enter}');
- expect(await screen.findByRole('menuitem', { name: 'testone' })).toBeInTheDocument();
-
- // Using modal button
- createButton = await screen.findByRole('button', { name: 'create' });
- await user.click(createButton);
- nameInput = screen.getByRole('textbox', { name: /name.*/ });
- const saveButton = screen.getByRole('button', { name: 'save' });
-
- expect(saveButton).toBeDisabled();
- await user.click(nameInput);
- await user.keyboard('testtwo');
- await user.click(saveButton);
-
- const newQG = await screen.findByRole('menuitem', { name: 'testtwo' });
- expect(newQG).toBeInTheDocument();
-
- // Delete the quality gate
- await user.click(newQG);
- const deleteButton = await screen.findByRole('button', { name: 'delete' });
- await user.click(deleteButton);
- const popup = screen.getByRole('dialog');
- const dialogDeleteButton = within(popup).getByRole('button', { name: 'delete' });
- await user.click(dialogDeleteButton);
-
- await waitFor(() => {
- expect(screen.queryByRole('menuitem', { name: 'testtwo' })).not.toBeInTheDocument();
- });
-});
-
-it('should be able to copy a quality gate', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const copyButton = await screen.findByRole('button', { name: 'copy' });
-
- await user.click(copyButton);
- const nameInput = screen.getByRole('textbox', { name: /name.*/ });
- expect(nameInput).toBeInTheDocument();
- await user.click(nameInput);
- await user.keyboard(' bis{Enter}');
-
- expect(await screen.findByRole('menuitem', { name: /.* bis/ })).toBeInTheDocument();
-});
-
-it('should be able to rename a quality gate', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const renameButton = await screen.findByRole('button', { name: 'rename' });
-
- await user.click(renameButton);
- const nameInput = screen.getByRole('textbox', { name: /name.*/ });
- expect(nameInput).toBeInTheDocument();
- await user.click(nameInput);
- await user.keyboard('{Control>}a{/Control}New Name{Enter}');
-
- expect(await screen.findByRole('menuitem', { name: /New Name.*/ })).toBeInTheDocument();
-});
-
-it('should be able to set as default a quality gate', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
- await user.click(notDefaultQualityGate);
- const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
- await user.click(setAsDefaultButton);
- expect(screen.getAllByRole('menuitem')[1]).toHaveTextContent('default');
-});
-
-it('should be able to add a condition', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- // On new code
- await user.click(await screen.findByText('quality_gates.add_condition'));
-
- let dialog = within(screen.getByRole('dialog'));
-
- await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' }));
- await selectEvent.select(dialog.getByRole('textbox'), ['Issues']);
- await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
- await user.keyboard('12{Enter}');
-
- const newConditions = within(
- await screen.findByRole('table', { name: 'quality_gates.conditions.new_code.long' })
- );
- expect(await newConditions.findByRole('cell', { name: 'Issues' })).toBeInTheDocument();
- expect(await newConditions.findByRole('cell', { name: '12' })).toBeInTheDocument();
-
- // On overall code
- await user.click(await screen.findByText('quality_gates.add_condition'));
-
- dialog = within(screen.getByRole('dialog'));
- await selectEvent.select(dialog.getByRole('textbox'), ['Info Issues']);
- await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
- await user.click(dialog.getByLabelText('quality_gates.conditions.operator'));
-
- await user.click(dialog.getByText('quality_gates.operator.LT'));
- await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
- await user.keyboard('42{Enter}');
-
- let overallConditions = within(
- await screen.findByRole('table', { name: 'quality_gates.conditions.overall_code.long' })
- );
-
- expect(await overallConditions.findByRole('cell', { name: 'Info Issues' })).toBeInTheDocument();
- expect(await overallConditions.findByRole('cell', { name: '42' })).toBeInTheDocument();
-
- // Select a rating
- await user.click(await screen.findByText('quality_gates.add_condition'));
-
- dialog = within(screen.getByRole('dialog'));
- await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
- await selectEvent.select(dialog.getByRole('textbox'), ['Maintainability Rating']);
- await user.click(dialog.getByLabelText('quality_gates.conditions.value'));
- await user.click(dialog.getByText('B'));
- await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
-
- overallConditions = within(
- await screen.findByRole('table', { name: 'quality_gates.conditions.overall_code.long' })
- );
-
- expect(
- await overallConditions.findByRole('cell', { name: 'Maintainability Rating' })
- ).toBeInTheDocument();
- expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument();
-});
-
-it('should be able to edit a condition', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const newConditions = within(
- await screen.findByRole('table', {
- name: 'quality_gates.conditions.new_code.long',
- })
- );
-
- await user.click(
- newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code')
- );
- const dialog = within(screen.getByRole('dialog'));
- await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
- await user.keyboard('{Backspace}{Backspace}23{Enter}');
-
- expect(await newConditions.findByText('Coverage')).toBeInTheDocument();
- expect(await newConditions.findByText('23.0%')).toBeInTheDocument();
-});
-
-it('should be able to handle duplicate or deprecated condition', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
- await user.click(
- await screen.findByRole('menuitem', { name: handler.getCorruptedQualityGateName() })
- );
-
- expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
- expect(
- await screen.findByRole('cell', { name: 'Complexity / Function deprecated' })
- ).toBeInTheDocument();
-});
-
-it('should be able to handle delete condition', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const newConditions = within(
- await screen.findByRole('table', {
- name: 'quality_gates.conditions.new_code.long',
- })
- );
-
- await user.click(
- newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code')
- );
-
- const dialog = within(screen.getByRole('dialog'));
- await user.click(dialog.getByRole('button', { name: 'delete' }));
-
- await waitFor(() => {
- expect(newConditions.queryByRole('cell', { name: 'Coverage' })).not.toBeInTheDocument();
- });
-});
-
-it('should explain condition on branch', async () => {
- renderQualityGateApp({ featureList: [Feature.BranchSupport] });
-
- expect(
- await screen.findByText('quality_gates.conditions.new_code.description')
- ).toBeInTheDocument();
- expect(
- await screen.findByText('quality_gates.conditions.overall_code.description')
- ).toBeInTheDocument();
-});
-
-describe('The Project section', () => {
- it('should render list of projects correctly in different tabs', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-
- await user.click(notDefaultQualityGate);
-
- // by default it shows "selected" values
- expect(screen.getAllByRole('checkbox')).toHaveLength(2);
-
- // change tabs to show deselected projects
- await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
- expect(screen.getAllByRole('checkbox')).toHaveLength(2);
-
- // change tabs to show all projects
- await user.click(screen.getByRole('button', { name: 'quality_gates.projects.all' }));
- expect(screen.getAllByRole('checkbox')).toHaveLength(4);
- });
-
- it('should handle select and deselect correctly', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-
- await user.click(notDefaultQualityGate);
-
- const checkedProjects = screen.getAllByRole('checkbox')[0];
- expect(screen.getAllByRole('checkbox')).toHaveLength(2);
- await user.click(checkedProjects);
- const reloadButton = screen.getByRole('button', { name: 'reload' });
- expect(reloadButton).toBeInTheDocument();
- await user.click(reloadButton);
-
- // FP
- // eslint-disable-next-line jest-dom/prefer-in-document
- expect(screen.getAllByRole('checkbox')).toHaveLength(1);
-
- // change tabs to show deselected projects
- await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
-
- const uncheckedProjects = screen.getAllByRole('checkbox')[0];
- expect(screen.getAllByRole('checkbox')).toHaveLength(3);
- await user.click(uncheckedProjects);
- expect(reloadButton).toBeInTheDocument();
- await user.click(reloadButton);
- expect(screen.getAllByRole('checkbox')).toHaveLength(2);
- });
-
- it('should handle the search of projects', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
-
- await user.click(notDefaultQualityGate);
-
- const searchInput = screen.getByRole('searchbox', { name: 'search_verb' });
- expect(searchInput).toBeInTheDocument();
- await user.click(searchInput);
- await user.keyboard('test2{Enter}');
-
- // FP
- // eslint-disable-next-line jest-dom/prefer-in-document
- expect(screen.getAllByRole('checkbox')).toHaveLength(1);
- });
-
- it('should display show more button if there are multiple pages of data', async () => {
- (searchProjects as jest.Mock).mockResolvedValueOnce({
- paging: { pageIndex: 2, pageSize: 3, total: 55 },
- results: [],
- });
-
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
- await user.click(notDefaultQualityGate);
-
- expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument();
- });
-});
-
-describe('The Permissions section', () => {
- it('should not show button to grant permission when user is not admin', async () => {
- renderQualityGateApp();
-
- // await just to make sure we've loaded the page
- expect(
- await screen.findByRole('menuitem', {
- name: `${handler.getDefaultQualityGate().name} default`,
- })
- ).toBeInTheDocument();
-
- expect(screen.queryByText('quality_gates.permissions')).not.toBeInTheDocument();
- });
- it('should show button to grant permission when user is admin', async () => {
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const grantPermissionButton = await screen.findByRole('button', {
- name: 'quality_gates.permissions.grant',
- });
- expect(screen.getByText('quality_gates.permissions')).toBeInTheDocument();
- expect(grantPermissionButton).toBeInTheDocument();
- });
-
- it('should assign permission to a user and delete it later', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
-
- // Granting permission to a user
- const grantPermissionButton = await screen.findByRole('button', {
- name: 'quality_gates.permissions.grant',
- });
- await user.click(grantPermissionButton);
- const popup = screen.getByRole('dialog');
- const searchUserInput = within(popup).getByRole('textbox');
- expect(searchUserInput).toBeInTheDocument();
- const addUserButton = screen.getByRole('button', {
- name: 'add_verb',
- });
- expect(addUserButton).toBeDisabled();
- await user.click(searchUserInput);
- expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
- await user.click(screen.getByText('userlogin'));
- expect(addUserButton).toBeEnabled();
- await user.click(addUserButton);
- expect(screen.getByText('userlogin')).toBeInTheDocument();
-
- // Cancel granting permission
- await user.click(grantPermissionButton);
- await user.click(searchUserInput);
- await user.keyboard('test{Enter}');
-
- const cancelButton = screen.getByRole('button', {
- name: 'cancel',
- });
- await user.click(cancelButton);
-
- // FP
- // eslint-disable-next-line jest-dom/prefer-in-document
- expect(screen.getAllByRole('listitem')).toHaveLength(1);
-
- // Delete the user permission
- const deleteButton = screen.getByTestId('permission-delete-button');
- await user.click(deleteButton);
- const deletePopup = screen.getByRole('dialog');
- const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
- await user.click(dialogDeleteButton);
- expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
- });
-
- it('should assign permission to a group and delete it later', async () => {
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
-
- // Granting permission to a group
- const grantPermissionButton = await screen.findByRole('button', {
- name: 'quality_gates.permissions.grant',
- });
- await user.click(grantPermissionButton);
- const popup = screen.getByRole('dialog');
- const searchUserInput = within(popup).getByRole('textbox');
- const addUserButton = screen.getByRole('button', {
- name: 'add_verb',
- });
- await user.click(searchUserInput);
- expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
- await user.click(screen.getAllByTestId('qg-add-permission-option')[1]);
- await user.click(addUserButton);
- expect(screen.getByText('Foo')).toBeInTheDocument();
-
- // Delete the group permission
- const deleteButton = screen.getByTestId('permission-delete-button');
- await user.click(deleteButton);
- const deletePopup = screen.getByRole('dialog');
- const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
- await user.click(dialogDeleteButton);
- expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
- });
-
- it('should handle searchUser service failure', async () => {
- (searchUsers as jest.Mock).mockRejectedValue('error');
-
- const user = userEvent.setup();
- handler.setIsAdmin(true);
- renderQualityGateApp();
-
- const grantPermissionButton = await screen.findByRole('button', {
- name: 'quality_gates.permissions.grant',
- });
- await user.click(grantPermissionButton);
- const popup = screen.getByRole('dialog');
- const searchUserInput = within(popup).getByRole('textbox');
- await user.click(searchUserInput);
-
- expect(screen.getByText('no_results')).toBeInTheDocument();
- });
-});
-
-function renderQualityGateApp(context?: RenderContext) {
- renderAppRoutes('quality_gates', routes, context);
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import selectEvent from 'react-select-event';
+import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
+import { searchProjects, searchUsers } from '../../../../api/quality-gates';
+import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../../types/features';
+import routes from '../../routes';
+
+jest.mock('../../../../api/quality-gates');
+
+let handler: QualityGatesServiceMock;
+
+beforeAll(() => {
+ handler = new QualityGatesServiceMock();
+});
+
+afterEach(() => handler.reset());
+
+it('should open the default quality gates', async () => {
+ renderQualityGateApp();
+
+ expect(await screen.findAllByRole('menuitem')).toHaveLength(handler.list.length);
+
+ const defaultQualityGate = handler.getDefaultQualityGate();
+ expect(await screen.findAllByText(defaultQualityGate.name)).toHaveLength(2);
+});
+
+it('should list all quality gates', async () => {
+ renderQualityGateApp();
+
+ expect(
+ await screen.findByRole('menuitem', {
+ name: `${handler.getDefaultQualityGate().name} default`,
+ })
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByRole('menuitem', {
+ name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
+ })
+ ).toBeInTheDocument();
+});
+
+it('should be able to create a quality gate then delete it', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+ let createButton = await screen.findByRole('button', { name: 'create' });
+
+ // Using keyboard
+ await user.click(createButton);
+ let nameInput = screen.getByRole('textbox', { name: /name.*/ });
+ expect(nameInput).toBeInTheDocument();
+ await user.click(nameInput);
+ await user.keyboard('testone{Enter}');
+ expect(await screen.findByRole('menuitem', { name: 'testone' })).toBeInTheDocument();
+
+ // Using modal button
+ createButton = await screen.findByRole('button', { name: 'create' });
+ await user.click(createButton);
+ nameInput = screen.getByRole('textbox', { name: /name.*/ });
+ const saveButton = screen.getByRole('button', { name: 'save' });
+
+ expect(saveButton).toBeDisabled();
+ await user.click(nameInput);
+ await user.keyboard('testtwo');
+ await user.click(saveButton);
+
+ const newQG = await screen.findByRole('menuitem', { name: 'testtwo' });
+ expect(newQG).toBeInTheDocument();
+
+ // Delete the quality gate
+ await user.click(newQG);
+ const deleteButton = await screen.findByRole('button', { name: 'delete' });
+ await user.click(deleteButton);
+ const popup = screen.getByRole('dialog');
+ const dialogDeleteButton = within(popup).getByRole('button', { name: 'delete' });
+ await user.click(dialogDeleteButton);
+
+ await waitFor(() => {
+ expect(screen.queryByRole('menuitem', { name: 'testtwo' })).not.toBeInTheDocument();
+ });
+});
+
+it('should be able to copy a quality gate', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const copyButton = await screen.findByRole('button', { name: 'copy' });
+
+ await user.click(copyButton);
+ const nameInput = screen.getByRole('textbox', { name: /name.*/ });
+ expect(nameInput).toBeInTheDocument();
+ await user.click(nameInput);
+ await user.keyboard(' bis{Enter}');
+
+ expect(await screen.findByRole('menuitem', { name: /.* bis/ })).toBeInTheDocument();
+});
+
+it('should be able to rename a quality gate', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const renameButton = await screen.findByRole('button', { name: 'rename' });
+
+ await user.click(renameButton);
+ const nameInput = screen.getByRole('textbox', { name: /name.*/ });
+ expect(nameInput).toBeInTheDocument();
+ await user.click(nameInput);
+ await user.keyboard('{Control>}a{/Control}New Name{Enter}');
+
+ expect(await screen.findByRole('menuitem', { name: /New Name.*/ })).toBeInTheDocument();
+});
+
+it('should be able to set as default a quality gate', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+ await user.click(notDefaultQualityGate);
+ const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
+ await user.click(setAsDefaultButton);
+ expect(screen.getAllByRole('menuitem')[1]).toHaveTextContent('default');
+});
+
+it('should be able to add a condition', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ // On new code
+ await user.click(await screen.findByText('quality_gates.add_condition'));
+
+ let dialog = within(screen.getByRole('dialog'));
+
+ await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' }));
+ await selectEvent.select(dialog.getByRole('textbox'), ['Issues']);
+ await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+ await user.keyboard('12{Enter}');
+
+ const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+ expect(await newConditions.findByRole('cell', { name: 'Issues' })).toBeInTheDocument();
+ expect(await newConditions.findByRole('cell', { name: '12' })).toBeInTheDocument();
+
+ // On overall code
+ await user.click(await screen.findByText('quality_gates.add_condition'));
+
+ dialog = within(screen.getByRole('dialog'));
+ await selectEvent.select(dialog.getByRole('textbox'), ['Info Issues']);
+ await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
+ await user.click(dialog.getByLabelText('quality_gates.conditions.operator'));
+
+ await user.click(dialog.getByText('quality_gates.operator.LT'));
+ await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+ await user.keyboard('42{Enter}');
+
+ const overallConditions = within(await screen.findByTestId('quality-gates__conditions-overall'));
+
+ expect(await overallConditions.findByRole('cell', { name: 'Info Issues' })).toBeInTheDocument();
+ expect(await overallConditions.findByRole('cell', { name: '42' })).toBeInTheDocument();
+
+ // Select a rating
+ await user.click(await screen.findByText('quality_gates.add_condition'));
+
+ dialog = within(screen.getByRole('dialog'));
+ await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' }));
+ await selectEvent.select(dialog.getByRole('textbox'), ['Maintainability Rating']);
+ await user.click(dialog.getByLabelText('quality_gates.conditions.value'));
+ await user.click(dialog.getByText('B'));
+ await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
+
+ expect(
+ await overallConditions.findByRole('cell', { name: 'Maintainability Rating' })
+ ).toBeInTheDocument();
+ expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument();
+});
+
+it('should be able to edit a condition', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+
+ await user.click(
+ newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code')
+ );
+ const dialog = within(screen.getByRole('dialog'));
+ await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
+ await user.keyboard('{Backspace}{Backspace}23{Enter}');
+
+ expect(await newConditions.findByText('Coverage')).toBeInTheDocument();
+ expect(await newConditions.findByText('23.0%')).toBeInTheDocument();
+});
+
+it('should be able to handle duplicate or deprecated condition', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+ await user.click(
+ await screen.findByRole('menuitem', { name: handler.getCorruptedQualityGateName() })
+ );
+
+ expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
+ expect(
+ await screen.findByRole('cell', { name: 'Complexity / Function deprecated' })
+ ).toBeInTheDocument();
+});
+
+it('should be able to handle delete condition', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+
+ await user.click(
+ newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code')
+ );
+
+ const dialog = within(screen.getByRole('dialog'));
+ await user.click(dialog.getByRole('button', { name: 'delete' }));
+
+ await waitFor(() => {
+ expect(newConditions.queryByRole('cell', { name: 'Coverage' })).not.toBeInTheDocument();
+ });
+});
+
+it('should explain condition on branch', async () => {
+ renderQualityGateApp({ featureList: [Feature.BranchSupport] });
+
+ expect(
+ await screen.findByText('quality_gates.conditions.new_code.description')
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText('quality_gates.conditions.overall_code.description')
+ ).toBeInTheDocument();
+});
+
+it('should be able to see warning when CAYC condition is not properly set and update them', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const qualityGate = await screen.findByText('SonarSource way - CFamily');
+
+ await user.click(qualityGate);
+
+ expect(
+ screen.getByText('quality_gates.cayc_condition.missing_warning.title')
+ ).toBeInTheDocument();
+ expect(screen.getByText('quality_gates.other_conditions')).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+ ).toBeInTheDocument();
+ expect(await screen.findAllByText('quality_gates.cayc_condition.missing')).toHaveLength(4);
+
+ await user.click(
+ screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+ );
+ expect(
+ screen.getByRole('dialog', {
+ name: 'quality_gates.cayc.review_update_modal.header.SonarSource way - CFamily',
+ })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('quality_gates.cayc.review_update_modal.description')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('quality_gates.cayc.review_update_modal.add_condition.header.4')
+ ).toBeInTheDocument();
+ expect(await screen.findAllByText('quality_gates.cayc_condition.ok')).toHaveLength(4);
+ expect(
+ screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' })
+ ).toBeInTheDocument();
+
+ await user.click(
+ screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' })
+ );
+
+ const newCaycConditions = within(await screen.findByTestId('quality-gates__conditions-new-cayc'));
+ expect(await newCaycConditions.findAllByText('quality_gates.cayc_condition.ok')).toHaveLength(4);
+});
+
+it('should not show any warning when CAYC condition are properly set', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const qualityGate = await screen.findByText('SonarSource way');
+
+ await user.click(qualityGate);
+
+ expect(
+ screen.queryByText('quality_gates.cayc_condition.missing_warning.title')
+ ).not.toBeInTheDocument();
+ expect(screen.getByText('quality_gates.other_conditions')).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+ ).not.toBeInTheDocument();
+
+ const newCaycConditions = within(await screen.findByTestId('quality-gates__conditions-new-cayc'));
+
+ expect(await newCaycConditions.findByText('Maintainability Rating')).toBeInTheDocument();
+ expect(await newCaycConditions.findByText('Reliability Rating')).toBeInTheDocument();
+ expect(await newCaycConditions.findByText('Security Hotspots Reviewed')).toBeInTheDocument();
+ expect(await newCaycConditions.findByText('Security Rating')).toBeInTheDocument();
+ expect(await newCaycConditions.findAllByText('quality_gates.cayc_condition.ok')).toHaveLength(4);
+
+ const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
+
+ expect(await newConditions.findByText('Coverage')).toBeInTheDocument();
+ expect(
+ newConditions.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
+ ).not.toBeInTheDocument();
+});
+
+it('should unlock editing option for CAYC conditions', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const qualityGate = await screen.findByText('SonarSource way');
+ await user.click(qualityGate);
+ expect(screen.getByText('quality_gates.cayc.unlock_edit')).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: 'quality_gates.condition.edit.Security Rating on New Code',
+ })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: 'quality_gates.condition.delete.Security Rating on New Code',
+ })
+ ).not.toBeInTheDocument();
+
+ await user.click(screen.getByText('quality_gates.cayc.unlock_edit'));
+ expect(
+ screen.getByRole('button', { name: 'quality_gates.condition.edit.Security Rating on New Code' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', {
+ name: 'quality_gates.condition.delete.Security Rating on New Code',
+ })
+ ).toBeInTheDocument();
+});
+
+describe('The Project section', () => {
+ it('should render list of projects correctly in different tabs', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+
+ await user.click(notDefaultQualityGate);
+
+ // by default it shows "selected" values
+ expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+
+ // change tabs to show deselected projects
+ await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
+ expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+
+ // change tabs to show all projects
+ await user.click(screen.getByRole('button', { name: 'quality_gates.projects.all' }));
+ expect(screen.getAllByRole('checkbox')).toHaveLength(4);
+ });
+
+ it('should handle select and deselect correctly', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+
+ await user.click(notDefaultQualityGate);
+
+ const checkedProjects = screen.getAllByRole('checkbox')[0];
+ expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+ await user.click(checkedProjects);
+ const reloadButton = screen.getByRole('button', { name: 'reload' });
+ expect(reloadButton).toBeInTheDocument();
+ await user.click(reloadButton);
+
+ // FP
+ // eslint-disable-next-line jest-dom/prefer-in-document
+ expect(screen.getAllByRole('checkbox')).toHaveLength(1);
+
+ // change tabs to show deselected projects
+ await user.click(screen.getByRole('button', { name: 'quality_gates.projects.without' }));
+
+ const uncheckedProjects = screen.getAllByRole('checkbox')[0];
+ expect(screen.getAllByRole('checkbox')).toHaveLength(3);
+ await user.click(uncheckedProjects);
+ expect(reloadButton).toBeInTheDocument();
+ await user.click(reloadButton);
+ expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+ });
+
+ it('should handle the search of projects', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+
+ await user.click(notDefaultQualityGate);
+
+ const searchInput = screen.getByRole('searchbox', { name: 'search_verb' });
+ expect(searchInput).toBeInTheDocument();
+ await user.click(searchInput);
+ await user.keyboard('test2{Enter}');
+
+ // FP
+ // eslint-disable-next-line jest-dom/prefer-in-document
+ expect(screen.getAllByRole('checkbox')).toHaveLength(1);
+ });
+
+ it('should display show more button if there are multiple pages of data', async () => {
+ (searchProjects as jest.Mock).mockResolvedValueOnce({
+ paging: { pageIndex: 2, pageSize: 3, total: 55 },
+ results: [],
+ });
+
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
+ await user.click(notDefaultQualityGate);
+
+ expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument();
+ });
+});
+
+describe('The Permissions section', () => {
+ it('should not show button to grant permission when user is not admin', async () => {
+ renderQualityGateApp();
+
+ // await just to make sure we've loaded the page
+ expect(
+ await screen.findByRole('menuitem', {
+ name: `${handler.getDefaultQualityGate().name} default`,
+ })
+ ).toBeInTheDocument();
+
+ expect(screen.queryByText('quality_gates.permissions')).not.toBeInTheDocument();
+ });
+ it('should show button to grant permission when user is admin', async () => {
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const grantPermissionButton = await screen.findByRole('button', {
+ name: 'quality_gates.permissions.grant',
+ });
+ expect(screen.getByText('quality_gates.permissions')).toBeInTheDocument();
+ expect(grantPermissionButton).toBeInTheDocument();
+ });
+
+ it('should assign permission to a user and delete it later', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
+
+ // Granting permission to a user
+ const grantPermissionButton = await screen.findByRole('button', {
+ name: 'quality_gates.permissions.grant',
+ });
+ await user.click(grantPermissionButton);
+ const popup = screen.getByRole('dialog');
+ const searchUserInput = within(popup).getByRole('textbox');
+ expect(searchUserInput).toBeInTheDocument();
+ const addUserButton = screen.getByRole('button', {
+ name: 'add_verb',
+ });
+ expect(addUserButton).toBeDisabled();
+ await user.click(searchUserInput);
+ expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
+ await user.click(screen.getByText('userlogin'));
+ expect(addUserButton).toBeEnabled();
+ await user.click(addUserButton);
+ expect(screen.getByText('userlogin')).toBeInTheDocument();
+
+ // Cancel granting permission
+ await user.click(grantPermissionButton);
+ await user.click(searchUserInput);
+ await user.keyboard('test{Enter}');
+
+ const cancelButton = screen.getByRole('button', {
+ name: 'cancel',
+ });
+ await user.click(cancelButton);
+
+ // FP
+ // eslint-disable-next-line jest-dom/prefer-in-document
+ expect(screen.getAllByRole('listitem')).toHaveLength(1);
+
+ // Delete the user permission
+ const deleteButton = screen.getByTestId('permission-delete-button');
+ await user.click(deleteButton);
+ const deletePopup = screen.getByRole('dialog');
+ const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
+ await user.click(dialogDeleteButton);
+ expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ });
+
+ it('should assign permission to a group and delete it later', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ expect(screen.queryByText('userlogin')).not.toBeInTheDocument();
+
+ // Granting permission to a group
+ const grantPermissionButton = await screen.findByRole('button', {
+ name: 'quality_gates.permissions.grant',
+ });
+ await user.click(grantPermissionButton);
+ const popup = screen.getByRole('dialog');
+ const searchUserInput = within(popup).getByRole('textbox');
+ const addUserButton = screen.getByRole('button', {
+ name: 'add_verb',
+ });
+ await user.click(searchUserInput);
+ expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
+ await user.click(screen.getAllByTestId('qg-add-permission-option')[1]);
+ await user.click(addUserButton);
+ expect(screen.getByText('Foo')).toBeInTheDocument();
+
+ // Delete the group permission
+ const deleteButton = screen.getByTestId('permission-delete-button');
+ await user.click(deleteButton);
+ const deletePopup = screen.getByRole('dialog');
+ const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
+ await user.click(dialogDeleteButton);
+ expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ });
+
+ it('should handle searchUser service failure', async () => {
+ (searchUsers as jest.Mock).mockRejectedValue('error');
+
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ const grantPermissionButton = await screen.findByRole('button', {
+ name: 'quality_gates.permissions.grant',
+ });
+ await user.click(grantPermissionButton);
+ const popup = screen.getByRole('dialog');
+ const searchUserInput = within(popup).getByRole('textbox');
+ await user.click(searchUserInput);
+
+ expect(screen.getByText('no_results')).toBeInTheDocument();
+ });
+});
+
+function renderQualityGateApp(context?: RenderContext) {
+ renderAppRoutes('quality_gates', routes, context);
+}
.quality-gate-permissions .permission-list-item:hover {
background-color: var(--rowHoverHighlight);
}
+
+.cayc-conditions-wrapper {
+ background-color: var(--barBackgroundColor);
+}
+
+.quality-gate-section tbody {
+ border: 1px solid var(--disableGrayBorder);
+}
+
+.quality-gate-section tr {
+ background-color: white !important;
+ border-bottom: 1px solid var(--disableGrayBorder);
+}
+
+.quality-gate-section tr th {
+ font-weight: 400 !important;
+ font-size: 11px;
+}
+
+.quality-gate-section thead tr {
+ background-color: transparent !important;
+ border: none;
+}
+
+.quality-gate-section thead:after {
+ display: none !important;
+}
+
+.qg-cayc-ok-badge {
+ color: var(--success500);
+ background-color: var(--badgeGreenBackground);
+}
+
+.qg-cayc-weak-badge {
+ color: var(--alertIconWarning);
+ background-color: var(--alertBackgroundWarning);
+}
+
+.qg-cayc-missing-badge {
+ color: var(--red);
+ background-color: var(--badgeRedBackground);
+}
+
+.text-through {
+ text-decoration: line-through;
+ color: var(--blacka60);
+}
+
+.bordered-bottom-cayc {
+ border-bottom: 1px solid var(--whitea18);
+}
+
+.cayc-warning-header {
+ color: var(--alertTextWarning);
+}
+
+.cayc-warning-description {
+ line-height: 18px;
+}
+
+.green-text {
+ color: var(--success500);
+}
+
+.red-text {
+ color: var(--red);
+}
import { isDiffMetric } from '../../helpers/measures';
import { Condition, Dict, Metric, QualityGate } from '../../types/types';
+const CAYC_CONDITIONS_WITH_EXPECTED_VALUE: { [key: string]: Condition } = {
+ new_reliability_rating: {
+ error: '1',
+ id: 'new_reliability_rating',
+ metric: 'new_reliability_rating',
+ op: 'GT',
+ },
+ new_security_rating: {
+ error: '1',
+ id: 'new_security_rating',
+ metric: 'new_security_rating',
+ op: 'GT',
+ },
+ new_maintainability_rating: {
+ error: '1',
+ id: 'new_maintainability_rating',
+ metric: 'new_maintainability_rating',
+ op: 'GT',
+ },
+ new_security_hotspots_reviewed: {
+ error: '100',
+ id: 'new_security_hotspots_reviewed',
+ metric: 'new_security_hotspots_reviewed',
+ op: 'LT',
+ },
+};
+
+export function getCaycConditions(conditions: Condition[]) {
+ return conditions.filter((condition) => isCaycCondition(condition));
+}
+
+export function isCaycCondition(condition: Condition) {
+ return Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).includes(condition.metric);
+}
+
+export function isCaycWeakCondition(condition: Condition) {
+ return (
+ isCaycCondition(condition) &&
+ CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric].error !== condition.error
+ );
+}
+
+export function getWeakAndMissingConditions(conditions: Condition[]) {
+ const result: {
+ weakConditions: Condition[];
+ missingConditions: Condition[];
+ } = {
+ weakConditions: [],
+ missingConditions: [],
+ };
+ Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).forEach((key) => {
+ const selectedCondition = conditions.find((condition) => condition.metric === key);
+ if (!selectedCondition) {
+ result.missingConditions.push(CAYC_CONDITIONS_WITH_EXPECTED_VALUE[key]);
+ } else if (CAYC_CONDITIONS_WITH_EXPECTED_VALUE[key].error !== selectedCondition.error) {
+ result.weakConditions.push(selectedCondition);
+ }
+ });
+ return result;
+}
+
+export function getOthersConditions(conditions: Condition[]) {
+ return conditions.filter(
+ (condition) => !Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).includes(condition.metric)
+ );
+}
+
+export function getCorrectCaycCondition(condition: Condition) {
+ return CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric];
+}
+
export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): boolean {
const finding = list.find((candidate) => candidate.id === qualityGate.id);
return (finding && finding.isDefault) || false;
renderElement: (element: string) => React.ReactNode;
selectedElements: string[];
withPaging?: boolean;
+ autoFocusSearch?: boolean;
}
export interface SelectListSearchParams {
labelSelected = translate('selected'),
labelUnselected = translate('unselected'),
labelAll = translate('all'),
+ autoFocusSearch = true,
} = this.props;
const { filter } = this.state.lastSearchParams;
/>
</span>
<SearchBox
- autoFocus={true}
+ autoFocus={autoFocusSearch}
loading={this.state.loading}
onChange={this.handleQueryChange}
placeholder={translate('search_verb')}
import withAppStateContext from '../../app/components/app-state/withAppStateContext';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../helpers/measures';
+import {
+ DIFF_METRIC_PREFIX_LENGTH,
+ getMaintainabilityGrid,
+ GRID_INDEX_OFFSET,
+ PERCENT_MULTIPLIER,
+} from '../../helpers/ratings';
import { AppState } from '../../types/appstate';
import { MetricKey } from '../../types/metrics';
import { GlobalSettingKeys } from '../../types/settings';
import { KNOWN_RATINGS } from './utils';
-const RATING_GRID_SIZE = 4;
-const DIFF_METRIC_PREFIX_LENGTH = 4;
-const PERCENT_MULTIPLIER = 100;
-const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2)
-
export interface RatingTooltipContentProps {
appState: AppState;
metricKey: MetricKey | string;
value: number | string;
}
-function getMaintainabilityGrid(ratingGridSetting: string) {
- const numbers = ratingGridSetting
- .split(',')
- .map((s) => parseFloat(s))
- .filter((n) => !isNaN(n));
-
- return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0];
-}
-
export function RatingTooltipContent(props: RatingTooltipContentProps) {
const {
appState: { settings },
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+const RATING_GRID_SIZE = 4;
+export const PERCENT_MULTIPLIER = 100;
+export const DIFF_METRIC_PREFIX_LENGTH = 4;
+export const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2)
+
function checkNumberRating(coverageRating: number): void {
if (!(typeof coverageRating === 'number' && coverageRating > 0 && coverageRating < 6)) {
throw new Error(`Unknown number rating: "${coverageRating}"`);
const mapping = [500, 5000, 50000, 250000, 750000];
return mapping[rating - 1];
}
+
+export const getMaintainabilityGrid = (ratingGridSetting: string) => {
+ const numbers = ratingGridSetting
+ .split(',')
+ .map((s) => parseFloat(s))
+ .filter((n) => !isNaN(n));
+
+ return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0];
+};
quality_gates.permissions.remove.user.confirmation=Are you sure you want to remove permission on this quality gate from user {user}?
quality_gates.permissions.remove.group=Remove permission from group
quality_gates.permissions.remove.group.confirmation=Are you sure you want to remove permission on this quality gate from group {user}?
-
+quality_gates.cayc=Clean as You Code
+quality_gates.cayc.description=The following conditions ensure that your project remains compliant with {link}.
+quality_gates.cayc.description.extended=We strongly recommend that you keep them unchanged.
+quality_gates.other_conditions=Other conditions
+quality_gates.cayc_condition.missing_warning.title=Some Clean as You Code conditions are missing or are too permissive
+quality_gates.cayc_condition.missing_warning.description=Four conditions are included by default in any new Quality Gate. They represent the minimum level of quality needed to follow Clean as You Code. We recommend that you add the missing conditions and increase the value of the conditions that are too permissive to benefit from the power of Clean as You Code.
+quality_gates.cayc_condition.review_update=Review and update
+quality_gates.cayc.review_update_modal.header=Update "{0}" to follow Cleas as You Code
+quality_gates.cayc.review_update_modal.confirm_text=Update Quality Gate
+quality_gates.cayc.review_update_modal.description=Please review the changes before you update this Quality Gate.
+quality_gates.cayc.review_update_modal.modify_condition.header={0} condition(s) will be modified
+quality_gates.cayc.review_update_modal.add_condition.header={0} condition(s) will be added
+quality_gates.cayc_condition.ok=OK
+quality_gates.cayc_condition.weak=WEAK
+quality_gates.cayc_condition.missing=MISSING
+quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0}
+quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1}
+quality_gates.cayc.new_reliability_rating.A=No bugs
+quality_gates.cayc.new_reliability_rating.B=Minor bug
+quality_gates.cayc.new_reliability_rating.C=Major bug
+quality_gates.cayc.new_reliability_rating.D=Critical bug
+quality_gates.cayc.new_reliability_rating.E=Blocker bug
+quality_gates.cayc.new_security_rating.A=No vulnerabilities
+quality_gates.cayc.new_security_rating.B=Minor vulnerability
+quality_gates.cayc.new_security_rating.C=Major vulnerability
+quality_gates.cayc.new_security_rating.D=Critical vulnerability
+quality_gates.cayc.new_security_rating.E=Blocker vulnerability
+quality_gates.cayc.unlock_edit=Unlock to edit
+quality_gates.cayc.lock_edit=Lock for editing
+quality_gates.cayc.tooltip.ok=This condition is compliant with Clean as You Code.
+quality_gates.cayc.tooltip.weak=This condition is too permissive. Increase its value to follow Clean as You Code.
+quality_gates.cayc.tooltip.missing=This condition is missing. Add it to follow Clean as You Code.
+quality_gates.cayc.badge.tooltip.learn_more=Learn more: Clean as You Code
+quality_gates.cayc_condition.delete_warning=This condition is part of Clean as You Code. If you delete it you will not benefit from the power of Clean As You Code anymore.
+quality_gates.cayc_condition.edit_warning=This condition is part of Clean as You Code. If you decrease its value you will not benefit from the power of Clean As You Code anymore.
#------------------------------------------------------------------------------
#
# RULES DOCUMENTATION PAGE