aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2024-08-26 17:26:00 +0200
committersonartech <sonartech@sonarsource.com>2024-08-28 20:02:45 +0000
commit0ab85ea54a1919a5e51ddfef61edbd0fb60b68b2 (patch)
tree326a86917eefc92b47a2b75c6a9a75d6d53944d6 /server/sonar-web/src/main/js
parentd4fd4fcbbc8d658e9863483a00fa91ffa34bc078 (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/test-utils.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/projectDeletion/__tests__/ProjectDeletionApp-it.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/projectKey/__tests__/ProjectKeyApp-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionModal.tsx116
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx115
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/EditConditionModal.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/DeleteModal.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/ConfirmProvisioningModal.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubAuthenticationTab.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationForm.tsx55
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx38
-rw-r--r--server/sonar-web/src/main/js/components/controls/ModalButton.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/permissions/usePermissionChange.tsx1
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)}
>