diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2024-08-26 17:26:00 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-08-28 20:02:45 +0000 |
commit | 0ab85ea54a1919a5e51ddfef61edbd0fb60b68b2 (patch) | |
tree | 326a86917eefc92b47a2b75c6a9a75d6d53944d6 /server/sonar-web/src/main/js | |
parent | d4fd4fcbbc8d658e9863483a00fa91ffa34bc078 (diff) | |
download | sonarqube-0ab85ea54a1919a5e51ddfef61edbd0fb60b68b2.tar.gz sonarqube-0ab85ea54a1919a5e51ddfef61edbd0fb60b68b2.zip |
SONAR-22813 and SONAR-22814 Use the new Modal and ModalAlert
Diffstat (limited to 'server/sonar-web/src/main/js')
27 files changed, 360 insertions, 365 deletions
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<Props, State> { )} </ActionsDropdown> - {this.state.cancelTaskOpen && ( - <ConfirmModal - cancelButtonText={translate('close')} - confirmButtonText={translate('background_tasks.cancel_task')} - header={translate('background_tasks.cancel_task')} - isDestructive - onClose={this.closeCancelTask} - onConfirm={this.handleCancelTask} - > - {translate('background_tasks.cancel_task.text')} - </ConfirmModal> - )} + <ConfirmModal + cancelButtonText={translate('close')} + confirmButtonText={translate('background_tasks.cancel_task')} + header={translate('background_tasks.cancel_task')} + isDestructive + isOpen={this.state.cancelTaskOpen} + onClose={this.closeCancelTask} + onConfirm={this.handleCancelTask} + > + {translate('background_tasks.cancel_task.text')} + </ConfirmModal> {this.state.scannerContextOpen && ( <ScannerContext onClose={this.closeScannerContext} task={task} /> 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<Props, State> { isLoading={loading} /> - {disclaimer && ( - <PublicProjectDisclaimer - component={component} - onClose={this.handleCloseDisclaimer} - onConfirm={this.handleTurnProjectToPublic} - /> - )} + <PublicProjectDisclaimer + component={component} + onClose={this.handleCloseDisclaimer} + onConfirm={this.handleTurnProjectToPublic} + isOpen={disclaimer} + /> </div> <AllHoldersList diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.tsx index b2d68fc6ca9..7c649c75e27 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.tsx @@ -28,11 +28,12 @@ interface Props { name: string; qualifier: string; }; + isOpen: boolean; onClose: () => void; onConfirm: () => void; } -export default function PublicProjectDisclaimer({ component, onClose, onConfirm }: Props) { +export default function PublicProjectDisclaimer({ component, isOpen, onClose, onConfirm }: Props) { const { qualifier } = component; return ( <ConfirmModal @@ -40,6 +41,7 @@ export default function PublicProjectDisclaimer({ component, onClose, onConfirm header={translateWithParameters('projects_role.turn_x_to_public', component.name)} onClose={onClose} onConfirm={onConfirm} + isOpen={isOpen} > <FlagMessage className="sw-mb-4" variant="warning"> {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) { </ComponentContext.Provider>, ); } + +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<Props>) { +export default function AddConditionModal({ qualityGate }: Readonly<Props>) { + 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 [selectedOperator, setSelectedOperator] = React.useState<string | undefined>(); 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<HTMLFormElement>) => { - event.preventDefault(); - - if (selectedMetric) { - const newCondition: Omit<Condition, 'id'> = { - 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<HTMLFormElement>) => { + event.preventDefault(); + + if (selectedMetric) { + const newCondition: Omit<Condition, 'id'> = { + 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')} > <MetricSelect - metric={selectedMetric} - metricsArray={metrics.filter((m) => + 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 ( <Modal - isScrollable={false} - isOverflowVisible - headerTitle={translate('quality_gates.add_condition')} - onClose={onClose} - body={renderBody()} + title={translate('quality_gates.add_condition')} + content={renderBody()} + isOpen={open} + onOpenChange={setOpen} primaryButton={ <Button - hasAutoFocus isDisabled={selectedMetric === undefined} id="add-condition-button" form={ADD_CONDITION_MODAL_ID} @@ -172,7 +224,11 @@ export default function AddConditionModal({ metrics, onClose, qualityGate }: Rea {translate('quality_gates.add_condition')} </Button> } - secondaryButtonLabel={translate('close')} - /> + secondaryButton={<Button onClick={closeModal}>{translate('close')}</Button>} + > + <Button data-test="quality-gates__add-condition"> + {translate('quality_gates.add_condition')} + </Button> + </Modal> ); } 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<Props>) { - 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) && ( - <> - <InteractiveIcon - Icon={PencilIcon} - aria-label={translateWithParameters( - 'quality_gates.condition.edit', - metric.name, - )} - data-test="quality-gates__condition-update" - onClick={handleOpenUpdate} - className="sw-mr-4" - size="small" - /> - {modal && ( - <EditConditionModal - condition={condition} - header={translate('quality_gates.update_condition')} - metric={metric} - onClose={handleUpdateClose} - qualityGate={qualityGate} - /> - )} - </> + <EditConditionModal + condition={condition} + header={translate('quality_gates.update_condition')} + metric={metric} + qualityGate={qualityGate} + /> )} {(!isCaycCompliantAndOverCompliant || !condition.isCaycCondition || (isCaycCompliantAndOverCompliant && showEdit)) && ( - <> - <DestructiveIcon - Icon={TrashIcon} - aria-label={translateWithParameters( - 'quality_gates.condition.delete', - metric.name, - )} - onClick={handleDeleteClick} - size="small" - /> - {deleteFormOpen && ( - <Modal - headerTitle={translate('quality_gates.delete_condition')} - onClose={closeDeleteForm} - body={translateWithParameters( - 'quality_gates.delete_condition.confirm.message', - getLocalizedMetricName(metric), - )} - primaryButton={ - <DangerButtonPrimary - autoFocus - type="submit" - onClick={() => deleteCondition(condition)} - > - {translate('delete')} - </DangerButtonPrimary> - } - secondaryButtonLabel={translate('close')} - /> + <ModalAlert + title={translate('quality_gates.delete_condition')} + description={translateWithParameters( + 'quality_gates.delete_condition.confirm.message', + getLocalizedMetricName(metric), )} - </> + primaryButton={ + <Button variety={ButtonVariety.Danger} onClick={() => deleteCondition(condition)}> + {translate('delete')} + </Button> + } + secondaryButtonLabel={translate('close')} + > + <ButtonIcon + Icon={IconDelete} + ariaLabel={translateWithParameters('quality_gates.condition.delete', metric.name)} + size={ButtonSize.Medium} + variety={ButtonVariety.DangerGhost} + /> + </ModalAlert> )} </> )} 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<Props>) { const [editing, setEditing] = React.useState<boolean>( qualityGate.caycStatus === CaycStatus.NonCompliant, @@ -118,30 +92,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>) 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 ( - <AddConditionModal metrics={availableMetrics} onClose={onClose} qualityGate={qualityGate} /> - ); - }, - [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<Props>) </div> <div> {(qualityGate.caycStatus === CaycStatus.NonCompliant || editing) && canEdit && ( - <ModalButton modal={renderConditionModal}> - {({ onClick }) => ( - <ButtonSecondary data-test="quality-gates__add-condition" onClick={onClick}> - {translate('quality_gates.add_condition')} - </ButtonSecondary> - )} - </ModalButton> + <AddConditionModal qualityGate={qualityGate} /> )} </div> </header> 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<Props>) { +export default function EditConditionModal({ condition, metric, qualityGate }: Readonly<Props>) { + 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<string | undefined>( @@ -59,13 +63,21 @@ export default function EditConditionModal({ const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); + setSubmitting(true); + const newCondition: Omit<Condition, 'id'> = { 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 ( <Modal - isScrollable={false} - isOverflowVisible - headerTitle={translate('quality_gates.update_condition')} - onClose={onClose} - body={renderBody()} + title={translate('quality_gates.update_condition')} + content={renderBody()} primaryButton={ - <Button form={EDIT_CONDITION_MODAL_ID} type="submit" variety={ButtonVariety.Primary}> + <Button + form={EDIT_CONDITION_MODAL_ID} + isLoading={submitting} + type="submit" + variety={ButtonVariety.Primary} + > {translate('quality_gates.update_condition')} </Button> } - secondaryButtonLabel={translate('close')} - /> + secondaryButton={ + <Button variety={ButtonVariety.Default} onClick={() => setOpen(false)}> + {translate('close')} + </Button> + } + isOpen={open} + onOpenChange={setOpen} + > + <ButtonIcon + Icon={IconEdit} + ariaLabel={translateWithParameters('quality_gates.condition.edit', metric.name)} + data-test="quality-gates__condition-update" + className="sw-mr-4" + size={ButtonSize.Medium} + variety={ButtonVariety.PrimaryGhost} + /> + </Modal> ); } 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<Metric>; metricsArray: Metric[]; onMetricChange: (metric: Metric) => void; + selectedMetric?: Metric; } -interface Option { - isDisabled?: boolean; - label: string; - value: string; -} - -export function MetricSelect({ metric, metricsArray, metrics, onMetricChange }: Readonly<Props>) { - const handleChange = (option: Option | null) => { - if (option) { - const selectedMetric = metricsArray.find((metric) => metric.key === option.value); +export function MetricSelect({ + selectedMetric, + metricsArray, + metrics, + onMetricChange, +}: Readonly<Props>) { + const handleChange = (key: string | null) => { + if (isDefined(key)) { + const selectedMetric = metricsArray.find((metric) => metric.key === key); if (selectedMetric) { onMetricChange(selectedMetric); } } }; - const options: Array<Option & { domain?: string }> = sortBy( - metricsArray.map((m) => ({ - value: m.key, - label: getLocalizedMetricNameNoDiffMetric(m, metrics), - domain: m.domain, - })), - 'domain', - ); - - // Use "disabled" property to emulate optgroups. - const optionsWithDomains: Option[] = []; - options.forEach((option, index, options) => { - const previous = index > 0 ? options[index - 1] : null; - if (option.domain && (!previous || previous.domain !== option.domain)) { - optionsWithDomains.push({ - value: '<domain>', - label: getLocalizedMetricDomain(option.domain), - isDisabled: true, - }); - } - optionsWithDomains.push(option); - }); - - const handleMetricsSearch = React.useCallback( - (query: string, resolve: (options: Options<LabelValueSelectOption<string>>) => void) => { - resolve(options.filter((opt) => opt.label.toLowerCase().includes(query.toLowerCase()))); - }, - [options], - ); + const options: SelectOption[] = metricsArray.map((m) => ({ + value: m.key, + label: getLocalizedMetricNameNoDiffMetric(m, metrics), + group: m.domain, + })); return ( - <SearchSelectDropdown - aria-label={translate('search.search_for_metrics')} - size="large" - controlSize="full" - inputId="condition-metric" - defaultOptions={optionsWithDomains} - loadOptions={handleMetricsSearch} + <Select + data={options} + value={selectedMetric?.key} onChange={handleChange} - placeholder={translate('search.search_for_metrics')} - controlLabel={ - optionsWithDomains.find((o) => o.value === metric?.key)?.label ?? translate('select_verb') - } + ariaLabel={translate('quality_gates.conditions.fails_when')} + isSearchable + isNotClearable /> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx index 42f453267d2..047c4106915 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx @@ -213,7 +213,9 @@ it('should be able to add a condition on new code', async () => { await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.new_code' }).get()); - await selectEvent.select(dialog.byRole('combobox').get(), 'Issues'); + await user.click(dialog.byLabelText('quality_gates.conditions.fails_when').get()); + await user.click(dialog.byRole('option', { name: 'Issues' }).get()); + await user.click( await dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).find(), ); @@ -236,10 +238,14 @@ it('should be able to add a condition on overall code', async () => { const dialog = byRole('dialog'); - await selectEvent.select(dialog.byRole('combobox').get(), ['Info Issues']); + await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get()); + + await user.click(dialog.byLabelText('quality_gates.conditions.fails_when').get()); + // In real app there are no metrics with selectable condition operator // so we manually changed direction for Info Issues to 0 to test this behavior - await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get()); + await user.click(await dialog.byRole('option', { name: 'Info Issues' }).find()); + await user.click(dialog.byLabelText('quality_gates.conditions.operator').get()); await user.click(dialog.byText('quality_gates.operator.LT').get()); @@ -268,7 +274,9 @@ it('should be able to select a rating', async () => { const dialog = byRole('dialog'); await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get()); - await selectEvent.select(dialog.byRole('combobox').get(), ['Maintainability Rating']); + await user.click(dialog.byLabelText('quality_gates.conditions.fails_when').get()); + await user.click(dialog.byRole('option', { name: 'Maintainability Rating' }).get()); + await user.click(dialog.byLabelText('quality_gates.conditions.value').get()); await user.click(dialog.byText('B').get()); await user.click(dialog.byRole('button', { name: 'quality_gates.add_condition' }).get()); @@ -334,7 +342,7 @@ it('should be able to handle delete condition', async () => { newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code'), ); - const dialog = within(screen.getByRole('dialog')); + const dialog = within(screen.getByRole('alertdialog')); await user.click(dialog.getByRole('button', { name: 'delete' })); await waitFor(() => { @@ -570,7 +578,8 @@ it('should not allow to change value of prioritized_rule_issues', async () => { const dialog = byRole('dialog'); await user.click(dialog.byRole('radio', { name: 'quality_gates.conditions.overall_code' }).get()); - await selectEvent.select(dialog.byRole('combobox').get(), ['Issues from prioritized rules']); + await user.click(dialog.byLabelText('quality_gates.conditions.fails_when').get()); + await user.click(dialog.byRole('option', { name: 'Issues from prioritized rules' }).get()); expect(dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).get()).toBeDisabled(); expect(dialog.byRole('textbox', { name: 'quality_gates.conditions.value' }).get()).toHaveValue( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx index 1fa99d63eea..3a2e6c88dd6 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx @@ -72,6 +72,7 @@ const ui = { compareDropdown: byRole('combobox', { name: 'quality_profiles.compare_with' }), changelogLink: byRole('link', { name: 'changelog' }), popup: byRole('dialog'), + confirmationModal: byRole('alertdialog'), restoreProfileDialog: byRole('dialog', { name: 'quality_profiles.restore_profile' }), copyRadio: byRole('radio', { name: 'quality_profiles.creation_from_copy quality_profiles.creation_from_copy_description_1 quality_profiles.creation_from_copy_description_2', @@ -357,7 +358,7 @@ it('should be able to activate or deactivate rules in comparison page', async () // Deactivate await user.click(await ui.deactivateRuleButton('java quality profile #2').find()); - expect(ui.popup.get()).toBeInTheDocument(); + expect(ui.confirmationModal.get()).toBeInTheDocument(); await user.click(ui.deactivateConfirmButton.get()); expect(ui.summaryAdditionalRules(1).query()).not.toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx index 10d65632660..e86daf521cf 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx @@ -165,14 +165,13 @@ export default function AlmIntegrationRenderer(props: AlmIntegrationRendererProp onUpdateDefinitions={props.onUpdateDefinitions} /> - {isDefined(definitionKeyForDeletion) && ( - <DeleteModal - id={definitionKeyForDeletion} - onCancel={props.onCancelDelete} - onDelete={props.onConfirmDelete} - projectCount={projectCount} - /> - )} + <DeleteModal + id={definitionKeyForDeletion} + isOpen={isDefined(definitionKeyForDeletion)} + onCancel={props.onCancelDelete} + onDelete={props.onConfirmDelete} + projectCount={projectCount} + /> </> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/DeleteModal.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/DeleteModal.tsx index 96b25eaea18..7334dd303d1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/DeleteModal.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/DeleteModal.tsx @@ -23,7 +23,8 @@ import ConfirmModal from '../../../../components/controls/ConfirmModal'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; export interface DeleteModalProps { - id: string; + id?: string; + isOpen: boolean; onCancel: () => void; onDelete: (id: string) => void; projectCount?: number; @@ -39,13 +40,20 @@ function showProjectCountWarning(projectCount?: number) { ) : null; } -export default function DeleteModal({ id, onDelete, onCancel, projectCount }: DeleteModalProps) { +export default function DeleteModal({ + id, + isOpen, + onDelete, + onCancel, + projectCount, +}: DeleteModalProps) { return ( <ConfirmModal confirmButtonText={translate('delete')} confirmData={id} header={translate('settings.almintegration.delete.header')} isDestructive + isOpen={isOpen} onClose={onCancel} onConfirm={onDelete} > diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfirmProvisioningModal.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfirmProvisioningModal.tsx index 7b430578b8b..5d6af12b6ed 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfirmProvisioningModal.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfirmProvisioningModal.tsx @@ -28,6 +28,7 @@ interface Props { allowUsersToSignUp?: boolean; hasProvisioningTypeChange?: boolean; isAllowListEmpty: boolean; + isOpen: boolean; onClose: VoidFunction; onConfirm: VoidFunction; provider: Provider.Github | Provider.Gitlab; @@ -39,6 +40,7 @@ export default function ConfirmProvisioningModal(props: Readonly<Props>) { allowUsersToSignUp, hasProvisioningTypeChange, isAllowListEmpty, + isOpen, onConfirm, onClose, provider, @@ -49,6 +51,7 @@ export default function ConfirmProvisioningModal(props: Readonly<Props>) { return ( <ConfirmModal + isOpen={isOpen} onConfirm={onConfirm} header={intl.formatMessage({ id: `settings.authentication.${provider}.confirm.${hasProvisioningTypeChange ? provisioningStatus : 'insecure'}`, diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubAuthenticationTab.tsx index 7c024a157d2..37ec3ec70e0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubAuthenticationTab.tsx @@ -345,17 +345,20 @@ export default function GitHubAuthenticationTab() { provisioningType={provisioningType ?? ProvisioningType.jit} synchronizationDetails={<GitHubSynchronisationWarning />} /> - {isConfirmProvisioningModalOpen && provisioningType && ( + + {provisioningType && ( <ConfirmProvisioningModal allowUsersToSignUp={allowUsersToSignUp} hasProvisioningTypeChange={changes?.provisioningType !== undefined} isAllowListEmpty={isEmpty(gitHubConfiguration.allowedOrganizations)} + isOpen={isConfirmProvisioningModalOpen} onClose={() => setIsConfirmProvisioningModalOpen(false)} onConfirm={onUpdateProvisioning} provider={Provider.Github} provisioningStatus={provisioningType} /> )} + {isMappingModalOpen && ( <GitHubMappingModal mapping={rolesMapping} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationForm.tsx index 23e5a512e67..693953ba25f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationForm.tsx @@ -17,8 +17,8 @@ * 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, Spinner } from '@sonarsource/echoes-react'; -import { FlagMessage, Modal } from 'design-system'; +import { Button, ButtonVariety, Modal } from '@sonarsource/echoes-react'; +import { FlagMessage } from 'design-system'; import { isEmpty, keyBy } from 'lodash'; import React, { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -228,35 +228,34 @@ export default function GitHubConfigurationForm(props: Readonly<Props>) { return ( <> <Modal - body={formBody} - headerTitle={header} - isScrollable - onClose={onClose} + content={formBody} + title={header} + isOpen + onOpenChange={onClose} primaryButton={ - <> - <Spinner className="sw-ml-2" isLoading={isCreating || isUpdating} /> - <Button - form={FORM_ID} - type="submit" - hasAutoFocus - isDisabled={!isFormValid} - variety={ButtonVariety.Primary} - > - <FormattedMessage id="settings.almintegration.form.save" /> - </Button> - </> + <Button + form={FORM_ID} + type="submit" + hasAutoFocus + isDisabled={!isFormValid} + isLoading={isCreating || isUpdating} + variety={ButtonVariety.Primary} + > + <FormattedMessage id="settings.almintegration.form.save" /> + </Button> } + secondaryButton={<Button onClick={onClose}>{translate('close')}</Button>} + /> + + <ConfirmProvisioningModal + allowUsersToSignUp={gitHubConfiguration?.allowUsersToSignUp} + isAllowListEmpty={isEmpty(gitHubConfiguration?.allowedOrganizations)} + isOpen={isConfirmModalOpen} + onClose={() => setIsConfirmModalOpen(false)} + onConfirm={onSave} + provider={Provider.Github} + provisioningStatus={gitHubConfiguration?.provisioningType ?? ProvisioningType.jit} /> - {isConfirmModalOpen && ( - <ConfirmProvisioningModal - allowUsersToSignUp={gitHubConfiguration?.allowUsersToSignUp} - isAllowListEmpty={isEmpty(gitHubConfiguration?.allowedOrganizations)} - onClose={() => setIsConfirmModalOpen(false)} - onConfirm={onSave} - provider={Provider.Github} - provisioningStatus={gitHubConfiguration?.provisioningType ?? ProvisioningType.jit} - /> - )} </> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx index 1499e8674f4..67dcab52af5 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx @@ -413,11 +413,12 @@ export default function GitLabAuthenticationTab() { </> )} </div> - {showConfirmProvisioningModal && provisioningType && ( + {provisioningType && ( <ConfirmProvisioningModal allowUsersToSignUp={allowUsersToSignUp} hasProvisioningTypeChange={Boolean(changes?.provisioningType)} isAllowListEmpty={isEmpty(allowedGroups)} + isOpen={showConfirmProvisioningModal} onClose={() => setShowConfirmProvisioningModal(false)} onConfirm={updateProvisioning} provider={Provider.Gitlab} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx index 42550d1ab9d..7715e16a60a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx @@ -204,24 +204,23 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { </> } /> - {showConfirmProvisioningModal && ( - <ConfirmModal - onConfirm={() => handleConfirmChangeProvisioning()} - header={translate( - 'settings.authentication.saml.confirm', - newScimStatus ? 'scim' : 'jit', - )} - onClose={() => setShowConfirmProvisioningModal(false)} - isDestructive={!newScimStatus} - confirmButtonText={translate('yes')} - > - {translate( - 'settings.authentication.saml.confirm', - newScimStatus ? 'scim' : 'jit', - 'description', - )} - </ConfirmModal> - )} + <ConfirmModal + onConfirm={() => handleConfirmChangeProvisioning()} + header={translate( + 'settings.authentication.saml.confirm', + newScimStatus ? 'scim' : 'jit', + )} + onClose={() => setShowConfirmProvisioningModal(false)} + isDestructive={!newScimStatus} + isOpen={showConfirmProvisioningModal} + confirmButtonText={translate('yes')} + > + {translate( + 'settings.authentication.saml.confirm', + newScimStatus ? 'scim' : 'jit', + 'description', + )} + </ConfirmModal> </> )} {showEditModal && ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx index 84dad7c9f3f..f7f8391ece1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx @@ -76,7 +76,7 @@ const ui = { textbox1: byRole('textbox', { name: 'test1' }), textbox2: byRole('textbox', { name: 'test2' }), tab: byRole('tab', { name: 'github GitHub' }), - cancelDialogButton: byRole('dialog').byRole('button', { name: 'cancel' }), + cancelDialogButton: byRole('alertdialog').byRole('button', { name: 'cancel' }), noGithubConfiguration: byText('settings.authentication.github.form.not_configured'), createConfigButton: ghContainer.byRole('button', { name: 'settings.authentication.form.create', @@ -153,7 +153,7 @@ const ui = { name: `settings.definition.delete_value.property.allowedOrganizations.name.${org}`, }), enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'), - insecureConfigWarning: byRole('dialog').byText( + insecureConfigWarning: byRole('alertdialog').byText( 'settings.authentication.github.provisioning_change.insecure_config', ), jitProvisioningButton: ghContainer.byRole('radio', { @@ -277,6 +277,7 @@ describe('Github tab', () => { await user.click(ui.deleteOrg('organization1').get()); await user.click(ui.saveConfigButton.get()); + await user.click(ui.confirmProvisioningButton.get()); await user.click(await ui.editConfigButton.find()); @@ -985,7 +986,7 @@ describe('Github tab', () => { await user.click(ui.saveGithubProvisioning.get()); expect(ui.insecureConfigWarning.get()).toBeInTheDocument(); - await user.click(ui.confirmProvisioningButton.get()); + await user.click(await ui.confirmProvisioningButton.find()); await user.click(ui.projectVisibility.get()); await user.click(ui.saveGithubProvisioning.get()); diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx index 5d25656e2b3..d8735204cee 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx @@ -118,10 +118,10 @@ const ui = { confirmAutoProvisioningDialog: byRole('dialog', { name: 'settings.authentication.gitlab.confirm.AUTO_PROVISIONING', }), - confirmJitProvisioningDialog: byRole('dialog', { + confirmJitProvisioningDialog: byRole('alertdialog', { name: 'settings.authentication.gitlab.confirm.JIT', }), - confirmInsecureProvisioningDialog: byRole('dialog', { + confirmInsecureProvisioningDialog: byRole('alertdialog', { name: 'settings.authentication.gitlab.confirm.insecure', }), confirmProvisioningChange: byRole('button', { diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx index 28f878067e7..448c817abf8 100644 --- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx @@ -17,11 +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. */ +import * as Echoes from '@sonarsource/echoes-react'; import * as React from 'react'; import ConfirmModal, { ConfirmModalProps } from './ConfirmModal'; import ModalButton, { ChildrenProps, ModalProps } from './ModalButton'; -interface Props<T> extends Omit<ConfirmModalProps<T>, 'children'> { +interface Props<T> extends Omit<ConfirmModalProps<T>, 'children' | 'isOpen'> { children: (props: ChildrenProps) => React.ReactNode; modalBody: React.ReactNode; modalHeader: string; @@ -32,6 +33,9 @@ interface State { modal: boolean; } +/** @deprecated Use {@link Echoes.ModalAlert | ModalAlert} from Echoes instead. + * See the {@link https://xtranet-sonarsource.atlassian.net/wiki/spaces/Platform/pages/3465543707/Modals | Migration Guide} + */ export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> { renderConfirmModal = ({ onClose }: ModalProps) => { const { children, modalBody, modalHeader, modalHeaderDescription, ...confirmModalProps } = @@ -41,6 +45,7 @@ export default class ConfirmButton<T> extends React.PureComponent<Props<T>, Stat header={modalHeader} headerDescription={modalHeaderDescription} onClose={onClose} + isOpen {...confirmModalProps} > {modalBody} diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx index 3e203077675..8d460a59c89 100644 --- a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx @@ -17,10 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ButtonPrimary, DangerButtonPrimary, Modal } from 'design-system'; +import * as Echoes from '@sonarsource/echoes-react'; +import { Button, ButtonVariety, ModalAlert } from '@sonarsource/echoes-react'; import React from 'react'; import { translate } from '../../helpers/l10n'; -import ClickEventBoundary from './ClickEventBoundary'; export interface ConfirmModalProps<T> { cancelButtonText?: string; @@ -29,6 +29,7 @@ export interface ConfirmModalProps<T> { confirmData?: T; confirmDisable?: boolean; isDestructive?: boolean; + isOpen: boolean; onConfirm: (data?: T) => void | Promise<void | Response>; } @@ -38,6 +39,9 @@ interface Props<T> extends ConfirmModalProps<T> { onClose: () => void; } +/** @deprecated Use {@link Echoes.ModalAlert | ModalAlert} from Echoes instead. + * See the {@link https://xtranet-sonarsource.atlassian.net/wiki/spaces/Platform/pages/3465543707/Modals | Migration Guide} + */ export default function ConfirmModal<T = string>(props: Readonly<Props<T>>) { const { header, @@ -49,6 +53,7 @@ export default function ConfirmModal<T = string>(props: Readonly<Props<T>>) { confirmDisable, headerDescription, isDestructive, + isOpen, cancelButtonText = translate('cancel'), } = props; @@ -61,37 +66,38 @@ export default function ConfirmModal<T = string>(props: Readonly<Props<T>>) { if (result) { return result.then( () => { + setSubmitting(false); onClose(); }, () => { - /* noop */ + setSubmitting(false); }, ); } + setSubmitting(false); onClose(); return undefined; }, [confirmData, onClose, onConfirm, setSubmitting]); - const Button = isDestructive ? DangerButtonPrimary : ButtonPrimary; - return ( - <Modal - headerTitle={header} - headerDescription={headerDescription} - body={ - <ClickEventBoundary> - <>{children}</> - </ClickEventBoundary> - } + <ModalAlert + title={header} + description={headerDescription} + isOpen={isOpen} + onOpenChange={onClose} + content={children} primaryButton={ - <Button autoFocus disabled={submitting || confirmDisable} onClick={handleSubmit}> + <Button + variety={isDestructive ? ButtonVariety.Danger : ButtonVariety.Primary} + isDisabled={submitting || confirmDisable} + isLoading={submitting} + onClick={handleSubmit} + > {confirmButtonText} </Button> } secondaryButtonLabel={cancelButtonText} - loading={submitting} - onClose={onClose} /> ); } diff --git a/server/sonar-web/src/main/js/components/controls/ModalButton.tsx b/server/sonar-web/src/main/js/components/controls/ModalButton.tsx index 8abdbb7f70d..0a2f9eecea0 100644 --- a/server/sonar-web/src/main/js/components/controls/ModalButton.tsx +++ b/server/sonar-web/src/main/js/components/controls/ModalButton.tsx @@ -17,6 +17,7 @@ * 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 Echoes from '@sonarsource/echoes-react'; import * as React from 'react'; export interface ChildrenProps { @@ -37,6 +38,9 @@ interface State { modal: boolean; } +/** @deprecated Use either {@link Echoes.Modal | Modal} or {@link Echoes.ModalAlert | ModalAlert} from Echoes instead. + * See the {@link https://xtranet-sonarsource.atlassian.net/wiki/spaces/Platform/pages/3465543707/Modals | Migration Guide} + */ export default class ModalButton extends React.PureComponent<Props, State> { mounted = false; state: State = { modal: false }; diff --git a/server/sonar-web/src/main/js/components/permissions/usePermissionChange.tsx b/server/sonar-web/src/main/js/components/permissions/usePermissionChange.tsx index 2bfb7c3a384..1d9381a70f2 100644 --- a/server/sonar-web/src/main/js/components/permissions/usePermissionChange.tsx +++ b/server/sonar-web/src/main/js/components/permissions/usePermissionChange.tsx @@ -79,6 +79,7 @@ export default function usePermissionChange<T extends PermissionGroup | Permissi confirmButtonText={translate('confirm')} header={translate('project_permission.remove_only_confirmation_title')} isDestructive + isOpen onClose={() => setConfirmPermission(null)} onConfirm={() => handleChangePermission(confirmPermission.key)} > |