} 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
<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>
)}
</>
);
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 = {
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'),
};
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();
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());
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 () => {
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();
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());
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() {
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;
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();
});
}
-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 }) => {
queryClient.invalidateQueries({ queryKey: ['settings', 'details', key] });
}
queryClient.invalidateQueries({ queryKey: ['settings', 'values', [key]] });
- addGlobalSuccessMessage(SETTINGS_SAVE_SUCCESS_MESSAGE);
+ addGlobalSuccessMessage(successMessage);
},
});
}
# 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