From 0ab85ea54a1919a5e51ddfef61edbd0fb60b68b2 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 26 Aug 2024 17:26:00 +0200 Subject: [PATCH] SONAR-22813 and SONAR-22814 Use the new Modal and ModalAlert --- server/sonar-web/__mocks__/@emotion/react.ts | 32 +++++ server/sonar-web/design-system/package.json | 2 +- .../src/components/modal/Modal.tsx | 23 ++++ server/sonar-web/package.json | 2 +- .../components/TaskActions.tsx | 23 ++-- .../main/js/apps/coding-rules/utils-tests.tsx | 2 +- .../components/PermissionsProjectApp.tsx | 13 +- .../components/PublicProjectDisclaimer.tsx | 4 +- .../main/js/apps/permissions/test-utils.ts | 2 +- .../__tests__/ProjectDeletionApp-it.tsx | 25 ++-- .../projectKey/__tests__/ProjectKeyApp-it.tsx | 2 +- .../components/AddConditionModal.tsx | 116 +++++++++++++----- .../quality-gates/components/Condition.tsx | 115 +++++------------ .../quality-gates/components/Conditions.tsx | 62 +--------- .../components/EditConditionModal.tsx | 69 ++++++++--- .../quality-gates/components/MetricSelect.tsx | 79 ++++-------- .../components/__tests__/QualityGate-it.tsx | 21 +++- .../__tests__/QualityProfilesApp-it.tsx | 3 +- .../almIntegration/AlmIntegrationRenderer.tsx | 15 ++- .../components/almIntegration/DeleteModal.tsx | 12 +- .../ConfirmProvisioningModal.tsx | 3 + .../GitHubAuthenticationTab.tsx | 5 +- .../GitHubConfigurationForm.tsx | 55 ++++----- .../GitLabAuthenticationTab.tsx | 3 +- .../authentication/SamlAuthenticationTab.tsx | 35 +++--- .../__tests__/Authentication-Github-it.tsx | 7 +- .../__tests__/Authentication-Gitlab-it.tsx | 4 +- .../js/components/controls/ConfirmButton.tsx | 7 +- .../js/components/controls/ConfirmModal.tsx | 38 +++--- .../js/components/controls/ModalButton.tsx | 4 + .../permissions/usePermissionChange.tsx | 1 + server/sonar-web/yarn.lock | 74 +++++++++-- 32 files changed, 483 insertions(+), 375 deletions(-) create mode 100644 server/sonar-web/__mocks__/@emotion/react.ts diff --git a/server/sonar-web/__mocks__/@emotion/react.ts b/server/sonar-web/__mocks__/@emotion/react.ts new file mode 100644 index 00000000000..1cae4bec7cf --- /dev/null +++ b/server/sonar-web/__mocks__/@emotion/react.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/** + * Mock Global from emotion which doesn't like jsdom + * (Throws `TypeError: node.setAttribute is not a function`) + */ +function Global() { + return null; +} + +module.exports = { + ...jest.requireActual('@emotion/react'), + Global, +}; diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index 7781300089c..33aa253317c 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -23,7 +23,7 @@ "@babel/preset-typescript": "7.24.7", "@emotion/babel-plugin": "11.11.0", "@emotion/babel-plugin-jsx-pragmatic": "0.2.1", - "@sonarsource/echoes-react": "0.5.0", + "@sonarsource/echoes-react": "0.6.0", "@testing-library/dom": "10.2.0", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "16.0.0", diff --git a/server/sonar-web/design-system/src/components/modal/Modal.tsx b/server/sonar-web/design-system/src/components/modal/Modal.tsx index 63589e5948d..680d0a501e4 100644 --- a/server/sonar-web/design-system/src/components/modal/Modal.tsx +++ b/server/sonar-web/design-system/src/components/modal/Modal.tsx @@ -75,6 +75,29 @@ function hasNoChildren(props: Partial): props is PropsWithSections { return (props as PropsWithChildren).children === undefined; } +/** @deprecated Use either Modal or ModalAlert from Echoes instead. + * + * The props have changed significantly: + * - `headerTitle` is now `title` + * - `headerDescription` is now `description` and is announced to screen readers. + * - `body` is replaced with `content` + * - `isLarge` is replaced with `size` (ModalSize.Default or ModalSize.Wide) + * - `isScrollable` and `isOverflowVisible` have been removed and the behavior is automatic! + * - `closeOnOverlayClick` has been removed and is either + * - always false for ModalAlert (it requires an action) + * or + * - always true for Modal + * + * By default, the Modal will be controlled automatically by its Trigger (child element). + * This is the preferred way. + * + * If you need to control the Modal (e.g. open as a side effect, close after async action): + * - `onClose` has been removed. Instead, use: + * - `onOpenChange`: callback for `isOpen` value changes. + * - `IsOpen`: controls the display of the Modal (conditional rendering isn't necessary anymore) + * + * See the {@link https://xtranet-sonarsource.atlassian.net/wiki/spaces/Platform/pages/3465543707/Modals | Migration Guide} for more + */ export function Modal({ closeOnOverlayClick = true, isLarge, diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 8af78e79abd..c0d1ac4c33a 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -13,7 +13,7 @@ "@primer/octicons-react": "19.10.0", "@react-spring/rafz": "9.7.3", "@react-spring/web": "9.7.3", - "@sonarsource/echoes-react": "0.5.0", + "@sonarsource/echoes-react": "0.6.0", "@tanstack/react-query": "5.18.1", "axios": "1.7.2", "classnames": "2.5.1", diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx index 1a91d5e740a..da40c748a0b 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx @@ -149,18 +149,17 @@ export default class TaskActions extends React.PureComponent { )} - {this.state.cancelTaskOpen && ( - - {translate('background_tasks.cancel_task.text')} - - )} + + {translate('background_tasks.cancel_task.text')} + {this.state.scannerContextOpen && ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx index b2dedfd2226..84c016051ba 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx @@ -179,7 +179,7 @@ const selectors = { // Custom rule form createCustomRuleDialog: byRole('dialog', { name: 'coding_rules.create_custom_rule' }), updateCustomRuleDialog: byRole('dialog', { name: 'coding_rules.update_custom_rule' }), - deleteCustomRuleDialog: byRole('dialog', { name: 'coding_rules.delete_rule' }), + deleteCustomRuleDialog: byRole('alertdialog', { name: 'coding_rules.delete_rule' }), ruleNameTextbox: byRole('textbox', { name: 'name' }), keyTextbox: byRole('textbox', { name: 'key' }), cleanCodeCategorySelect: byRole('combobox', { name: 'category' }), diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx index c0d22a92660..3741378ba1f 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx @@ -378,13 +378,12 @@ class PermissionsProjectApp extends React.PureComponent { isLoading={loading} /> - {disclaimer && ( - - )} + void; onConfirm: () => void; } -export default function PublicProjectDisclaimer({ component, onClose, onConfirm }: Props) { +export default function PublicProjectDisclaimer({ component, isOpen, onClose, onConfirm }: Props) { const { qualifier } = component; return ( {translate('projects_role.are_you_sure_to_turn_project_to_public.warning', qualifier)} diff --git a/server/sonar-web/src/main/js/apps/permissions/test-utils.ts b/server/sonar-web/src/main/js/apps/permissions/test-utils.ts index 3c0f93d2bec..f39396bd8cb 100644 --- a/server/sonar-web/src/main/js/apps/permissions/test-utils.ts +++ b/server/sonar-web/src/main/js/apps/permissions/test-utils.ts @@ -44,7 +44,7 @@ export function getPageObject(user: UserEvent) { githubExplanations: byText('roles.page.description.github'), gitlabLogo: byRole('img', { name: 'project_permission.managed.alm.gitlab' }), gitlabExplanations: byText('roles.page.description.gitlab'), - confirmRemovePermissionDialog: byRole('dialog', { + confirmRemovePermissionDialog: byRole('alertdialog', { name: 'project_permission.remove_only_confirmation_title', }), nonGHProjectWarning: byText('project_permission.local_project_with_github_provisioning'), diff --git a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/ProjectDeletionApp-it.tsx b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/ProjectDeletionApp-it.tsx index 4b0bc4294b4..d57c66693f5 100644 --- a/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/ProjectDeletionApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectDeletion/__tests__/ProjectDeletionApp-it.tsx @@ -53,10 +53,11 @@ it('should be able to delete project', async () => { expect(byText('deletion.page').get()).toBeInTheDocument(); expect(byText('project_deletion.page.description').get()).toBeInTheDocument(); - await user.click(byRole('button', { name: 'delete' }).get()); - expect(await byRole('dialog', { name: 'qualifier.delete.TRK' }).find()).toBeInTheDocument(); + await user.click(ui.deleteButton.get()); + expect(await ui.confirmationModal(ComponentQualifier.Project).find()).toBeInTheDocument(); + await user.click( - byRole('dialog', { name: 'qualifier.delete.TRK' }).byRole('button', { name: 'delete' }).get(), + ui.confirmationModal(ComponentQualifier.Project).byRole('button', { name: 'delete' }).get(), ); expect(await byText(/project_deletion.resource_dele/).find()).toBeInTheDocument(); @@ -80,11 +81,11 @@ it('should be able to delete Portfolio', async () => { expect(byText('deletion.page').get()).toBeInTheDocument(); expect(byText('portfolio_deletion.page.description').get()).toBeInTheDocument(); - await user.click(byRole('button', { name: 'delete' }).get()); + await user.click(ui.deleteButton.get()); - expect(await byRole('dialog', { name: 'qualifier.delete.VW' }).find()).toBeInTheDocument(); + expect(await ui.confirmationModal(ComponentQualifier.Portfolio).find()).toBeInTheDocument(); await user.click( - byRole('dialog', { name: 'qualifier.delete.VW' }).byRole('button', { name: 'delete' }).get(), + ui.confirmationModal(ComponentQualifier.Portfolio).byRole('button', { name: 'delete' }).get(), ); expect(await byText(/project_deletion.resource_dele/).find()).toBeInTheDocument(); @@ -108,10 +109,10 @@ it('should be able to delete Application', async () => { expect(byText('deletion.page').get()).toBeInTheDocument(); expect(byText('application_deletion.page.description').get()).toBeInTheDocument(); - await user.click(byRole('button', { name: 'delete' }).get()); - expect(await byRole('dialog', { name: 'qualifier.delete.APP' }).find()).toBeInTheDocument(); + await user.click(ui.deleteButton.get()); + expect(await ui.confirmationModal(ComponentQualifier.Application).find()).toBeInTheDocument(); await user.click( - byRole('dialog', { name: 'qualifier.delete.APP' }).byRole('button', { name: 'delete' }).get(), + ui.confirmationModal(ComponentQualifier.Application).byRole('button', { name: 'delete' }).get(), ); expect(await byText(/project_deletion.resource_dele/).find()).toBeInTheDocument(); @@ -132,3 +133,9 @@ function renderProjectDeletionApp(component?: Component) { , ); } + +const ui = { + confirmationModal: (qualifier: ComponentQualifier) => + byRole('alertdialog', { name: `qualifier.delete.${qualifier}` }), + deleteButton: byRole('button', { name: 'delete' }), +}; diff --git a/server/sonar-web/src/main/js/apps/projectKey/__tests__/ProjectKeyApp-it.tsx b/server/sonar-web/src/main/js/apps/projectKey/__tests__/ProjectKeyApp-it.tsx index a3b25328532..7e91c2e6d48 100644 --- a/server/sonar-web/src/main/js/apps/projectKey/__tests__/ProjectKeyApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/ProjectKeyApp-it.tsx @@ -77,7 +77,7 @@ function getPageObjects() { const ui = { pageTitle: byRole('heading', { name: 'update_key.page' }), - updateKeyDialog: byRole('dialog'), + updateKeyDialog: byRole('alertdialog'), newKeyInput: byRole('textbox'), updateInputButton: byRole('button', { name: 'update_verb' }), resetInputButton: byRole('button', { name: 'reset_verb' }), diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx index ed785c588f4..4f0bfadf919 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx @@ -17,13 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Button, ButtonVariety, RadioButtonGroup } from '@sonarsource/echoes-react'; -import { FormField, Modal } from 'design-system'; +import { Button, ButtonVariety, Modal, RadioButtonGroup } from '@sonarsource/echoes-react'; +import { FormField } from 'design-system'; +import { differenceWith, map } from 'lodash'; import * as React from 'react'; +import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; +import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; import { translate } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; import { useCreateConditionMutation } from '../../../queries/quality-gates'; -import { MetricKey } from '../../../sonar-aligned/types/metrics'; +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 ConditionOperator from './ConditionOperator'; @@ -31,38 +35,88 @@ import MetricSelect from './MetricSelect'; import ThresholdInput from './ThresholdInput'; interface Props { - metrics: Metric[]; - onClose: () => void; qualityGate: QualityGate; } +const FORBIDDEN_METRIC_TYPES = [MetricType.Data, MetricType.Distribution, 'STRING', 'BOOL']; +const FORBIDDEN_METRICS: string[] = [ + MetricKey.alert_status, + 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.software_quality_security_review_rating, + MetricKey.new_software_quality_security_review_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({ metrics, onClose, qualityGate }: Readonly) { +export default function AddConditionModal({ qualityGate }: Readonly) { + 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(); const [selectedOperator, setSelectedOperator] = React.useState(); const { mutateAsync: createCondition } = useCreateConditionMutation(qualityGate.name); + const { hasFeature } = useAvailableFeatures(); + const metrics = useMetrics(); const getSinglePossibleOperator = (metric: Metric) => { const operators = getPossibleOperators(metric); return Array.isArray(operators) ? undefined : operators; }; - const handleFormSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (selectedMetric) { - const newCondition: Omit = { - metric: selectedMetric.key, - op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator, - error: errorThreshold, - }; - await createCondition(newCondition); - onClose(); - } - }; + const { conditions = [] } = qualityGate; + + const availableMetrics = React.useMemo(() => { + return differenceWith( + map(metrics, (metric) => metric).filter( + (metric) => + !metric.hidden && + !FORBIDDEN_METRIC_TYPES.includes(metric.type) && + !FORBIDDEN_METRICS.includes(metric.key) && + !( + metric.key === MetricKey.prioritized_rule_issues && + !hasFeature(Feature.PrioritizedRules) + ), + ), + conditions, + (metric, condition) => metric.key === condition.metric, + ); + }, [conditions, hasFeature, metrics]); + + const handleFormSubmit = React.useCallback( + async (event: React.FormEvent) => { + event.preventDefault(); + + if (selectedMetric) { + const newCondition: Omit = { + metric: selectedMetric.key, + op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator, + error: errorThreshold, + }; + await createCondition(newCondition); + closeModal(); + } + }, + [closeModal, createCondition, errorThreshold, selectedMetric, selectedOperator], + ); const handleScopeChange = (scope: 'new' | 'overall') => { let correspondingMetric; @@ -70,7 +124,7 @@ export default function AddConditionModal({ metrics, onClose, qualityGate }: Rea if (selectedMetric) { const correspondingMetricKey = scope === 'new' ? `new_${selectedMetric.key}` : selectedMetric.key.replace(/^new_/, ''); - correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey); + correspondingMetric = availableMetrics.find((m) => m.key === correspondingMetricKey); } setScope(scope); @@ -114,8 +168,8 @@ export default function AddConditionModal({ metrics, onClose, qualityGate }: Rea label={translate('quality_gates.conditions.fails_when')} > + selectedMetric={selectedMetric} + metricsArray={availableMetrics.filter((m) => scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key), )} onMetricChange={handleMetricChange} @@ -155,14 +209,12 @@ export default function AddConditionModal({ metrics, onClose, qualityGate }: Rea return ( } - secondaryButtonLabel={translate('close')} - /> + secondaryButton={} + > + + ); } 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 51ba56ea28d..0ef31a504e3 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 @@ -18,18 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { - ActionCell, - ContentCell, - DangerButtonPrimary, - DestructiveIcon, - InteractiveIcon, - Modal, - NumericalCell, - PencilIcon, - TableRow, - TextError, - TrashIcon, -} from 'design-system'; + Button, + ButtonIcon, + ButtonSize, + ButtonVariety, + IconDelete, + ModalAlert, +} from '@sonarsource/echoes-react'; +import { ActionCell, ContentCell, NumericalCell, TableRow, TextError } from 'design-system'; import * as React from 'react'; import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; @@ -68,28 +64,10 @@ export default function ConditionComponent({ showEdit, isCaycModal, }: Readonly) { - const [deleteFormOpen, setDeleteFormOpen] = React.useState(false); - const [modal, setModal] = React.useState(false); const { mutateAsync: deleteCondition } = useDeleteConditionMutation(qualityGate.name); const metrics = useMetrics(); const { op = 'GT' } = condition; - const handleOpenUpdate = () => { - setModal(true); - }; - - const handleUpdateClose = () => { - setModal(false); - }; - - const handleDeleteClick = () => { - setDeleteFormOpen(true); - }; - - const closeDeleteForm = () => { - setDeleteFormOpen(false); - }; - const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; return ( @@ -116,63 +94,36 @@ export default function ConditionComponent({ !isConditionWithFixedValue(condition) || (isCaycCompliantAndOverCompliant && showEdit)) && !isNonEditableMetric(condition.metric as MetricKey) && ( - <> - - {modal && ( - - )} - + )} {(!isCaycCompliantAndOverCompliant || !condition.isCaycCondition || (isCaycCompliantAndOverCompliant && showEdit)) && ( - <> - - {deleteFormOpen && ( - deleteCondition(condition)} - > - {translate('delete')} - - } - secondaryButtonLabel={translate('close')} - /> + + primaryButton={ + + } + secondaryButtonLabel={translate('close')} + > + + )} )} 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 b540822929a..581878af0cc 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 @@ -31,15 +31,14 @@ import { Spinner, SubHeading, } from 'design-system'; -import { differenceWith, map, uniqBy } from 'lodash'; +import { uniqBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip'; -import { MetricKey } from '~sonar-aligned/types/metrics'; import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; import DocumentationLink from '../../../components/common/DocumentationLink'; -import ModalButton, { ModalProps } from '../../../components/controls/ModalButton'; +import { ModalProps } from '../../../components/controls/ModalButton'; import { DocLink } from '../../../helpers/doc-links'; import { useDocUrl } from '../../../helpers/docs'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; @@ -60,31 +59,6 @@ interface Props { qualityGate: QualityGate; } -const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL']; -const FORBIDDEN_METRICS: string[] = [ - MetricKey.alert_status, - 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.software_quality_security_review_rating, - MetricKey.new_software_quality_security_review_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, -]; - export default function Conditions({ qualityGate, isFetching }: Readonly) { const [editing, setEditing] = React.useState( qualityGate.caycStatus === CaycStatus.NonCompliant, @@ -118,30 +92,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly) setEditing(qualityGate.caycStatus === CaycStatus.NonCompliant); }, [name]); // eslint-disable-line react-hooks/exhaustive-deps - const renderConditionModal = React.useCallback( - ({ onClose }: ModalProps) => { - const { conditions = [] } = qualityGate; - const availableMetrics = differenceWith( - map(metrics, (metric) => metric).filter( - (metric) => - !metric.hidden && - !FORBIDDEN_METRIC_TYPES.includes(metric.type) && - !FORBIDDEN_METRICS.includes(metric.key) && - !( - metric.key === MetricKey.prioritized_rule_issues && - !hasFeature(Feature.PrioritizedRules) - ), - ), - conditions, - (metric, condition) => metric.key === condition.metric, - ); - return ( - - ); - }, - [metrics, qualityGate], - ); - const docUrl = useDocUrl(DocLink.CaYC); const isCompliantCustomQualityGate = qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn; @@ -221,13 +171,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly)
{(qualityGate.caycStatus === CaycStatus.NonCompliant || editing) && canEdit && ( - - {({ onClick }) => ( - - {translate('quality_gates.add_condition')} - - )} - + )}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx index 7c9a805007d..27857edd757 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx @@ -17,11 +17,18 @@ * 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 } from '@sonarsource/echoes-react'; -import { FormField, Highlight, Modal, Note } from 'design-system'; +import { + Button, + ButtonIcon, + ButtonSize, + ButtonVariety, + IconEdit, + Modal, +} from '@sonarsource/echoes-react'; +import { FormField, Highlight, Note } from 'design-system'; import { isArray } from 'lodash'; import * as React from 'react'; -import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; +import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; import { useUpdateConditionMutation } from '../../../queries/quality-gates'; import { Condition, Metric, QualityGate } from '../../../types/types'; import { getPossibleOperators } from '../utils'; @@ -32,18 +39,15 @@ interface Props { condition: Condition; header: string; metric: Metric; - onClose: () => void; qualityGate: QualityGate; } const EDIT_CONDITION_MODAL_ID = 'edit-condition-modal'; -export default function EditConditionModal({ - condition, - metric, - onClose, - qualityGate, -}: Readonly) { +export default function EditConditionModal({ condition, metric, qualityGate }: Readonly) { + const [open, setOpen] = React.useState(false); + const [submitting, setSubmitting] = React.useState(false); + const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : ''); const [selectedOperator, setSelectedOperator] = React.useState( @@ -59,13 +63,21 @@ export default function EditConditionModal({ const handleFormSubmit = async (event: React.FormEvent) => { event.preventDefault(); + setSubmitting(true); + const newCondition: Omit = { metric: metric.key, op: getSinglePossibleOperator(metric), error: errorThreshold, }; - await updateCondition({ id: condition.id, ...newCondition }); - onClose(); + try { + await updateCondition({ id: condition.id, ...newCondition }); + setOpen(false); + } catch (_) { + /* Error already handled */ + } + + setSubmitting(false); }; const handleErrorChange = (error: string) => { @@ -116,17 +128,34 @@ export default function EditConditionModal({ return ( + } - secondaryButtonLabel={translate('close')} - /> + secondaryButton={ + + } + isOpen={open} + onOpenChange={setOpen} + > + + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx index dd098646e45..73626d20cb1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx @@ -17,81 +17,50 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { LabelValueSelectOption, SearchSelectDropdown } from 'design-system'; -import { sortBy } from 'lodash'; +import { Select, SelectOption } from '@sonarsource/echoes-react'; import * as React from 'react'; -import { Options } from 'react-select'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; -import { getLocalizedMetricDomain, translate } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; +import { isDefined } from '../../../helpers/types'; import { Dict, Metric } from '../../../types/types'; import { getLocalizedMetricNameNoDiffMetric } from '../utils'; interface Props { - metric?: Metric; metrics: Dict; metricsArray: Metric[]; onMetricChange: (metric: Metric) => void; + selectedMetric?: Metric; } -interface Option { - isDisabled?: boolean; - label: string; - value: string; -} - -export function MetricSelect({ metric, metricsArray, metrics, onMetricChange }: Readonly) { - const handleChange = (option: Option | null) => { - if (option) { - const selectedMetric = metricsArray.find((metric) => metric.key === option.value); +export function MetricSelect({ + selectedMetric, + metricsArray, + metrics, + onMetricChange, +}: Readonly) { + const handleChange = (key: string | null) => { + if (isDefined(key)) { + const selectedMetric = metricsArray.find((metric) => metric.key === key); if (selectedMetric) { onMetricChange(selectedMetric); } } }; - const options: Array