From b8b0efb9cf1408d9d9fcfffedfc39cc6d0604387 Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Mon, 2 Jan 2023 10:02:39 +0100 Subject: [PATCH] SONAR-17815 Adding CAYC section QG conditions --- server/sonar-web/src/main/js/app/theme.js | 4 + .../components/CaycBadgeTooltip.tsx | 39 +++++ .../components/CaycConditions.tsx | 132 +++++++++++++++++ .../components/CaycStatusBadge.tsx | 71 +++++++++ .../quality-gates/components/Condition.tsx | 128 +++++++++++----- .../components/ConditionModal.tsx | 12 +- .../ConditionReviewAndUpdateModal.tsx | 138 ++++++++++++++++++ .../components/ConditionValue.tsx | 73 +++++++++ .../components/ConditionValueDescription.tsx | 88 +++++++++++ .../quality-gates/components/Conditions.tsx | 127 +++++++++------- .../components/ConditionsTable.tsx | 105 +++++++++++++ .../js/apps/quality-gates/components/List.tsx | 4 +- .../quality-gates/components/Projects.tsx | 1 + .../{App-it.tsx => QualityGate-it.tsx} | 132 ++++++++++++++--- .../src/main/js/apps/quality-gates/styles.css | 67 +++++++++ .../src/main/js/apps/quality-gates/utils.ts | 71 +++++++++ .../js/components/controls/SelectList.tsx | 4 +- .../measure/RatingTooltipContent.tsx | 20 +-- .../sonar-web/src/main/js/helpers/ratings.ts | 15 ++ .../resources/org/sonar/l10n/core.properties | 36 ++++- 20 files changed, 1138 insertions(+), 129 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx rename server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/{App-it.tsx => QualityGate-it.tsx} (79%) diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 09a01a3906c..5167a9c05d7 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -139,6 +139,10 @@ module.exports = { darkBackgroundSeparator: '#413b3b', darkBackgroundFontColor: '#f6f8fa', + //quality gate badges + badgeGreenBackground: '#f0faec', + badgeRedBackground: '#ffeaea', + // new color palette // Some of these colors duplicate what we have above, but this will make it // easier to align with the UX designers on what colors to use where. diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx new file mode 100644 index 00000000000..7c983379e3c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycBadgeTooltip.tsx @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DocLink from '../../../components/common/DocLink'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + badgeType: 'missing' | 'weak' | 'ok'; +} + +export default function CaycBadgeTooltip({ badgeType }: Props) { + return ( +
+

+ {translate('quality_gates.cayc.tooltip', badgeType)} +

+ + {translate('quality_gates.cayc.badge.tooltip.learn_more')} + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx new file mode 100644 index 00000000000..546c79d27fa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditions.tsx @@ -0,0 +1,132 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../components/common/DocLink'; +import { Button, ButtonLink } from '../../../components/controls/buttons'; +import ModalButton, { ModalProps } from '../../../components/controls/ModalButton'; +import { Alert } from '../../../components/ui/Alert'; +import { translate } from '../../../helpers/l10n'; +import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; +import { getWeakAndMissingConditions } from '../utils'; +import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; +import ConditionsTable from './ConditionsTable'; + +interface Props { + canEdit: boolean; + metrics: Dict; + 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 { + 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) => ( + + ); + + 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 ( +
+

{translate('quality_gates.cayc')}

+
+ + {translate('quality_gates.cayc')} + + ), + }} + /> +
+ {(weakConditions.length > 0 || missingConditions.length > 0) && ( + +

+ {translate('quality_gates.cayc_condition.missing_warning.title')} +

+

+ {translate('quality_gates.cayc_condition.missing_warning.description')} +

+ {canEdit && ( + + {({ onClick }) => ( + + )} + + )} +
+ )} + {canEdit && ( + + {showEdit + ? translate('quality_gates.cayc.lock_edit') + : translate('quality_gates.cayc.unlock_edit')} + + )} + +
+ +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx new file mode 100644 index 00000000000..31931453b38 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycStatusBadge.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import classNames from 'classnames'; +import * as React from 'react'; +import Tooltip from '../../../components/controls/Tooltip'; +import AlertErrorIcon from '../../../components/icons/AlertErrorIcon'; +import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; +import CheckIcon from '../../../components/icons/CheckIcon'; +import { translate } from '../../../helpers/l10n'; +import { Condition } from '../../../types/types'; +import { isCaycWeakCondition } from '../utils'; +import CaycBadgeTooltip from './CaycBadgeTooltip'; + +interface Props { + className?: string; + isMissingCondition?: boolean; + condition: Condition; + isCaycModal?: boolean; +} + +export default function CaycStatusBadge({ + className, + isMissingCondition, + condition, + isCaycModal, +}: Props) { + if (isMissingCondition && !isCaycModal) { + return ( + }> +
+ + {translate('quality_gates.cayc_condition.missing')} +
+
+ ); + } else if (isCaycWeakCondition(condition) && !isCaycModal) { + return ( + }> +
+ + {translate('quality_gates.cayc_condition.weak')} +
+
+ ); + } + return ( + }> +
+ + {translate('quality_gates.cayc_condition.ok')} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx index d9c328072d8..3458febd88c 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx @@ -23,11 +23,13 @@ import { deleteCondition } from '../../../api/quality-gates'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; import { DeleteButton, EditButton } from '../../../components/controls/buttons'; import ConfirmModal from '../../../components/controls/ConfirmModal'; +import { Alert } from '../../../components/ui/Alert'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; -import { formatMeasure } from '../../../helpers/measures'; import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; -import { getLocalizedMetricNameNoDiffMetric } from '../utils'; +import { getLocalizedMetricNameNoDiffMetric, isCaycCondition } from '../utils'; +import CaycStatusBadge from './CaycStatusBadge'; import ConditionModal from './ConditionModal'; +import ConditionValue from './ConditionValue'; interface Props { condition: ConditionType; @@ -38,6 +40,9 @@ interface Props { qualityGate: QualityGate; updated?: boolean; metrics: Dict; + showEdit?: boolean; + isMissingCondition?: boolean; + isCaycModal?: boolean; } interface State { @@ -90,63 +95,106 @@ export class ConditionComponent extends React.PureComponent { } 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 ( - + {getLocalizedMetricNameNoDiffMetric(metric, metrics)} {metric.hidden && ( {translate('deprecated')} )} - {this.renderOperator()} - - {formatMeasure(condition.error, metric.type)} + + {this.renderOperator()} + - {canEdit && ( - <> - + + + + + {isCaycCondition(condition) && ( + + )} + {canEdit && showEdit && !isMissingCondition && ( + <> - - - - {this.state.modal && ( - - )} - {this.state.deleteFormOpen && ( - - {translateWithParameters( - 'quality_gates.delete_condition.confirm.message', - getLocalizedMetricName(this.props.metric) - )} - - )} - - )} + {this.state.modal && ( + + )} + {this.state.deleteFormOpen && ( + + {translateWithParameters( + 'quality_gates.delete_condition.confirm.message', + getLocalizedMetricName(this.props.metric) + )} + {isCaycCondition(condition) && ( + +

+ {translate('quality_gates.cayc_condition.delete_warning')} +

+
+ )} +
+ )} + + )} + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx index b542d00ea24..5a5392db369 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx @@ -25,7 +25,7 @@ import { Alert } from '../../../components/ui/Alert'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; import { Condition, Metric, QualityGate } from '../../../types/types'; -import { getPossibleOperators } from '../utils'; +import { getPossibleOperators, isCaycCondition, isCaycWeakCondition } from '../utils'; import ConditionOperator from './ConditionOperator'; import MetricSelect from './MetricSelect'; import ThresholdInput from './ThresholdInput'; @@ -105,7 +105,7 @@ export default class ConditionModal extends React.PureComponent { }; render() { - const { header, metrics, onClose } = this.props; + const { header, metrics, onClose, condition } = this.props; const { op, error, scope, metric } = this.state; return ( { )} + {condition && isCaycCondition(condition) && !isCaycWeakCondition(condition) && ( + +

+ {translate('quality_gates.cayc_condition.edit_warning')} +

+
+ )} +
)} -
-

{translate('quality_gates.conditions')}

+
+

{translate('quality_gates.conditions')}

{ {sortedConditionsOnNewMetrics.length > 0 && (
- {this.renderConditionsTable(sortedConditionsOnNewMetrics, 'new')} +

+ {translate('quality_gates.conditions.new_code', 'long')} +

+ {this.props.hasFeature(Feature.BranchSupport) && ( +

+ {translate('quality_gates.conditions.new_code', 'description')} +

+ )} + + + +

{translate('quality_gates.other_conditions')}

+
)} {sortedConditionsOnOverallMetrics.length > 0 && (
- {this.renderConditionsTable(sortedConditionsOnOverallMetrics, 'overall')} +

+ {translate('quality_gates.conditions.overall_code', 'long')} +

+ + {this.props.hasFeature(Feature.BranchSupport) && ( +

+ {translate('quality_gates.conditions.overall_code', 'description')} +

+ )} + +
)} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx new file mode 100644 index 00000000000..bdacfd41ffe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; +import Condition from './Condition'; + +interface Props { + canEdit: boolean; + metrics: Dict; + 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 { + render() { + const { + qualityGate, + metrics, + canEdit, + onRemoveCondition, + onSaveCondition, + updatedConditionId, + scope, + conditions, + missingConditions, + showEdit, + isCaycModal, + } = this.props; + + return ( + + + + + + + + + + + {conditions.map((condition) => ( + + ))} + + {missingConditions && + missingConditions.map((condition) => ( + + ))} + +
{translate('quality_gates.conditions.metric')}{translate('quality_gates.conditions.operator')}{translate('quality_gates.conditions.value')} 
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx index 38168366339..82a8b06001b 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx @@ -39,7 +39,9 @@ export default function List({ qualityGates }: Props) { key={qualityGate.id} to={getQualityGateUrl(String(qualityGate.id))} > - {qualityGate.name} + + {qualityGate.name} + {qualityGate.isDefault && ( {translate('default')} )} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx index 1675f373d5e..8a5ff197b01 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx @@ -174,6 +174,7 @@ export default class Projects extends React.PureComponent { renderElement={this.renderElement} selectedElements={this.state.selectedProjects} withPaging={true} + autoFocusSearch={false} /> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx similarity index 79% rename from server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx rename to server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx index da9646730d0..786f53c5f9d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx @@ -160,9 +160,7 @@ it('should be able to add a condition', async () => { 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' }) - ); + 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(); @@ -178,9 +176,7 @@ it('should be able to add a condition', async () => { 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' }) - ); + 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(); @@ -195,10 +191,6 @@ it('should be able to add a condition', async () => { 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(); @@ -210,11 +202,7 @@ it('should be able to edit a condition', async () => { handler.setIsAdmin(true); renderQualityGateApp(); - const newConditions = within( - await screen.findByRole('table', { - name: 'quality_gates.conditions.new_code.long', - }) - ); + const newConditions = within(await screen.findByTestId('quality-gates__conditions-new')); await user.click( newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code') @@ -246,11 +234,7 @@ it('should be able to handle delete condition', async () => { handler.setIsAdmin(true); renderQualityGateApp(); - const newConditions = within( - await screen.findByRole('table', { - name: 'quality_gates.conditions.new_code.long', - }) - ); + const newConditions = within(await screen.findByTestId('quality-gates__conditions-new')); await user.click( newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code') @@ -275,6 +259,114 @@ it('should explain condition on branch', async () => { ).toBeInTheDocument(); }); +it('should be able to see warning when CAYC condition is not properly set and update them', async () => { + 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(); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/styles.css b/server/sonar-web/src/main/js/apps/quality-gates/styles.css index d1b7a539417..6591d52354f 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/styles.css +++ b/server/sonar-web/src/main/js/apps/quality-gates/styles.css @@ -29,3 +29,70 @@ .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); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts index 79b894c3609..1548766e61e 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -21,6 +21,77 @@ import { getLocalizedMetricName } from '../../helpers/l10n'; import { isDiffMetric } from '../../helpers/measures'; import { Condition, Dict, Metric, QualityGate } from '../../types/types'; +const CAYC_CONDITIONS_WITH_EXPECTED_VALUE: { [key: string]: Condition } = { + new_reliability_rating: { + error: '1', + id: 'new_reliability_rating', + metric: 'new_reliability_rating', + op: 'GT', + }, + new_security_rating: { + error: '1', + id: 'new_security_rating', + metric: 'new_security_rating', + op: 'GT', + }, + new_maintainability_rating: { + error: '1', + id: 'new_maintainability_rating', + metric: 'new_maintainability_rating', + op: 'GT', + }, + new_security_hotspots_reviewed: { + error: '100', + id: 'new_security_hotspots_reviewed', + metric: 'new_security_hotspots_reviewed', + op: 'LT', + }, +}; + +export function getCaycConditions(conditions: Condition[]) { + return conditions.filter((condition) => isCaycCondition(condition)); +} + +export function isCaycCondition(condition: Condition) { + return Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).includes(condition.metric); +} + +export function isCaycWeakCondition(condition: Condition) { + return ( + isCaycCondition(condition) && + CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric].error !== condition.error + ); +} + +export function getWeakAndMissingConditions(conditions: Condition[]) { + const result: { + weakConditions: Condition[]; + missingConditions: Condition[]; + } = { + weakConditions: [], + missingConditions: [], + }; + Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).forEach((key) => { + const selectedCondition = conditions.find((condition) => condition.metric === key); + if (!selectedCondition) { + result.missingConditions.push(CAYC_CONDITIONS_WITH_EXPECTED_VALUE[key]); + } else if (CAYC_CONDITIONS_WITH_EXPECTED_VALUE[key].error !== selectedCondition.error) { + result.weakConditions.push(selectedCondition); + } + }); + return result; +} + +export function getOthersConditions(conditions: Condition[]) { + return conditions.filter( + (condition) => !Object.keys(CAYC_CONDITIONS_WITH_EXPECTED_VALUE).includes(condition.metric) + ); +} + +export function getCorrectCaycCondition(condition: Condition) { + return CAYC_CONDITIONS_WITH_EXPECTED_VALUE[condition.metric]; +} + export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): boolean { const finding = list.find((candidate) => candidate.id === qualityGate.id); return (finding && finding.isDefault) || false; diff --git a/server/sonar-web/src/main/js/components/controls/SelectList.tsx b/server/sonar-web/src/main/js/components/controls/SelectList.tsx index 083a3716be7..549a3a04e6e 100644 --- a/server/sonar-web/src/main/js/components/controls/SelectList.tsx +++ b/server/sonar-web/src/main/js/components/controls/SelectList.tsx @@ -48,6 +48,7 @@ interface Props { renderElement: (element: string) => React.ReactNode; selectedElements: string[]; withPaging?: boolean; + autoFocusSearch?: boolean; } export interface SelectListSearchParams { @@ -136,6 +137,7 @@ export default class SelectList extends React.PureComponent { labelSelected = translate('selected'), labelUnselected = translate('unselected'), labelAll = translate('all'), + autoFocusSearch = true, } = this.props; const { filter } = this.state.lastSearchParams; @@ -157,7 +159,7 @@ export default class SelectList extends React.PureComponent { /> 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 }, diff --git a/server/sonar-web/src/main/js/helpers/ratings.ts b/server/sonar-web/src/main/js/helpers/ratings.ts index 60fa6b5b70c..6b85b84c162 100644 --- a/server/sonar-web/src/main/js/helpers/ratings.ts +++ b/server/sonar-web/src/main/js/helpers/ratings.ts @@ -17,6 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +const RATING_GRID_SIZE = 4; +export const PERCENT_MULTIPLIER = 100; +export const DIFF_METRIC_PREFIX_LENGTH = 4; +export const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2) + function checkNumberRating(coverageRating: number): void { if (!(typeof coverageRating === 'number' && coverageRating > 0 && coverageRating < 6)) { throw new Error(`Unknown number rating: "${coverageRating}"`); @@ -58,3 +64,12 @@ export function getSizeRatingAverageValue(rating: number): number { const mapping = [500, 5000, 50000, 250000, 750000]; return mapping[rating - 1]; } + +export const getMaintainabilityGrid = (ratingGridSetting: string) => { + const numbers = ratingGridSetting + .split(',') + .map((s) => parseFloat(s)) + .filter((n) => !isNaN(n)); + + return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0]; +}; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 95ff97d115e..54cb7e6714e 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1837,7 +1837,41 @@ quality_gates.permissions.remove.user=Remove permission from user quality_gates.permissions.remove.user.confirmation=Are you sure you want to remove permission on this quality gate from user {user}? quality_gates.permissions.remove.group=Remove permission from group quality_gates.permissions.remove.group.confirmation=Are you sure you want to remove permission on this quality gate from group {user}? - +quality_gates.cayc=Clean as You Code +quality_gates.cayc.description=The following conditions ensure that your project remains compliant with {link}. +quality_gates.cayc.description.extended=We strongly recommend that you keep them unchanged. +quality_gates.other_conditions=Other conditions +quality_gates.cayc_condition.missing_warning.title=Some Clean as You Code conditions are missing or are too permissive +quality_gates.cayc_condition.missing_warning.description=Four conditions are included by default in any new Quality Gate. They represent the minimum level of quality needed to follow Clean as You Code. We recommend that you add the missing conditions and increase the value of the conditions that are too permissive to benefit from the power of Clean as You Code. +quality_gates.cayc_condition.review_update=Review and update +quality_gates.cayc.review_update_modal.header=Update "{0}" to follow Cleas as You Code +quality_gates.cayc.review_update_modal.confirm_text=Update Quality Gate +quality_gates.cayc.review_update_modal.description=Please review the changes before you update this Quality Gate. +quality_gates.cayc.review_update_modal.modify_condition.header={0} condition(s) will be modified +quality_gates.cayc.review_update_modal.add_condition.header={0} condition(s) will be added +quality_gates.cayc_condition.ok=OK +quality_gates.cayc_condition.weak=WEAK +quality_gates.cayc_condition.missing=MISSING +quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0} +quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1} +quality_gates.cayc.new_reliability_rating.A=No bugs +quality_gates.cayc.new_reliability_rating.B=Minor bug +quality_gates.cayc.new_reliability_rating.C=Major bug +quality_gates.cayc.new_reliability_rating.D=Critical bug +quality_gates.cayc.new_reliability_rating.E=Blocker bug +quality_gates.cayc.new_security_rating.A=No vulnerabilities +quality_gates.cayc.new_security_rating.B=Minor vulnerability +quality_gates.cayc.new_security_rating.C=Major vulnerability +quality_gates.cayc.new_security_rating.D=Critical vulnerability +quality_gates.cayc.new_security_rating.E=Blocker vulnerability +quality_gates.cayc.unlock_edit=Unlock to edit +quality_gates.cayc.lock_edit=Lock for editing +quality_gates.cayc.tooltip.ok=This condition is compliant with Clean as You Code. +quality_gates.cayc.tooltip.weak=This condition is too permissive. Increase its value to follow Clean as You Code. +quality_gates.cayc.tooltip.missing=This condition is missing. Add it to follow Clean as You Code. +quality_gates.cayc.badge.tooltip.learn_more=Learn more: Clean as You Code +quality_gates.cayc_condition.delete_warning=This condition is part of Clean as You Code. If you delete it you will not benefit from the power of Clean As You Code anymore. +quality_gates.cayc_condition.edit_warning=This condition is part of Clean as You Code. If you decrease its value you will not benefit from the power of Clean As You Code anymore. #------------------------------------------------------------------------------ # # RULES DOCUMENTATION PAGE -- 2.39.5