From: Viktor Vorona Date: Fri, 25 Oct 2024 14:41:35 +0000 (+0200) Subject: SONAR-23299 Update conditions dialog X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ab877d5b4bf3b8f1cdff07278d6b52af2f1c357b;p=sonarqube.git SONAR-23299 Update conditions dialog --- diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index c815e3a8daa..be3d0fb8f82 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Button, Spinner } from '@sonarsource/echoes-react'; import { uniqBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -31,7 +32,6 @@ import { LightPrimary, Link, Note, - Spinner, SubHeading, } from '~design-system'; import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip'; @@ -42,9 +42,16 @@ import { ModalProps } from '../../../components/controls/ModalButton'; import { DocLink } from '../../../helpers/doc-links'; import { useDocUrl } from '../../../helpers/docs'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; +import { useStandardExperienceMode } from '../../../queries/settings'; +import { MetricKey } from '../../../sonar-aligned/types/metrics'; import { Feature } from '../../../types/features'; import { CaycStatus, Condition as ConditionType, QualityGate } from '../../../types/types'; -import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils'; +import { + groupAndSortByPriorityConditions, + isQualityGateOptimized, + MQR_CONDITIONS_MAP, + STANDARD_CONDITIONS_MAP, +} from '../utils'; import AddConditionModal from './AddConditionModal'; import AIGeneratedIcon from './AIGeneratedIcon'; import CaycCompliantBanner from './CaycCompliantBanner'; @@ -54,6 +61,7 @@ import CaycFixOptimizeBanner from './CaycFixOptimizeBanner'; import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; import ConditionsTable from './ConditionsTable'; import QGRecommendedIcon from './QGRecommendedIcon'; +import UpdateConditionsFromOtherModeModal from './UpdateConditionsFromOtherModeModal'; interface Props { isFetching?: boolean; @@ -66,6 +74,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly) const [editing, setEditing] = React.useState(caycStatus === CaycStatus.NonCompliant); const metrics = useMetrics(); const { hasFeature } = useAvailableFeatures(); + const { data: isStandardMode, isLoading } = useStandardExperienceMode(); const canEdit = Boolean(actions?.manageConditions); const existingConditions = conditions.filter((condition) => metrics[condition.metric]); @@ -116,10 +125,11 @@ export default function Conditions({ qualityGate, isFetching }: Readonly) [qualityGate, conditions, metrics, isOptimizing, canEdit], ); + const conditionsToOtherModeMap = isStandardMode ? MQR_CONDITIONS_MAP : STANDARD_CONDITIONS_MAP; + return ( -
+ - {isBuiltIn && (
@@ -138,7 +148,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly)
)} - {isAICodeAssuranceQualityGate && (
@@ -157,7 +166,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly)
)} - {isCompliantCustomQualityGate && !isOptimizing && } {isCompliantCustomQualityGate && isOptimizing && canEdit && ( @@ -165,7 +173,26 @@ export default function Conditions({ qualityGate, isFetching }: Readonly) {caycStatus === CaycStatus.NonCompliant && canEdit && ( )} - + conditionsToOtherModeMap[c.metric as MetricKey] !== undefined, + )} + overallCodeConditions={overallCodeConditions.filter( + (c) => conditionsToOtherModeMap[c.metric as MetricKey] !== undefined, + )} + > + {/* TODO test example */} + + + c.metric.includes('rating'))!} + > + {/* TODO test example */} + +
@@ -185,7 +212,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly) )} - +
{(caycStatus === CaycStatus.NonCompliant || editing) && canEdit && ( @@ -193,7 +220,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly) )}
- {uniqDuplicates.length > 0 && (
@@ -206,7 +232,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly)
)} -
{caycConditions.length > 0 && (
@@ -288,7 +313,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly)
)}
- {caycStatus !== CaycStatus.NonCompliant && !editing && canEdit && (
@@ -305,12 +329,11 @@ export default function Conditions({ qualityGate, isFetching }: Readonly)
)} - {existingConditions.length === 0 && (
{translate('quality_gates.no_conditions')}
)} -
+ ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/UpdateConditionsFromOtherModeModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/UpdateConditionsFromOtherModeModal.tsx new file mode 100644 index 00000000000..50775c76dda --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/UpdateConditionsFromOtherModeModal.tsx @@ -0,0 +1,321 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { + Button, + ButtonVariety, + Heading, + IconArrowRight, + Modal, + ModalSize, + Text, + TextSize, +} from '@sonarsource/echoes-react'; +import * as React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; +import DocumentationLink from '../../../components/common/DocumentationLink'; +import { ContentCell, FlagMessageV2, Table, TableRow } from '../../../design-system'; +import { DocLink } from '../../../helpers/doc-links'; +import { translate } from '../../../helpers/l10n'; +import { getOperatorLabel } from '../../../helpers/qualityGates'; +import { useUpdateOrDeleteConditionsMutation } from '../../../queries/quality-gates'; +import { useStandardExperienceMode } from '../../../queries/settings'; +import { MetricKey } from '../../../sonar-aligned/types/metrics'; +import { Condition } from '../../../types/types'; +import { + getLocalizedMetricNameNoDiffMetric, + MQR_CONDITIONS_MAP, + STANDARD_CONDITIONS_MAP, +} from '../utils'; +import ConditionValue from './ConditionValue'; + +type Props = React.PropsWithChildren & { qualityGateName: string } & ( + | { + condition?: never; + isSingleMetric?: false; + newCodeConditions: Condition[]; + overallCodeConditions: Condition[]; + } + | { + condition: Condition; + isSingleMetric: true; + newCodeConditions?: never; + overallCodeConditions?: never; + } + ); + +export default function UpdateConditionsFromOtherModeModal({ + newCodeConditions, + overallCodeConditions, + qualityGateName, + isSingleMetric, + condition, + children, +}: Readonly) { + const { data: isStandard } = useStandardExperienceMode(); + const [isOpen, setOpen] = React.useState(false); + const [error, setError] = React.useState(false); + const intl = useIntl(); + const mapper = isStandard ? MQR_CONDITIONS_MAP : STANDARD_CONDITIONS_MAP; + const { mutate: updateConditions, isPending } = useUpdateOrDeleteConditionsMutation( + qualityGateName, + isSingleMetric, + ); + + const onSubmit = () => { + const conditions = isSingleMetric + ? [condition] + : [...newCodeConditions, ...overallCodeConditions]; + + updateConditions( + conditions.map((c) => ({ ...c, metric: mapper[c.metric as MetricKey] })), + { + onSuccess: () => setOpen(false), + onError: () => setError(true), + }, + ); + }; + + return ( + + {intl.formatMessage({ + id: isSingleMetric ? 'update_verb' : 'quality_gates.update_conditions.update_metrics', + })} + + } + secondaryButton={} + content={ + <> + {error && ( +
+ + {intl.formatMessage({ id: 'quality_gates.update_conditions.error' })} + +
+ )} + + {chunks}, + mode: intl.formatMessage({ + id: `settings.mode.${isStandard ? 'standard' : 'mqr'}.name`, + }), + }} + /> + + + {isSingleMetric && } + + {chunks}, + }} + /> +
+
+ + {intl.formatMessage({ + id: 'quality_gates.update_conditions.description.link', + })} + + ), + }} + /> +
+ {!isSingleMetric && ( + <> + {newCodeConditions.length > 0 && ( + <> + + {intl.formatMessage({ id: 'overview.new_code' })} + + } + > + {newCodeConditions.map((condition) => ( + + ))} +
+ + )} + {overallCodeConditions.length > 0 && ( + <> + + {intl.formatMessage({ id: 'overview.overall_code' })} + + } + > + {overallCodeConditions.map((condition) => ( + + ))} +
+ + )} + + )} + + } + > + {React.cloneElement(children as React.ReactElement, { onClick: () => setOpen(true) })} +
+ ); +} + +function SingleMetric({ condition }: Readonly<{ condition: Condition }>) { + const { data: isStandard } = useStandardExperienceMode(); + const intl = useIntl(); + const metrics = useMetrics(); + const metric = metrics[condition.metric]; + const mapper = isStandard ? MQR_CONDITIONS_MAP : STANDARD_CONDITIONS_MAP; + const metricFromOtherMode = mapper[metric.key as MetricKey]; + + return ( +
+
+ + {intl.formatMessage({ + id: `quality_gates.update_conditions.${isStandard ? 'mqr' : 'standard'}_mode_header`, + })} + + + {getLocalizedMetricNameNoDiffMetric(metrics[condition.metric], metrics)} + +
+ +
+ {metricFromOtherMode ? ( + <> + + {intl.formatMessage({ + id: `quality_gates.update_conditions.${isStandard ? 'standard' : 'mqr'}_mode_header`, + })} + + + {getLocalizedMetricNameNoDiffMetric(metrics[metricFromOtherMode], metrics)} + + + ) : ( + + {intl.formatMessage({ id: 'quality_gates.update_conditions.removed' })} + + )} +
+
+ ); +} + +function Header() { + const intl = useIntl(); + const { data: isStandard } = useStandardExperienceMode(); + + return ( + + + + {intl.formatMessage({ + id: `quality_gates.update_conditions.${isStandard ? 'mqr' : 'standard'}_mode_header`, + })} + + + + + + {intl.formatMessage({ + id: `quality_gates.update_conditions.${isStandard ? 'standard' : 'mqr'}_mode_header`, + })} + + + + + {intl.formatMessage({ id: 'quality_gates.update_conditions.operator_and_value_header' })} + + + + + ); +} + +function ConditionRow({ condition }: Readonly<{ condition: Condition }>) { + const { data: isStandard } = useStandardExperienceMode(); + const intl = useIntl(); + const metrics = useMetrics(); + const { op = 'GT' } = condition; + const metric = metrics[condition.metric]; + const mapper = isStandard ? MQR_CONDITIONS_MAP : STANDARD_CONDITIONS_MAP; + const metricFromOtherMode = mapper[metric?.key as MetricKey]; + + return ( + + {getLocalizedMetricNameNoDiffMetric(metric, metrics)} + + {metricFromOtherMode ? ( + getLocalizedMetricNameNoDiffMetric(metrics[metricFromOtherMode], metrics) + ) : ( + + {intl.formatMessage({ id: 'quality_gates.update_conditions.removed' })} + + )} + + + + {metricFromOtherMode && ( + + {getOperatorLabel(op, metric)}  + + + )} + + + ); +} 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 8cfc4f50e87..99e3aad55b3 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 @@ -20,6 +20,7 @@ import { sortBy } from 'lodash'; import { MetricKey } from '~sonar-aligned/types/metrics'; +import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from '../../helpers/constants'; import { getLocalizedMetricName } from '../../helpers/l10n'; import { isDiffMetric } from '../../helpers/measures'; import { CaycStatus, Condition, Dict, Group, Metric, QualityGate } from '../../types/types'; @@ -148,6 +149,33 @@ const CAYC_CONDITIONS_WITH_FIXED_VALUE: AllCaycMetricKeys[] = [ ]; const NON_EDITABLE_CONDITIONS: MetricKey[] = [MetricKey.prioritized_rule_issues]; +export const STANDARD_CONDITIONS_MAP: Partial> = { + [MetricKey.new_blocker_violations]: MetricKey.new_software_quality_blocker_issues, + [MetricKey.new_critical_violations]: MetricKey.new_software_quality_high_issues, + [MetricKey.new_major_violations]: MetricKey.new_software_quality_medium_issues, + [MetricKey.new_minor_violations]: MetricKey.new_software_quality_low_issues, + [MetricKey.new_info_violations]: MetricKey.new_software_quality_info_issues, + [MetricKey.blocker_violations]: MetricKey.software_quality_blocker_issues, + [MetricKey.critical_violations]: MetricKey.software_quality_high_issues, + [MetricKey.major_violations]: MetricKey.software_quality_medium_issues, + [MetricKey.minor_violations]: MetricKey.software_quality_low_issues, + [MetricKey.info_violations]: MetricKey.software_quality_info_issues, + [MetricKey.new_vulnerabilities]: MetricKey.new_software_quality_security_issues, + [MetricKey.new_bugs]: MetricKey.new_software_quality_reliability_issues, + [MetricKey.new_code_smells]: MetricKey.new_software_quality_maintainability_issues, + [MetricKey.vulnerabilities]: MetricKey.software_quality_security_issues, + [MetricKey.bugs]: MetricKey.software_quality_reliability_issues, + [MetricKey.code_smells]: MetricKey.software_quality_maintainability_issues, + ...SOFTWARE_QUALITY_RATING_METRICS_MAP, +}; + +export const MQR_CONDITIONS_MAP: Partial> = { + ...Object.fromEntries( + Object.entries(STANDARD_CONDITIONS_MAP).map(([key, value]) => [value, key]), + ), + [MetricKey.high_impact_accepted_issues]: null, +}; + export function isConditionWithFixedValue(condition: Condition) { return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as OptimizedCaycMetricKeys); } diff --git a/server/sonar-web/src/main/js/queries/quality-gates.ts b/server/sonar-web/src/main/js/queries/quality-gates.ts index 5b2524e4be2..acc880b1e73 100644 --- a/server/sonar-web/src/main/js/queries/quality-gates.ts +++ b/server/sonar-web/src/main/js/queries/quality-gates.ts @@ -19,6 +19,7 @@ */ import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useIntl } from 'react-intl'; import { addGlobalSuccessMessage } from '~design-system'; import { BranchParameters } from '~sonar-aligned/types/branch-like'; import { @@ -238,6 +239,42 @@ export function useUpdateConditionMutation(gateName: string) { }); } +export function useUpdateOrDeleteConditionsMutation(gateName: string, isSingleMetric?: boolean) { + const queryClient = useQueryClient(); + const intl = useIntl(); + + return useMutation({ + mutationFn: ( + conditions: (Omit & { metric: string | null | undefined })[], + ) => { + const promiseArr = conditions.map((condition) => + condition.metric + ? updateCondition(condition as Condition) + : deleteCondition({ id: condition.id }), + ); + + return Promise.all(promiseArr); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: qualityQuery.list() }); + queryClient.invalidateQueries({ queryKey: qualityQuery.detail(gateName) }); + addGlobalSuccessMessage( + intl.formatMessage( + { + id: isSingleMetric + ? 'quality_gates.condition_updated' + : 'quality_gates.conditions_updated_to_the_mode', + }, + { qualityGateName: gateName }, + ), + ); + }, + onError: () => { + queryClient.invalidateQueries({ queryKey: qualityQuery.detail(gateName) }); + }, + }); +} + export function useDeleteConditionMutation(gateName: string) { const queryClient = useQueryClient(); diff --git a/server/sonar-web/src/main/js/sonar-aligned/types/metrics.ts b/server/sonar-web/src/main/js/sonar-aligned/types/metrics.ts index d711330cfa7..db99c08030b 100644 --- a/server/sonar-web/src/main/js/sonar-aligned/types/metrics.ts +++ b/server/sonar-web/src/main/js/sonar-aligned/types/metrics.ts @@ -22,13 +22,16 @@ export enum MetricKey { accepted_issues = 'accepted_issues', alert_status = 'alert_status', blocker_violations = 'blocker_violations', + software_quality_blocker_issues = 'software_quality_blocker_issues', branch_coverage = 'branch_coverage', bugs = 'bugs', + software_quality_reliability_issues = 'software_quality_reliability_issues', burned_budget = 'burned_budget', business_value = 'business_value', class_complexity = 'class_complexity', classes = 'classes', code_smells = 'code_smells', + software_quality_maintainability_issues = 'software_quality_maintainability_issues', cognitive_complexity = 'cognitive_complexity', comment_lines = 'comment_lines', comment_lines_data = 'comment_lines_data', @@ -40,6 +43,7 @@ export enum MetricKey { confirmed_issues = 'confirmed_issues', coverage = 'coverage', critical_violations = 'critical_violations', + software_quality_high_issues = 'software_quality_high_issues', development_cost = 'development_cost', directories = 'directories', duplicated_blocks = 'duplicated_blocks', @@ -63,6 +67,7 @@ export enum MetricKey { generated_ncloc = 'generated_ncloc', high_impact_accepted_issues = 'high_impact_accepted_issues', info_violations = 'info_violations', + software_quality_info_issues = 'software_quality_info_issues', issues = 'issues', last_change_on_maintainability_rating = 'last_change_on_maintainability_rating', last_change_on_releasability_rating = 'last_change_on_releasability_rating', @@ -83,23 +88,30 @@ export enum MetricKey { maintainability_rating_effort = 'maintainability_rating_effort', software_quality_maintainability_rating_effort = 'software_quality_maintainability_rating_effort', major_violations = 'major_violations', + software_quality_medium_issues = 'software_quality_medium_issues', minor_violations = 'minor_violations', + software_quality_low_issues = 'software_quality_low_issues', ncloc = 'ncloc', ncloc_data = 'ncloc_data', ncloc_language_distribution = 'ncloc_language_distribution', new_accepted_issues = 'new_accepted_issues', new_blocker_violations = 'new_blocker_violations', + new_software_quality_blocker_issues = 'new_software_quality_blocker_issues', new_branch_coverage = 'new_branch_coverage', new_bugs = 'new_bugs', + new_software_quality_reliability_issues = 'new_software_quality_reliability_issues', new_code_smells = 'new_code_smells', + new_software_quality_maintainability_issues = 'new_software_quality_maintainability_issues', new_conditions_to_cover = 'new_conditions_to_cover', new_coverage = 'new_coverage', new_critical_violations = 'new_critical_violations', + new_software_quality_high_issues = 'new_software_quality_high_issues', new_development_cost = 'new_development_cost', new_duplicated_blocks = 'new_duplicated_blocks', new_duplicated_lines = 'new_duplicated_lines', new_duplicated_lines_density = 'new_duplicated_lines_density', new_info_violations = 'new_info_violations', + new_software_quality_info_issues = 'new_software_quality_info_issues', new_issues = 'new_issues', new_line_coverage = 'new_line_coverage', new_lines = 'new_lines', @@ -110,7 +122,9 @@ export enum MetricKey { new_maintainability_rating_distribution = 'new_maintainability_rating_distribution', new_software_quality_maintainability_rating_distribution = 'new_software_quality_maintainability_rating_distribution', new_major_violations = 'new_major_violations', + new_software_quality_medium_issues = 'new_software_quality_medium_issues', new_minor_violations = 'new_minor_violations', + new_software_quality_low_issues = 'new_software_quality_low_issues', new_reliability_issues = 'new_reliability_issues', new_reliability_rating = 'new_reliability_rating', new_software_quality_reliability_rating = 'new_software_quality_reliability_rating', @@ -138,6 +152,7 @@ export enum MetricKey { new_violations = 'new_violations', new_violations_rating = 'new_violations_rating', new_vulnerabilities = 'new_vulnerabilities', + new_software_quality_security_issues = 'new_software_quality_security_issues', open_issues = 'open_issues', prioritized_rule_issues = 'prioritized_rule_issues', projects = 'projects', @@ -195,6 +210,7 @@ export enum MetricKey { violations = 'violations', violations_rating = 'violations_rating', vulnerabilities = 'vulnerabilities', + software_quality_security_issues = 'software_quality_security_issues', wont_fix_issues = 'wont_fix_issues', } 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 e0abaec5dc3..eb50f757551 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2446,6 +2446,7 @@ quality_gates.condition_added=Successfully added condition. quality_gates.update_condition=Update Condition quality_gates.condition_updated=Successfully updated condition. quality_gates.conditions_updated=Successfully updated conditions. +quality_gates.conditions_updated_to_the_mode=The metrics of "{qualityGateName}" gate were successfully updated quality_gates.no_conditions=No Conditions quality_gates.health_icons=Project health icons represent: quality_gates.projects_for_default=Every project not specifically associated to a quality gate will be associated to this one by default. @@ -2554,6 +2555,19 @@ quality_gates.ai_generated.tootltip.message=Sonar way ensures clean AI-generated quality_gates.ai_generated.description=Sonar way ensures {link} quality_gates.ai_generated.description.clean_ai_generated_code=clean AI-generated code quality_gates.mqr_mode_update.tooltip.message=Update the metrics of this quality gate +quality_gates.update_conditions.update_metrics=Update metrics +quality_gates.update_conditions.header=Update all metrics of “{qualityGate}” gate +quality_gates.update_conditions.header.single_metric=Update this metric +quality_gates.update_conditions.description.line1=Metrics of the conditions listed below will be updated to align with the {mode} mode of this instance. +quality_gates.update_conditions.description.line2=They will be calculated differently even if the names of the conditions persist between the Standard Experience and the MQR modes. Operator and value will remain unchanged. +quality_gates.update_conditions.description.line3=Note that the update to {mode} might cause your quality gate to fail. {link} +quality_gates.update_conditions.description.link=For more information, refer to the documentation. +quality_gates.update_conditions.standard_mode_header=Standard Experience Metric +quality_gates.update_conditions.mqr_mode_header=MQR Mode Metric +quality_gates.update_conditions.operator_and_value_header=Operator and Value +quality_gates.update_conditions.removed=Condition will be removed +quality_gates.update_conditions.error=Failed to update some conditions + #------------------------------------------------------------------------------ #