From 90db97dd90b2c7375862768ce423492f9c26c02c Mon Sep 17 00:00:00 2001 From: stanislavh Date: Mon, 28 Oct 2024 15:57:26 +0100 Subject: [PATCH] SONAR-23301 Enhance the experience for transition between Standard and MQR modes --- .../main/js/apps/settings/components/Mode.tsx | 81 +++++++++++++------ .../settings/components/__tests__/Mode-it.tsx | 34 +++++++- .../src/main/js/queries/quality-gates.ts | 10 +-- .../sonar-web/src/main/js/queries/settings.ts | 7 +- .../resources/org/sonar/l10n/core.properties | 7 +- 5 files changed, 103 insertions(+), 36 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/settings/components/Mode.tsx b/server/sonar-web/src/main/js/apps/settings/components/Mode.tsx index 0080d601240..f04bdf7ea28 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Mode.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/Mode.tsx @@ -29,17 +29,32 @@ import { } from '@sonarsource/echoes-react'; import * as React from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { SelectionCard } from '~design-system'; +import { FlagMessage, SelectionCard } from '~design-system'; import DocumentationLink from '../../../components/common/DocumentationLink'; import { DocLink } from '../../../helpers/doc-links'; +import { useQualityGatesQuery } from '../../../queries/quality-gates'; import { useSaveSimpleValueMutation, useStandardExperienceMode } from '../../../queries/settings'; import { SettingsKey } from '../../../types/settings'; export function Mode() { + const intl = useIntl(); const { data: isStandardMode, isLoading } = useStandardExperienceMode(); - const { mutate: setMode, isPending } = useSaveSimpleValueMutation(true); const [changedMode, setChangedMode] = React.useState(false); - const intl = useIntl(); + const { mutate: setMode, isPending } = useSaveSimpleValueMutation( + true, + intl.formatMessage( + { + id: 'settings.mode.save.success', + }, + { isStandardMode: !isStandardMode && changedMode }, + ), + ); + const { data: { qualitygates } = {}, isLoading: loadingGates } = useQualityGatesQuery({ + enabled: changedMode, + }); + + const QGCheckKey = isStandardMode ? 'hasStandardConditions' : 'hasMQRConditions'; + const hasQGConditionsFromOtherMode = changedMode && qualitygates?.some((qg) => qg[QGCheckKey]); const handleSave = () => { // we need to invert because on BE we store isMQRMode @@ -112,29 +127,45 @@ export function Mode() { {changedMode && ( - <> - - +
+ + + - - - - {intl.formatMessage({ id: 'settings.mode.save.warning' })} - - + + +
+ {hasQGConditionsFromOtherMode ? ( + + {intl.formatMessage( + { id: 'settings.mode.instance_conditions_from_other_mode' }, + { isStandardMode: isStandardMode && changedMode }, + )} + + ) : ( + + {intl.formatMessage({ id: 'settings.mode.save.warning' })} + + )} +
+
+
)} ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Mode-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Mode-it.tsx index f84120d72c5..ed25563dc0e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Mode-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Mode-it.tsx @@ -20,21 +20,26 @@ import userEvent from '@testing-library/user-event'; import { byRole, byText } from '~sonar-aligned/helpers/testSelector'; +import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock'; import { definitions } from '../../../../helpers/mocks/definitions-list'; +import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { SettingsKey } from '../../../../types/settings'; import { Mode } from '../Mode'; let settingServiceMock: SettingsServiceMock; +let qualityGatesServiceMock: QualityGatesServiceMock; beforeAll(() => { settingServiceMock = new SettingsServiceMock(); + qualityGatesServiceMock = new QualityGatesServiceMock(); settingServiceMock.setDefinitions(definitions); }); afterEach(() => { settingServiceMock.reset(); + qualityGatesServiceMock.reset(); }); const ui = { @@ -42,6 +47,8 @@ const ui = { mqr: byRole('radio', { name: /settings.mode.mqr/ }), saveButton: byRole('button', { name: /settings.mode.save/ }), cancelButton: byRole('button', { name: 'cancel' }), + qgHasOtherModeConditionMessage: (isStandardMode = false) => + byText(`settings.mode.instance_conditions_from_other_mode.${isStandardMode}`), saveWarning: byText('settings.mode.save.warning'), }; @@ -55,13 +62,15 @@ it('should be able to select standard mode', async () => { expect(ui.saveButton.query()).not.toBeInTheDocument(); expect(ui.cancelButton.query()).not.toBeInTheDocument(); expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage().query()).not.toBeInTheDocument(); await user.click(ui.standard.get()); expect(ui.mqr.get()).not.toBeChecked(); expect(ui.standard.get()).toBeChecked(); expect(ui.saveButton.get()).toBeInTheDocument(); expect(ui.cancelButton.get()).toBeInTheDocument(); - expect(ui.saveWarning.get()).toBeInTheDocument(); + expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage().get()).toBeInTheDocument(); await user.click(ui.cancelButton.get()); expect(ui.mqr.get()).toBeChecked(); @@ -69,6 +78,7 @@ it('should be able to select standard mode', async () => { expect(ui.saveButton.query()).not.toBeInTheDocument(); expect(ui.cancelButton.query()).not.toBeInTheDocument(); expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage().query()).not.toBeInTheDocument(); await user.click(ui.standard.get()); await user.click(ui.saveButton.get()); @@ -77,6 +87,7 @@ it('should be able to select standard mode', async () => { expect(ui.saveButton.query()).not.toBeInTheDocument(); expect(ui.cancelButton.query()).not.toBeInTheDocument(); expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage().query()).not.toBeInTheDocument(); }); it('should be able to select mqr mode', async () => { @@ -90,13 +101,15 @@ it('should be able to select mqr mode', async () => { expect(ui.saveButton.query()).not.toBeInTheDocument(); expect(ui.cancelButton.query()).not.toBeInTheDocument(); expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage(true).query()).not.toBeInTheDocument(); await user.click(ui.mqr.get()); expect(ui.mqr.get()).toBeChecked(); expect(ui.standard.get()).not.toBeChecked(); expect(ui.saveButton.get()).toBeInTheDocument(); expect(ui.cancelButton.get()).toBeInTheDocument(); - expect(ui.saveWarning.get()).toBeInTheDocument(); + expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage(true).get()).toBeInTheDocument(); await user.click(ui.cancelButton.get()); expect(ui.mqr.get()).not.toBeChecked(); @@ -104,6 +117,7 @@ it('should be able to select mqr mode', async () => { expect(ui.saveButton.query()).not.toBeInTheDocument(); expect(ui.cancelButton.query()).not.toBeInTheDocument(); expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage(true).query()).not.toBeInTheDocument(); await user.click(ui.mqr.get()); await user.click(ui.saveButton.get()); @@ -112,6 +126,22 @@ it('should be able to select mqr mode', async () => { expect(ui.saveButton.query()).not.toBeInTheDocument(); expect(ui.cancelButton.query()).not.toBeInTheDocument(); expect(ui.saveWarning.query()).not.toBeInTheDocument(); + expect(ui.qgHasOtherModeConditionMessage(true).query()).not.toBeInTheDocument(); +}); + +it('should not see quality gate info message when there are no Quality Gates that have conditions from other mode', async () => { + const user = userEvent.setup(); + qualityGatesServiceMock.list = [ + mockQualityGate({ hasMQRConditions: false, hasStandardConditions: false }), + ]; + renderMode(); + + expect(await ui.standard.find()).toBeInTheDocument(); + expect(ui.mqr.get()).toBeChecked(); + + await user.click(ui.standard.get()); + expect(ui.qgHasOtherModeConditionMessage().query()).not.toBeInTheDocument(); + expect(ui.saveWarning.get()).toBeInTheDocument(); }); function renderMode() { diff --git a/server/sonar-web/src/main/js/queries/quality-gates.ts b/server/sonar-web/src/main/js/queries/quality-gates.ts index d8f6a4e8842..d18dca53474 100644 --- a/server/sonar-web/src/main/js/queries/quality-gates.ts +++ b/server/sonar-web/src/main/js/queries/quality-gates.ts @@ -40,7 +40,7 @@ import { import { getCorrectCaycCondition } from '../apps/quality-gates/utils'; import { translate } from '../helpers/l10n'; import { Condition, QualityGate } from '../types/types'; -import { createQueryHook } from './common'; +import { createQueryHook, StaleTime } from './common'; const QUERY_STALE_TIME = 5 * 60 * 1000; @@ -84,15 +84,15 @@ export function useComponentQualityGateQuery(project: string) { return useQualityGateQueryInner(name); } -export function useQualityGatesQuery() { - return useQuery({ +export const useQualityGatesQuery = createQueryHook(() => { + return queryOptions({ queryKey: qualityQuery.list(), queryFn: () => { return fetchQualityGates(); }, - staleTime: QUERY_STALE_TIME, + staleTime: StaleTime.LONG, }); -} +}); export function useCreateQualityGateMutation() { const queryClient = useQueryClient(); diff --git a/server/sonar-web/src/main/js/queries/settings.ts b/server/sonar-web/src/main/js/queries/settings.ts index 872d41bfe9e..1ba0c6114c8 100644 --- a/server/sonar-web/src/main/js/queries/settings.ts +++ b/server/sonar-web/src/main/js/queries/settings.ts @@ -143,7 +143,10 @@ export function useSaveValueMutation() { }); } -export function useSaveSimpleValueMutation(updateCache = false) { +export function useSaveSimpleValueMutation( + updateCache = false, + successMessage = SETTINGS_SAVE_SUCCESS_MESSAGE, +) { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ key, value }: { key: string; value: string }) => { @@ -163,7 +166,7 @@ export function useSaveSimpleValueMutation(updateCache = false) { queryClient.invalidateQueries({ queryKey: ['settings', 'details', key] }); } queryClient.invalidateQueries({ queryKey: ['settings', 'values', [key]] }); - addGlobalSuccessMessage(SETTINGS_SAVE_SUCCESS_MESSAGE); + addGlobalSuccessMessage(successMessage); }, }); } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ea963b436ed..d06d3c747b7 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1842,15 +1842,18 @@ settings.email_notification.header=SMTP Configuration # Mode settings.mode.title=Mode settings.mode.description.line1=There are two options to reflect the health of all the projects in this instance: {mqrLink} and {standardLink} -settings.mode.description.line2=Changing the mode will change how issues are categorized and ranked based on the results of the analysis. +settings.mode.description.line2=Changing the mode will change how issues are categorized and ranked based on the results of the analysis for all users. +settings.mode.checking_instance=Checking your instance... +settings.mode.instance_conditions_from_other_mode=Some of the Quality Gates in this instance are using metrics that belong to the {isStandardMode, select, true {Standard Experience} other {Multi-Quality Rule Mode}}. You will be able to update them once you save the changes. settings.mode.standard.name=Standard Experience settings.mode.mqr.name=Multi-Quality Rule (MQR) Mode settings.mode.standard.description.line1=Encompasses the traditional use of rule types such as bugs, code smells, and vulnerabilities, with a single category and severity level for each rule. settings.mode.standard.description.line2=This approach focuses on assigning severity to a rule and its issues based on the single software quality (for example, security, reliability or maintainability) it has the largest impact on. This is the rule categorization used in SonarQube 9.9 LTA and earlier. -settings.mode.mqr.description.line1=Aims to more accurately represent the impact software has on all software qualities. Very few issues impact only a single software quality. For instance, most vulnerabilities are also bugs. And vice versa. The MQR mode maps each rule to each of the qualities it impacts, with a separate severity rating for each quality. +settings.mode.mqr.description.line1=Aims to more accurately represent the impact issues have on all software qualities. Very few issues impact only a single software quality. For instance, most vulnerabilities are also bugs. And vice versa. The MQR mode maps each rule to each of the qualities it impacts, with a separate severity rating for each quality. settings.mode.mqr.description.line2=This approach focuses on ensuring the impact of an issue on all software qualities is clear, not just the most severe one. settings.mode.save.warning=Save changes to see them reflected in your instance settings.mode.save=Save the mode. The current mode will be switched to {isStandardMode, select, true {Standard Experience} other {Multi-Quality Rule Mode}} +settings.mode.save.success=This instance is now in {isStandardMode, select, true {Standard Experience} other {Multi-Quality Rule Mode}}. property.category.announcement=Announcement property.category.general=General -- 2.39.5