import { translate } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures';
import { useCreateConditionMutation } from '../../../queries/quality-gates';
+import { useStandardExperienceMode } from '../../../queries/settings';
import { MetricKey, MetricType } from '../../../sonar-aligned/types/metrics';
import { Feature } from '../../../types/features';
import { Condition, Metric, QualityGate } from '../../../types/types';
-import { getPossibleOperators, isNonEditableMetric } from '../utils';
+import {
+ getPossibleOperators,
+ isNonEditableMetric,
+ MQR_CONDITIONS_MAP,
+ STANDARD_CONDITIONS_MAP,
+} from '../utils';
import ConditionOperator from './ConditionOperator';
import MetricSelect from './MetricSelect';
import ThresholdInput from './ThresholdInput';
MetricKey.releasability_rating,
MetricKey.security_hotspots,
MetricKey.new_security_hotspots,
- MetricKey.software_quality_maintainability_rating,
- MetricKey.new_software_quality_maintainability_rating,
- MetricKey.software_quality_reliability_rating,
- MetricKey.new_software_quality_reliability_rating,
- MetricKey.software_quality_security_rating,
- MetricKey.new_software_quality_security_rating,
- MetricKey.effort_to_reach_software_quality_maintainability_rating_a,
- MetricKey.software_quality_maintainability_remediation_effort,
- MetricKey.new_software_quality_maintainability_remediation_effort,
- MetricKey.software_quality_security_remediation_effort,
- MetricKey.new_software_quality_security_remediation_effort,
- MetricKey.software_quality_reliability_remediation_effort,
- MetricKey.new_software_quality_reliability_remediation_effort,
- MetricKey.software_quality_maintainability_debt_ratio,
- MetricKey.new_software_quality_maintainability_debt_ratio,
];
const ADD_CONDITION_MODAL_ID = 'add-condition-modal';
export default function AddConditionModal({ qualityGate }: Readonly<Props>) {
+ const { data: isStandardMode } = useStandardExperienceMode();
const [open, setOpen] = React.useState(false);
const closeModal = React.useCallback(() => setOpen(false), []);
-
const [errorThreshold, setErrorThreshold] = React.useState('');
const [scope, setScope] = React.useState<'new' | 'overall'>('new');
const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>();
const { conditions = [] } = qualityGate;
+ const similarMetricFromAnotherMode = findSimilarConditionMetricFromAnotherMode(
+ qualityGate.conditions,
+ selectedMetric,
+ );
+
const availableMetrics = React.useMemo(() => {
return differenceWith(
map(metrics, (metric) => metric).filter(
!metric.hidden &&
!FORBIDDEN_METRIC_TYPES.includes(metric.type) &&
!FORBIDDEN_METRICS.includes(metric.key) &&
+ !(
+ isStandardMode
+ ? Object.values(STANDARD_CONDITIONS_MAP)
+ : Object.values(MQR_CONDITIONS_MAP)
+ ).includes(metric.key as MetricKey) &&
!(
metric.key === MetricKey.prioritized_rule_issues &&
!hasFeature(Feature.PrioritizedRules)
conditions,
(metric, condition) => metric.key === condition.metric,
);
- }, [conditions, hasFeature, metrics]);
+ }, [conditions, hasFeature, metrics, isStandardMode]);
const handleFormSubmit = React.useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
label={translate('quality_gates.conditions.fails_when')}
>
<MetricSelect
+ similarMetricFromAnotherMode={similarMetricFromAnotherMode}
selectedMetric={selectedMetric}
metricsArray={availableMetrics.filter((m) =>
scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key),
label={translate('quality_gates.conditions.operator')}
>
<ConditionOperator
+ isDisabled={Boolean(similarMetricFromAnotherMode)}
metric={selectedMetric}
onOperatorChange={handleOperatorChange}
op={selectedOperator}
>
<ThresholdInput
metric={selectedMetric}
- disabled={isNonEditableMetric(selectedMetric.key as MetricKey)}
+ disabled={
+ isNonEditableMetric(selectedMetric.key as MetricKey) ||
+ Boolean(similarMetricFromAnotherMode)
+ }
name="error"
onChange={handleErrorChange}
value={errorThreshold}
onOpenChange={setOpen}
primaryButton={
<Button
- isDisabled={selectedMetric === undefined}
+ isDisabled={selectedMetric === undefined || Boolean(similarMetricFromAnotherMode)}
id="add-condition-button"
form={ADD_CONDITION_MODAL_ID}
type="submit"
</Modal>
);
}
+
+function findSimilarConditionMetricFromAnotherMode(
+ conditions: Condition[] = [],
+ selectedMetric?: Metric,
+) {
+ if (!selectedMetric) {
+ return undefined;
+ }
+
+ const selectedMetricFromAnotherMode =
+ STANDARD_CONDITIONS_MAP[selectedMetric.key as MetricKey] ??
+ MQR_CONDITIONS_MAP[selectedMetric.key as MetricKey];
+
+ if (!selectedMetricFromAnotherMode) {
+ return undefined;
+ }
+
+ const qgMetrics = conditions.map((condition) => condition.metric);
+ return qgMetrics.find((metric) => metric === selectedMetricFromAnotherMode);
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
-import { InputSelect, Note } from '~design-system';
+import { InputSize, Select } from '@sonarsource/echoes-react';
+import { Note } from '~design-system';
import { getOperatorLabel } from '../../../helpers/qualityGates';
import { Metric } from '../../../types/types';
import { getPossibleOperators } from '../utils';
interface Props {
+ isDisabled?: boolean;
metric: Metric;
onOperatorChange: (op: string) => void;
op?: string;
}
-export default class ConditionOperator extends React.PureComponent<Props> {
- handleChange = ({ value }: { label: string; value: string }) => {
- this.props.onOperatorChange(value);
- };
+export default function ConditionOperator(props: Readonly<Props>) {
+ const operators = getPossibleOperators(props.metric);
- render() {
- const operators = getPossibleOperators(this.props.metric);
-
- if (Array.isArray(operators)) {
- const operatorOptions = operators.map((op) => {
- const label = getOperatorLabel(op, this.props.metric);
- return { label, value: op };
- });
-
- return (
- <InputSelect
- autoFocus
- size="small"
- isClearable={false}
- inputId="condition-operator"
- name="operator"
- onChange={this.handleChange}
- options={operatorOptions}
- isSearchable={false}
- value={operatorOptions.filter((o) => o.value === this.props.op)}
- />
- );
- }
-
- return <Note className="sw-w-abs-150">{getOperatorLabel(operators, this.props.metric)}</Note>;
+ if (!Array.isArray(operators)) {
+ return <Note className="sw-w-abs-150">{getOperatorLabel(operators, props.metric)}</Note>;
}
+ const operatorOptions = operators.map((op) => {
+ const label = getOperatorLabel(op, props.metric);
+ return { label, value: op };
+ });
+
+ return (
+ <Select
+ isDisabled={props.isDisabled}
+ size={InputSize.Small}
+ id="condition-operator"
+ isNotClearable
+ onChange={props.onOperatorChange}
+ data={operatorOptions}
+ value={operatorOptions.find((o) => o.value === props.op)?.value}
+ />
+ );
}
import { Select } from '@sonarsource/echoes-react';
import { groupBy, sortBy } from 'lodash';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
import { translate } from '../../../helpers/l10n';
import { isDefined } from '../../../helpers/types';
+import { MetricKey } from '../../../sonar-aligned/types/metrics';
import { Dict, Metric } from '../../../types/types';
-import { getLocalizedMetricNameNoDiffMetric } from '../utils';
+import { getLocalizedMetricNameNoDiffMetric, STANDARD_CONDITIONS_MAP } from '../utils';
interface Props {
metrics: Dict<Metric>;
metricsArray: Metric[];
onMetricChange: (metric: Metric) => void;
selectedMetric?: Metric;
+ similarMetricFromAnotherMode?: string;
}
export function MetricSelect({
metricsArray,
metrics,
onMetricChange,
+ similarMetricFromAnotherMode,
}: Readonly<Props>) {
+ const intl = useIntl();
+
const handleChange = (key: string | null) => {
if (isDefined(key)) {
const selectedMetric = metricsArray.find((metric) => metric.key === key);
data={options}
value={selectedMetric?.key}
onChange={handleChange}
- ariaLabel={translate('quality_gates.conditions.fails_when')}
+ ariaLabel={intl.formatMessage({ id: 'quality_gates.conditions.fails_when' })}
+ labelError={
+ Boolean(similarMetricFromAnotherMode) &&
+ intl.formatMessage(
+ { id: 'quality_gates.add_condition.metric_from_other_mode' },
+ {
+ isStandardMode: Boolean(
+ STANDARD_CONDITIONS_MAP[similarMetricFromAnotherMode as MetricKey],
+ ),
+ metric: intl.formatMessage({ id: `metric.${similarMetricFromAnotherMode}.name` }),
+ },
+ )
+ }
isSearchable
isNotClearable
/>
};
renderRatingInput() {
- const { name, value } = this.props;
+ const { name, value, disabled } = this.props;
const options = [
{ label: 'A', value: '1' },
return (
<InputSelect
+ isDisabled={disabled}
className="sw-w-abs-150"
inputId="condition-threshold"
name={name}
qualityGateListItem: (qualityGateName: string) => byRole('link', { name: qualityGateName }),
newConditionRow: byTestId('quality-gates__conditions-new').byRole('row'),
overallConditionRow: byTestId('quality-gates__conditions-overall').byRole('row'),
+ addConditionButton: byRole('button', { name: 'quality_gates.add_condition' }),
batchDialog: byRole('dialog', { name: /quality_gates.update_conditions.header/ }),
singleDialog: byRole('dialog', { name: /quality_gates.update_conditions.header.single_metric/ }),
updateMetricsBtn: byRole('button', { name: 'quality_gates.update_conditions.update_metrics' }),
expect(ui.singleUpdate.query()).not.toBeInTheDocument();
expect(ui.standardBadge.query()).not.toBeInTheDocument();
});
+
+ it('should not let adding condition if a similar one from another mode already added', async () => {
+ const user = userEvent.setup();
+ qualityGateHandler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ expect(await ui.listItem.findAll()).toHaveLength(9);
+ await user.click(ui.qualityGateListItem('QG without new code conditions').get());
+ await user.click(await ui.addConditionButton.find());
+
+ const dialog = byRole('dialog');
+
+ await user.click(
+ dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get(),
+ );
+
+ // try adding Security Rating from MQR Mode
+ await user.click(
+ dialog.byRole('combobox', { name: 'quality_gates.conditions.fails_when' }).get(),
+ );
+ await user.click(dialog.byRole('option', { name: 'Security Rating' }).get());
+ expect(
+ byText(
+ 'quality_gates.add_condition.metric_from_other_mode.true.metric.security_rating.name',
+ ).get(),
+ ).toBeInTheDocument();
+ expect(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get()).toBeDisabled();
+ });
});
describe('Standard mode', () => {
expect(ui.singleUpdate.query()).not.toBeInTheDocument();
expect(ui.mqrBadge.query()).not.toBeInTheDocument();
});
+
+ it('should not let adding condition if a similar one from another mode already added', async () => {
+ const user = userEvent.setup();
+ qualityGateHandler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ expect(await ui.listItem.findAll()).toHaveLength(9);
+ await user.click(ui.qualityGateListItem('QG with MQR conditions').get());
+ await user.click(await ui.addConditionButton.find());
+
+ const dialog = byRole('dialog');
+
+ // try adding blocker issues metric
+ await user.click(
+ dialog.byRole('combobox', { name: 'quality_gates.conditions.fails_when' }).get(),
+ );
+ await user.click(dialog.byRole('option', { name: 'Blocker Issues' }).get());
+ expect(
+ byText(
+ 'quality_gates.add_condition.metric_from_other_mode.false.metric.new_software_quality_blocker_issues.name',
+ ).get(),
+ ).toBeInTheDocument();
+ expect(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get()).toBeDisabled();
+ });
});
});
quality_gates.projects=Projects
quality_gates.projects.help=The Default gate is applied to all projects not explicitly assigned to a gate. Quality Gate administrators can assign projects to a non-default gate, or always make it follow the system default. Project administrators may choose any gate.
quality_gates.add_condition=Add Condition
+quality_gates.add_condition.metric_from_other_mode=This quality gate already has an equivalent condition based on the same concept ("{metric}") that persists from the {isStandardMode, select, true {Standard Experience} other {Multi-Quality Rule Mode}}. Update the metric and you will be able to edit the condition.
quality_gates.condition.edit=Edit condition on {0}
quality_gates.condition.delete=Delete condition on {0}
quality_gates.condition_added=Successfully added condition.