]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23301 Enhance the experience for transition between Standard and MQR modes
authorstanislavh <stanislav.honcharov@sonarsource.com>
Mon, 28 Oct 2024 14:57:26 +0000 (15:57 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 5 Nov 2024 20:03:02 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/settings/components/Mode.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/Mode-it.tsx
server/sonar-web/src/main/js/queries/quality-gates.ts
server/sonar-web/src/main/js/queries/settings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0080d601240d8491efaf2996b0902c8f76b7685c..f04bdf7ea282059533adfca0e6a0a17e4b7bd859 100644 (file)
@@ -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() {
         <FormattedMessage id="settings.key_x" values={{ '0': SettingsKey.MQRMode }} />
       </Text>
       {changedMode && (
-        <>
-          <ButtonGroup className="sw-mt-6">
-            <Button
-              isDisabled={isPending}
-              isLoading={isPending}
-              aria-label={intl.formatMessage(
-                { id: 'settings.mode.save' },
-                { isStandardMode: !isStandardMode },
-              )}
-              onClick={handleSave}
-              variety={ButtonVariety.Primary}
-            >
-              {intl.formatMessage({ id: 'save' })}
-            </Button>
+        <div className="sw-mt-6">
+          <Spinner
+            isLoading={loadingGates}
+            label={intl.formatMessage({ id: 'settings.mode.checking_instance' })}
+          >
+            <ButtonGroup>
+              <Button
+                isDisabled={isPending}
+                isLoading={isPending}
+                aria-label={intl.formatMessage(
+                  { id: 'settings.mode.save' },
+                  { isStandardMode: !isStandardMode },
+                )}
+                onClick={handleSave}
+                variety={ButtonVariety.Primary}
+              >
+                {intl.formatMessage({ id: 'save' })}
+              </Button>
 
-            <Button isDisabled={isPending} onClick={() => setChangedMode(false)}>
-              {intl.formatMessage({ id: 'cancel' })}
-            </Button>
-          </ButtonGroup>
-          <Text as="div" size={TextSize.Small} className="sw-mt-2">
-            {intl.formatMessage({ id: 'settings.mode.save.warning' })}
-          </Text>
-        </>
+              <Button isDisabled={isPending} onClick={() => setChangedMode(false)}>
+                {intl.formatMessage({ id: 'cancel' })}
+              </Button>
+            </ButtonGroup>
+            <div>
+              {hasQGConditionsFromOtherMode ? (
+                <FlagMessage variant="info" className="sw-mt-6">
+                  {intl.formatMessage(
+                    { id: 'settings.mode.instance_conditions_from_other_mode' },
+                    { isStandardMode: isStandardMode && changedMode },
+                  )}
+                </FlagMessage>
+              ) : (
+                <Text size={TextSize.Small} className="sw-mt-2">
+                  {intl.formatMessage({ id: 'settings.mode.save.warning' })}
+                </Text>
+              )}
+            </div>
+          </Spinner>
+        </div>
       )}
     </>
   );
index f84120d72c55f0f3d461337d795bc022bfc9575a..ed25563dc0e42068a218860fb138e6ae6453896e 100644 (file)
 
 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() {
index d8f6a4e88423666cfded4be701d3efdf75d63359..d18dca5347401759a16064ec89ba159eaee4ed14 100644 (file)
@@ -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();
index 872d41bfe9ee88aed392b03c2f7e3b854aefa3f5..1ba0c6114c84d1a14b510f587d14077aac768be4 100644 (file)
@@ -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);
     },
   });
 }
index ea963b436ed82b229dc93c52b46e7d7901d4ff2a..d06d3c747b73fb745979dcccf1bdeb816a80e042 100644 (file)
@@ -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